execsql2 2.15.1__tar.gz → 2.15.4__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 (297) hide show
  1. {execsql2-2.15.1 → execsql2-2.15.4}/CHANGELOG.md +34 -0
  2. {execsql2-2.15.1 → execsql2-2.15.4}/PKG-INFO +11 -2
  3. {execsql2-2.15.1 → execsql2-2.15.4}/README.md +3 -1
  4. {execsql2-2.15.1 → execsql2-2.15.4}/docs/about/divergence.md +5 -0
  5. {execsql2-2.15.1 → execsql2-2.15.4}/docs/reference/security.md +2 -2
  6. {execsql2-2.15.1 → execsql2-2.15.4}/pyproject.toml +6 -2
  7. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/config.py +1 -1
  8. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/duckdb.py +6 -7
  9. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/sqlite.py +46 -46
  10. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/parser.py +3 -2
  11. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/types.py +28 -15
  12. execsql2-2.15.4/tests/importers/test_csv_edge_cases.py +209 -0
  13. execsql2-2.15.4/tests/test_debug_repl.py +555 -0
  14. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_format.py +119 -0
  15. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_parser.py +81 -2
  16. {execsql2-2.15.1 → execsql2-2.15.4}/uv.lock +64 -2
  17. execsql2-2.15.1/CLAUDE.md +0 -56
  18. {execsql2-2.15.1 → execsql2-2.15.4}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  19. {execsql2-2.15.1 → execsql2-2.15.4}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  20. {execsql2-2.15.1 → execsql2-2.15.4}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  21. {execsql2-2.15.1 → execsql2-2.15.4}/.github/workflows/ci-cd.yml +0 -0
  22. {execsql2-2.15.1 → execsql2-2.15.4}/.gitignore +0 -0
  23. {execsql2-2.15.1 → execsql2-2.15.4}/.pre-commit-config.yaml +0 -0
  24. {execsql2-2.15.1 → execsql2-2.15.4}/.pre-commit-hooks.yaml +0 -0
  25. {execsql2-2.15.1 → execsql2-2.15.4}/.python-version +0 -0
  26. {execsql2-2.15.1 → execsql2-2.15.4}/.readthedocs.yaml +0 -0
  27. {execsql2-2.15.1 → execsql2-2.15.4}/CONTRIBUTING.md +0 -0
  28. {execsql2-2.15.1 → execsql2-2.15.4}/LICENSE.txt +0 -0
  29. {execsql2-2.15.1 → execsql2-2.15.4}/NOTICE +0 -0
  30. {execsql2-2.15.1 → execsql2-2.15.4}/SECURITY.md +0 -0
  31. {execsql2-2.15.1 → execsql2-2.15.4}/docs/about/contributors.md +0 -0
  32. {execsql2-2.15.1 → execsql2-2.15.4}/docs/about/copyright.md +0 -0
  33. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/cli.md +0 -0
  34. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/db.md +0 -0
  35. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/exporters.md +0 -0
  36. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/importers.md +0 -0
  37. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/index.md +0 -0
  38. {execsql2-2.15.1 → execsql2-2.15.4}/docs/api/metacommands.md +0 -0
  39. {execsql2-2.15.1 → execsql2-2.15.4}/docs/dev/adding_db_adapters.md +0 -0
  40. {execsql2-2.15.1 → execsql2-2.15.4}/docs/dev/adding_exporters.md +0 -0
  41. {execsql2-2.15.1 → execsql2-2.15.4}/docs/dev/adding_importers.md +0 -0
  42. {execsql2-2.15.1 → execsql2-2.15.4}/docs/dev/adding_metacommands.md +0 -0
  43. {execsql2-2.15.1 → execsql2-2.15.4}/docs/dev/architecture.md +0 -0
  44. {execsql2-2.15.1 → execsql2-2.15.4}/docs/getting-started/installation.md +0 -0
  45. {execsql2-2.15.1 → execsql2-2.15.4}/docs/getting-started/requirements.md +0 -0
  46. {execsql2-2.15.1 → execsql2-2.15.4}/docs/getting-started/syntax.md +0 -0
  47. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/debugging.md +0 -0
  48. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/documentation.md +0 -0
  49. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/encoding.md +0 -0
  50. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/examples.md +0 -0
  51. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/formatter.md +0 -0
  52. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/logging.md +0 -0
  53. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/sql_syntax.md +0 -0
  54. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/usage.md +0 -0
  55. {execsql2-2.15.1 → execsql2-2.15.4}/docs/guides/using_scripts.md +0 -0
  56. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/Compare_planets.png +0 -0
  57. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/actions.png +0 -0
  58. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/actions2.png +0 -0
  59. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/checkboxes.png +0 -0
  60. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/connect.b64 +0 -0
  61. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/connect.png +0 -0
  62. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/create_conf.png +0 -0
  63. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/data_error1_screenshot.jpg +0 -0
  64. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/entry_form.png +0 -0
  65. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/execsql_console.png +0 -0
  66. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/execsql_logo_01.png +0 -0
  67. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/fatals.png +0 -0
  68. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/logo_small.png +0 -0
  69. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/pause_terminal.png +0 -0
  70. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/pause_terminal_sm.b64 +0 -0
  71. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/pause_terminal_sm.png +0 -0
  72. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/prompt_compare.png +0 -0
  73. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/set_build_commands.jpg +0 -0
  74. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/unit_conversions.b64 +0 -0
  75. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/unit_conversions_029.png +0 -0
  76. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/unmatched.png +0 -0
  77. {execsql2-2.15.1 → execsql2-2.15.4}/docs/images/vim_execsql_highlight.png +0 -0
  78. {execsql2-2.15.1 → execsql2-2.15.4}/docs/index.md +0 -0
  79. {execsql2-2.15.1 → execsql2-2.15.4}/docs/reference/configuration.md +0 -0
  80. {execsql2-2.15.1 → execsql2-2.15.4}/docs/reference/metacommands.md +0 -0
  81. {execsql2-2.15.1 → execsql2-2.15.4}/docs/reference/substitution_vars.md +0 -0
  82. {execsql2-2.15.1 → execsql2-2.15.4}/extras/vscode-execsql/README.md +0 -0
  83. {execsql2-2.15.1 → execsql2-2.15.4}/extras/vscode-execsql/package.json +0 -0
  84. {execsql2-2.15.1 → execsql2-2.15.4}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  85. {execsql2-2.15.1 → execsql2-2.15.4}/justfile +0 -0
  86. {execsql2-2.15.1 → execsql2-2.15.4}/scripts/generate_vscode_grammar.py +0 -0
  87. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/__init__.py +0 -0
  88. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/__main__.py +0 -0
  89. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/cli/__init__.py +0 -0
  90. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/cli/dsn.py +0 -0
  91. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/cli/help.py +0 -0
  92. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/cli/lint.py +0 -0
  93. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/cli/run.py +0 -0
  94. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/__init__.py +0 -0
  95. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/access.py +0 -0
  96. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/base.py +0 -0
  97. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/dsn.py +0 -0
  98. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/factory.py +0 -0
  99. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/firebird.py +0 -0
  100. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/mysql.py +0 -0
  101. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/oracle.py +0 -0
  102. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/postgres.py +0 -0
  103. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/db/sqlserver.py +0 -0
  104. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/debug/__init__.py +0 -0
  105. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/debug/repl.py +0 -0
  106. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exceptions.py +0 -0
  107. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/__init__.py +0 -0
  108. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/base.py +0 -0
  109. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/delimited.py +0 -0
  110. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/duckdb.py +0 -0
  111. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/feather.py +0 -0
  112. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/html.py +0 -0
  113. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/json.py +0 -0
  114. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/latex.py +0 -0
  115. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/markdown.py +0 -0
  116. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/ods.py +0 -0
  117. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/parquet.py +0 -0
  118. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/pretty.py +0 -0
  119. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/protocol.py +0 -0
  120. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/raw.py +0 -0
  121. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/sqlite.py +0 -0
  122. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/templates.py +0 -0
  123. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/values.py +0 -0
  124. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/xls.py +0 -0
  125. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/xlsx.py +0 -0
  126. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/xml.py +0 -0
  127. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/yaml.py +0 -0
  128. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/exporters/zip.py +0 -0
  129. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/format.py +0 -0
  130. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/gui/__init__.py +0 -0
  131. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/gui/base.py +0 -0
  132. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/gui/console.py +0 -0
  133. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/gui/desktop.py +0 -0
  134. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/gui/tui.py +0 -0
  135. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/__init__.py +0 -0
  136. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/base.py +0 -0
  137. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/csv.py +0 -0
  138. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/feather.py +0 -0
  139. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/json.py +0 -0
  140. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/ods.py +0 -0
  141. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/importers/xls.py +0 -0
  142. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/__init__.py +0 -0
  143. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/conditions.py +0 -0
  144. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/connect.py +0 -0
  145. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/control.py +0 -0
  146. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/data.py +0 -0
  147. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/debug.py +0 -0
  148. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/dispatch.py +0 -0
  149. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/io.py +0 -0
  150. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/io_export.py +0 -0
  151. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/io_fileops.py +0 -0
  152. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/io_import.py +0 -0
  153. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/io_write.py +0 -0
  154. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/prompt.py +0 -0
  155. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/script_ext.py +0 -0
  156. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/system.py +0 -0
  157. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/metacommands/upsert.py +0 -0
  158. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/models.py +0 -0
  159. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/py.typed +0 -0
  160. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/script/__init__.py +0 -0
  161. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/script/control.py +0 -0
  162. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/script/engine.py +0 -0
  163. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/script/variables.py +0 -0
  164. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/state.py +0 -0
  165. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/__init__.py +0 -0
  166. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/auth.py +0 -0
  167. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/crypto.py +0 -0
  168. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/datetime.py +0 -0
  169. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/errors.py +0 -0
  170. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/fileio.py +0 -0
  171. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/gui.py +0 -0
  172. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/mail.py +0 -0
  173. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/numeric.py +0 -0
  174. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/regex.py +0 -0
  175. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/strings.py +0 -0
  176. {execsql2-2.15.1 → execsql2-2.15.4}/src/execsql/utils/timer.py +0 -0
  177. {execsql2-2.15.1 → execsql2-2.15.4}/templates/README.md +0 -0
  178. {execsql2-2.15.1 → execsql2-2.15.4}/templates/config_settings.sqlite +0 -0
  179. {execsql2-2.15.1 → execsql2-2.15.4}/templates/example_config_prompt.sql +0 -0
  180. {execsql2-2.15.1 → execsql2-2.15.4}/templates/execsql.conf +0 -0
  181. {execsql2-2.15.1 → execsql2-2.15.4}/templates/make_config_db.sql +0 -0
  182. {execsql2-2.15.1 → execsql2-2.15.4}/templates/md_compare.sql +0 -0
  183. {execsql2-2.15.1 → execsql2-2.15.4}/templates/md_glossary.sql +0 -0
  184. {execsql2-2.15.1 → execsql2-2.15.4}/templates/md_upsert.sql +0 -0
  185. {execsql2-2.15.1 → execsql2-2.15.4}/templates/pg_compare.sql +0 -0
  186. {execsql2-2.15.1 → execsql2-2.15.4}/templates/pg_glossary.sql +0 -0
  187. {execsql2-2.15.1 → execsql2-2.15.4}/templates/pg_upsert.sql +0 -0
  188. {execsql2-2.15.1 → execsql2-2.15.4}/templates/script_template.sql +0 -0
  189. {execsql2-2.15.1 → execsql2-2.15.4}/templates/ss_compare.sql +0 -0
  190. {execsql2-2.15.1 → execsql2-2.15.4}/templates/ss_glossary.sql +0 -0
  191. {execsql2-2.15.1 → execsql2-2.15.4}/templates/ss_upsert.sql +0 -0
  192. {execsql2-2.15.1 → execsql2-2.15.4}/tests/__init__.py +0 -0
  193. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/__init__.py +0 -0
  194. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_cli.py +0 -0
  195. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_cli_e2e.py +0 -0
  196. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_cli_run.py +0 -0
  197. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_lint.py +0 -0
  198. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_ping.py +0 -0
  199. {execsql2-2.15.1 → execsql2-2.15.4}/tests/cli/test_profile.py +0 -0
  200. {execsql2-2.15.1 → execsql2-2.15.4}/tests/conftest.py +0 -0
  201. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/__init__.py +0 -0
  202. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_base.py +0 -0
  203. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_duckdb.py +0 -0
  204. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_factory.py +0 -0
  205. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_postgres.py +0 -0
  206. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_sqlite.py +0 -0
  207. {execsql2-2.15.1 → execsql2-2.15.4}/tests/db/test_sqlite_extra.py +0 -0
  208. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/__init__.py +0 -0
  209. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_base.py +0 -0
  210. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_db.py +0 -0
  211. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_delimited.py +0 -0
  212. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_duckdb_exporter.py +0 -0
  213. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_exporters.py +0 -0
  214. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_feather.py +0 -0
  215. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_html_extended.py +0 -0
  216. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_html_latex.py +0 -0
  217. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_json.py +0 -0
  218. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_json_extended.py +0 -0
  219. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_latex_extended.py +0 -0
  220. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_markdown.py +0 -0
  221. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_ods.py +0 -0
  222. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_parquet.py +0 -0
  223. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_pretty_extended.py +0 -0
  224. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_raw_extended.py +0 -0
  225. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_sqlite_exporter.py +0 -0
  226. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_templates.py +0 -0
  227. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_templates_extended.py +0 -0
  228. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_values_extended.py +0 -0
  229. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_xls_xlsx.py +0 -0
  230. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_xlsx.py +0 -0
  231. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_xml.py +0 -0
  232. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_yaml.py +0 -0
  233. {execsql2-2.15.1 → execsql2-2.15.4}/tests/exporters/test_zip.py +0 -0
  234. {execsql2-2.15.1 → execsql2-2.15.4}/tests/gui/__init__.py +0 -0
  235. {execsql2-2.15.1 → execsql2-2.15.4}/tests/gui/test_backends.py +0 -0
  236. {execsql2-2.15.1 → execsql2-2.15.4}/tests/gui/test_compare_stats.py +0 -0
  237. {execsql2-2.15.1 → execsql2-2.15.4}/tests/gui/test_compute_row_diffs.py +0 -0
  238. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/__init__.py +0 -0
  239. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_base_extended.py +0 -0
  240. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_csv_importer.py +0 -0
  241. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_feather_importer.py +0 -0
  242. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_json_importer.py +0 -0
  243. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_ods_importer.py +0 -0
  244. {execsql2-2.15.1 → execsql2-2.15.4}/tests/importers/test_xls_importer.py +0 -0
  245. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/__init__.py +0 -0
  246. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/conftest.py +0 -0
  247. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/test_dsn.py +0 -0
  248. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/test_duckdb.py +0 -0
  249. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/test_mysql.py +0 -0
  250. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/test_postgres.py +0 -0
  251. {execsql2-2.15.1 → execsql2-2.15.4}/tests/integration/test_sqlite.py +0 -0
  252. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/__init__.py +0 -0
  253. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_assert.py +0 -0
  254. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_breakpoint.py +0 -0
  255. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_connect.py +0 -0
  256. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_io_export.py +0 -0
  257. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_io_import.py +0 -0
  258. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands.py +0 -0
  259. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_connect.py +0 -0
  260. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_data.py +0 -0
  261. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_extended.py +0 -0
  262. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  263. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_io.py +0 -0
  264. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  265. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  266. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_system.py +0 -0
  267. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  268. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_pg_upsert.py +0 -0
  269. {execsql2-2.15.1 → execsql2-2.15.4}/tests/metacommands/test_row_count.py +0 -0
  270. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_config.py +0 -0
  271. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_config_data.py +0 -0
  272. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_config_extended.py +0 -0
  273. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_engine.py +0 -0
  274. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_error_messages.py +0 -0
  275. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_exceptions.py +0 -0
  276. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_mail.py +0 -0
  277. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_models.py +0 -0
  278. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_package.py +0 -0
  279. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_registry.py +0 -0
  280. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_script.py +0 -0
  281. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_state.py +0 -0
  282. {execsql2-2.15.1 → execsql2-2.15.4}/tests/test_types.py +0 -0
  283. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/__init__.py +0 -0
  284. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_auth.py +0 -0
  285. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_auth_extra.py +0 -0
  286. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_crypto.py +0 -0
  287. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_datetime.py +0 -0
  288. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_errors.py +0 -0
  289. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_errors_extra.py +0 -0
  290. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_fileio.py +0 -0
  291. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_fileio_extra.py +0 -0
  292. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_numeric.py +0 -0
  293. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_regex.py +0 -0
  294. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_strings.py +0 -0
  295. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_timer.py +0 -0
  296. {execsql2-2.15.1 → execsql2-2.15.4}/tests/utils/test_timer_extra.py +0 -0
  297. {execsql2-2.15.1 → execsql2-2.15.4}/zensical.toml +0 -0
@@ -13,6 +13,40 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.15.4] - 2026-04-15
17
+
18
+ ### Fixed
19
+
20
+ - Fixed typo in `test_latin1_encoding` test data (`calf\xe9` → `calf\xe9`) that caused assertion failure on Windows CI.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.15.3] - 2026-04-15
25
+
26
+ ### Added
27
+
28
+ - New optional dependency extras `auth-plaintext` and `auth-encrypted` for headless Linux keyring backends. `pip install execsql2[auth-plaintext]` installs `keyring` + `keyrings.alt`; `pip install execsql2[auth-encrypted]` adds `pycryptodome` for the encrypted file backend.
29
+
30
+ ______________________________________________________________________
31
+
32
+ ## [2.15.2] - 2026-04-14
33
+
34
+ ### Changed
35
+
36
+ - `DT_Integer`, `DT_Float`, and `DT_Decimal` data type matchers now use pre-compiled regex class attributes instead of recompiling on every call — reduces overhead during large imports.
37
+ - `DT_Boolean` match tuples are now cached and only rebuilt when the `boolean_words`/`boolean_int` config changes, instead of on every `_is_match()`/`_from_data()` call.
38
+ - SQLite and DuckDB adapter methods (`table_exists`, `table_columns`, `view_exists`, `schema_exists`) now use the `_cursor()` context manager to prevent cursor leaks on exceptions.
39
+
40
+ ### Fixed
41
+
42
+ - `DT_Text.data_type_name` corrected from `"character"` to `"text"` — error messages now correctly identify the text data type instead of showing "character".
43
+ - `DT_Varchar._from_data()` now converts non-string data to string and enforces the 255-character length limit. Previously, non-string values passed through without conversion or length check.
44
+ - `WriteHooks.write_err()` no longer crashes on empty string input.
45
+ - `CondAstNode.eval()` now raises `CondParserError` for unknown node types instead of silently returning `None`.
46
+ - `NumericAstNode.eval()` now raises `NumericParserError` on division by zero instead of an unhandled `ZeroDivisionError`.
47
+
48
+ ______________________________________________________________________
49
+
16
50
  ## [2.15.1] - 2026-04-14
17
51
 
18
52
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.1
3
+ Version: 2.15.4
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
@@ -69,6 +69,13 @@ Requires-Dist: pymysql; extra == 'all-db'
69
69
  Requires-Dist: pyodbc; extra == 'all-db'
70
70
  Provides-Extra: auth
71
71
  Requires-Dist: keyring; extra == 'auth'
72
+ Provides-Extra: auth-encrypted
73
+ Requires-Dist: keyring; extra == 'auth-encrypted'
74
+ Requires-Dist: keyrings-alt; extra == 'auth-encrypted'
75
+ Requires-Dist: pycryptodome; extra == 'auth-encrypted'
76
+ Provides-Extra: auth-plaintext
77
+ Requires-Dist: keyring; extra == 'auth-plaintext'
78
+ Requires-Dist: keyrings-alt; extra == 'auth-plaintext'
72
79
  Provides-Extra: dev
73
80
  Requires-Dist: build>=1.2.2.post1; extra == 'dev'
74
81
  Requires-Dist: bump-my-version>=1.2.7; extra == 'dev'
@@ -167,7 +174,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
167
174
 
168
175
  # Feature bundles
169
176
  pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
170
- pip install execsql2[auth] # OS keyring integration
177
+ pip install execsql2[auth] # OS keyring integration
178
+ pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
179
+ pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
171
180
 
172
181
  # Convenience
173
182
  pip install execsql2[all-db] # All database drivers
@@ -52,7 +52,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
52
52
 
53
53
  # Feature bundles
54
54
  pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
55
- pip install execsql2[auth] # OS keyring integration
55
+ pip install execsql2[auth] # OS keyring integration
56
+ pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
57
+ pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
56
58
 
57
59
  # Convenience
58
60
  pip install execsql2[all-db] # All database drivers
@@ -248,6 +248,11 @@ These are behavioral changes driven by security or correctness issues in the ups
248
248
  | 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. Inherited from upstream. |
249
249
  | 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. Inherited from upstream. |
250
250
  | `$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. |
251
+ | `DT_Text.data_type_name` wrong | Was `"character"` (same as `DT_Character`), making error messages indistinguishable. Corrected to `"text"`. Inherited from upstream. |
252
+ | `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. Inherited from upstream. |
253
+ | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. Inherited from upstream. |
254
+ | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
255
+ | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
251
256
 
252
257
  ______________________________________________________________________
253
258
 
@@ -52,7 +52,7 @@ To disable keyring integration, set `use_keyring = No` in the `[connect]` sectio
52
52
  Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
53
53
 
54
54
  ```bash
55
- pip install keyrings.alt pycryptodome
55
+ pip install execsql2[auth-encrypted]
56
56
  mkdir -p ~/.config/python_keyring
57
57
  cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
58
58
  [backend]
@@ -67,7 +67,7 @@ The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_p
67
67
  Passwords are stored in plain text on disk. No master password is needed — execsql will never prompt for a password after the first successful entry.
68
68
 
69
69
  ```bash
70
- pip install keyrings.alt
70
+ pip install execsql2[auth-plaintext]
71
71
  mkdir -p ~/.config/python_keyring
72
72
  cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
73
73
  [backend]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.15.1"
7
+ version = "2.15.4"
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" }
@@ -59,6 +59,8 @@ odbc = ["pyodbc"]
59
59
  # Feature bundles
60
60
  formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
61
61
  auth = ["keyring"]
62
+ auth-plaintext = ["keyring", "keyrings.alt"]
63
+ auth-encrypted = ["keyring", "keyrings.alt", "pycryptodome"]
62
64
  upsert = ["pg-upsert>=1.21.0"]
63
65
  # Convenience groups
64
66
  all-db = [
@@ -162,7 +164,7 @@ skip-magic-trailing-comma = false
162
164
  line-ending = "auto"
163
165
 
164
166
  [tool.bumpversion]
165
- current_version = "2.15.1"
167
+ current_version = "2.15.4"
166
168
  commit = true
167
169
  commit_args = "--no-verify"
168
170
  tag = true
@@ -231,6 +233,8 @@ hel = "hel"
231
233
  fo = "fo"
232
234
  # odfpy package imports as 'odf', not 'of'
233
235
  odf = "odf"
236
+ # Latin-1 test data: b"caf\xe9" decodes to "café"
237
+ caf = "caf"
234
238
 
235
239
  [tool.tox]
236
240
  required = ["tox-uv"]
@@ -569,7 +569,7 @@ class WriteHooks:
569
569
 
570
570
  def write_err(self, strval: str) -> None:
571
571
  """Write an error string to the error-output hook, or to sys.stderr if unset."""
572
- if strval[-1] != "\n":
572
+ if not strval.endswith("\n"):
573
573
  strval += "\n"
574
574
  if self.err_func:
575
575
  self.err_func(strval)
@@ -81,11 +81,10 @@ class DuckDBDatabase(Database):
81
81
  def schema_exists(self, schema_name: str) -> bool:
82
82
  """Return True if the named schema exists in the current DuckDB catalog."""
83
83
  # In DuckDB, the 'schemata' view is not limited to the current database.
84
- curs = self.cursor()
85
- curs.execute(
86
- "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ? and catalog_name = ?;",
87
- (schema_name, self.catalog_name),
88
- )
89
- rows = curs.fetchall()
90
- curs.close()
84
+ with self._cursor() as curs:
85
+ curs.execute(
86
+ "SELECT schema_name FROM information_schema.schemata WHERE schema_name = ? and catalog_name = ?;",
87
+ (schema_name, self.catalog_name),
88
+ )
89
+ rows = curs.fetchall()
91
90
  return len(rows) > 0
@@ -82,21 +82,21 @@ class SQLiteDatabase(Database):
82
82
 
83
83
  def table_exists(self, table_name: str, schema_name: str | None = None) -> bool:
84
84
  """Return True if the named table exists in the SQLite database."""
85
- curs = self.cursor()
86
- sql = "select name from sqlite_master where type='table' and name=?;"
87
- try:
88
- curs.execute(sql, (table_name,))
89
- except ErrInfo:
90
- raise
91
- except Exception as e:
92
- self.rollback()
93
- raise ErrInfo(
94
- type="db",
95
- command_text=sql,
96
- exception_msg=exception_desc(),
97
- other_msg=f'Failed test for existence of SQLite table "{table_name}";',
98
- ) from e
99
- rows = curs.fetchall()
85
+ with self._cursor() as curs:
86
+ sql = "select name from sqlite_master where type='table' and name=?;"
87
+ try:
88
+ curs.execute(sql, (table_name,))
89
+ except ErrInfo:
90
+ raise
91
+ except Exception as e:
92
+ self.rollback()
93
+ raise ErrInfo(
94
+ type="db",
95
+ command_text=sql,
96
+ exception_msg=exception_desc(),
97
+ other_msg=f'Failed test for existence of SQLite table "{table_name}";',
98
+ ) from e
99
+ rows = curs.fetchall()
100
100
  return len(rows) > 0
101
101
 
102
102
  def column_exists(
@@ -111,40 +111,40 @@ class SQLiteDatabase(Database):
111
111
 
112
112
  def table_columns(self, table_name: str, schema_name: str | None = None) -> list[str]:
113
113
  """Return a list of column names for the given SQLite table."""
114
- curs = self.cursor()
115
- quoted_tbl = self.quote_identifier(table_name)
116
- sql = f"select * from {quoted_tbl} where 1=0;"
117
- try:
118
- curs.execute(sql)
119
- except ErrInfo:
120
- raise
121
- except Exception as e:
122
- self.rollback()
123
- raise ErrInfo(
124
- type="db",
125
- command_text=sql,
126
- exception_msg=exception_desc(),
127
- other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
128
- ) from e
129
- return [d[0] for d in curs.description]
114
+ with self._cursor() as curs:
115
+ quoted_tbl = self.quote_identifier(table_name)
116
+ sql = f"select * from {quoted_tbl} where 1=0;"
117
+ try:
118
+ curs.execute(sql)
119
+ except ErrInfo:
120
+ raise
121
+ except Exception as e:
122
+ self.rollback()
123
+ raise ErrInfo(
124
+ type="db",
125
+ command_text=sql,
126
+ exception_msg=exception_desc(),
127
+ other_msg=f"Failed to get column names for table {table_name} of {self.name()}",
128
+ ) from e
129
+ return [d[0] for d in curs.description]
130
130
 
131
131
  def view_exists(self, view_name: str) -> bool:
132
132
  """Return True if the named view exists in the SQLite database."""
133
- curs = self.cursor()
134
- sql = "select name from sqlite_master where type='view' and name=?;"
135
- try:
136
- curs.execute(sql, (view_name,))
137
- except ErrInfo:
138
- raise
139
- except Exception as e:
140
- self.rollback()
141
- raise ErrInfo(
142
- type="db",
143
- command_text=sql,
144
- exception_msg=exception_desc(),
145
- other_msg=f'Failed test for existence of SQLite view "{view_name}";',
146
- ) from e
147
- rows = curs.fetchall()
133
+ with self._cursor() as curs:
134
+ sql = "select name from sqlite_master where type='view' and name=?;"
135
+ try:
136
+ curs.execute(sql, (view_name,))
137
+ except ErrInfo:
138
+ raise
139
+ except Exception as e:
140
+ self.rollback()
141
+ raise ErrInfo(
142
+ type="db",
143
+ command_text=sql,
144
+ exception_msg=exception_desc(),
145
+ other_msg=f'Failed test for existence of SQLite view "{view_name}";',
146
+ ) from e
147
+ rows = curs.fetchall()
148
148
  return len(rows) > 0
149
149
 
150
150
  def schema_exists(self, schema_name: str) -> bool:
@@ -151,8 +151,6 @@ class CondAstNode(CondTokens):
151
151
 
152
152
  def eval(self) -> bool:
153
153
  """Evaluate this subtree and return a boolean result."""
154
- # Evaluates the subtrees and/or conditional value for this node,
155
- # returning True or False.
156
154
  if self.type == self.CONDITIONAL:
157
155
  exec_fn = self.left[0].exec_fn
158
156
  cmdargs = self.left[1]
@@ -168,6 +166,7 @@ class CondAstNode(CondTokens):
168
166
  if lcond:
169
167
  return True
170
168
  return self.right.eval()
169
+ raise CondParserError(f"Unknown conditional node type: {self.type}")
171
170
 
172
171
 
173
172
  # AST for numeric expressions
@@ -200,6 +199,8 @@ class NumericAstNode(NumTokens):
200
199
  if self.type == self.MUL:
201
200
  return lnum * rnum
202
201
  elif self.type == self.DIV:
202
+ if rnum == 0:
203
+ raise NumericParserError("Division by zero.")
203
204
  return lnum / rnum
204
205
  elif self.type == self.ADD:
205
206
  return lnum + rnum
@@ -296,14 +296,24 @@ class DT_Boolean(DataType):
296
296
  data_type_name = "boolean"
297
297
  data_type = bool
298
298
 
299
+ def __init__(self) -> None:
300
+ """Initialise with empty match caches; populated on first use."""
301
+ self.true: tuple = ()
302
+ self.false: tuple = ()
303
+ self.bool_repr: tuple = ()
304
+ self._cache_key: tuple | None = None
305
+
299
306
  def __repr__(self) -> str:
300
307
  return "DT_Boolean()"
301
308
 
302
- def set_bool_matches(self) -> None:
303
- """Populate the true/false match tuples from the current configuration."""
309
+ def _ensure_bool_matches(self) -> None:
310
+ """Rebuild true/false match tuples only when the config has changed."""
304
311
  import execsql.state as _state
305
312
 
306
313
  conf = _state.conf
314
+ key = (conf.boolean_words, conf.boolean_int)
315
+ if key == self._cache_key:
316
+ return
307
317
  self.true = ("yes", "true")
308
318
  self.false = ("no", "false")
309
319
  if not conf.boolean_words:
@@ -313,6 +323,7 @@ class DT_Boolean(DataType):
313
323
  self.true += ("1",)
314
324
  self.false += ("0",)
315
325
  self.bool_repr = self.true + self.false
326
+ self._cache_key = key
316
327
 
317
328
  def _is_match(self, data: object) -> bool:
318
329
  import execsql.state as _state
@@ -320,7 +331,7 @@ class DT_Boolean(DataType):
320
331
  conf = _state.conf
321
332
  if data is None:
322
333
  return False
323
- self.set_bool_matches()
334
+ self._ensure_bool_matches()
324
335
  return bool(
325
336
  isinstance(data, bool)
326
337
  or conf.boolean_int
@@ -336,7 +347,7 @@ class DT_Boolean(DataType):
336
347
  conf = _state.conf
337
348
  if data is None:
338
349
  raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
339
- self.set_bool_matches()
350
+ self._ensure_bool_matches()
340
351
  if isinstance(data, bool):
341
352
  return data
342
353
  elif conf.boolean_int and type(data) is int and data in (0, 1):
@@ -352,6 +363,7 @@ class DT_Integer(DataType):
352
363
 
353
364
  data_type_name = "integer"
354
365
  data_type = int
366
+ _INT_RX = re.compile(r"^\s*[+-]?\d+\s*$")
355
367
 
356
368
  def __repr__(self) -> str:
357
369
  return "DT_Integer()"
@@ -367,7 +379,7 @@ class DT_Integer(DataType):
367
379
  elif isinstance(data, str):
368
380
  if leading_zero_num(data):
369
381
  return False
370
- if not re.match(r"^\s*[+-]?\d+\s*$", data):
382
+ if not self._INT_RX.match(data):
371
383
  return False
372
384
  try:
373
385
  i = int(data)
@@ -387,7 +399,7 @@ class DT_Integer(DataType):
387
399
  return int(data)
388
400
  else:
389
401
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
390
- if isinstance(data, str) and not re.match(r"^\s*[+-]?\d+\s*$", data):
402
+ if isinstance(data, str) and not self._INT_RX.match(data):
391
403
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
392
404
  try:
393
405
  i = int(data)
@@ -439,6 +451,7 @@ class DT_Float(DataType):
439
451
 
440
452
  data_type_name = "float"
441
453
  data_type = float
454
+ _FLOAT_RX = re.compile(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$")
442
455
 
443
456
  def __repr__(self) -> str:
444
457
  return "DT_Float()"
@@ -450,7 +463,7 @@ class DT_Float(DataType):
450
463
  return True
451
464
  if leading_zero_num(data):
452
465
  return False
453
- if isinstance(data, str) and not re.match(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$", data):
466
+ if isinstance(data, str) and not self._FLOAT_RX.match(data):
454
467
  return False
455
468
  try:
456
469
  float(data)
@@ -465,7 +478,7 @@ class DT_Float(DataType):
465
478
  return data
466
479
  if leading_zero_num(data):
467
480
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
468
- if isinstance(data, str) and not re.match(r"^[+-]?(\d+(\.\d*)?|\.\d+)([eE][+-]?\d+)?$", data):
481
+ if isinstance(data, str) and not self._FLOAT_RX.match(data):
469
482
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
470
483
  try:
471
484
  i = float(data)
@@ -480,6 +493,7 @@ class DT_Decimal(DataType):
480
493
  data_type_name = "decimal"
481
494
  data_type = Decimal
482
495
  precspec = True
496
+ _DECIMAL_RX = re.compile(r"^[+-]?(\d+(\.\d*)?|\.\d+)$")
483
497
 
484
498
  def __repr__(self) -> str:
485
499
  return "DT_Decimal()"
@@ -504,7 +518,7 @@ class DT_Decimal(DataType):
504
518
  self.set_scale_prec(data)
505
519
  return data
506
520
  elif isinstance(data, str):
507
- if not re.match(r"^[+-]?(\d+(\.\d*)?|\.\d+)$", data):
521
+ if not self._DECIMAL_RX.match(data):
508
522
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
509
523
  try:
510
524
  dec = Decimal(data)
@@ -565,21 +579,20 @@ class DT_Varchar(DataType):
565
579
  # This varchar data type is the same as the character data type.
566
580
  if data is None:
567
581
  raise DataTypeError(self.data_type_name, self._CONV_ERR % "NULL")
568
- data_type = str
569
- if isinstance(data, str):
582
+ if not isinstance(data, str):
570
583
  try:
571
- data = data_type(data)
584
+ data = str(data)
572
585
  except ValueError as e:
573
586
  raise DataTypeError(self.data_type_name, self._CONV_ERR % data) from e
574
- if len(data) > 255:
575
- raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
587
+ if len(data) > 255:
588
+ raise DataTypeError(self.data_type_name, self._CONV_ERR % data)
576
589
  return data
577
590
 
578
591
 
579
592
  class DT_Text(DataType):
580
593
  """Unbounded text string data type."""
581
594
 
582
- data_type_name = "character"
595
+ data_type_name = "text"
583
596
 
584
597
  def __repr__(self) -> str:
585
598
  return "DT_Text()"