execsql2 2.12.2__tar.gz → 2.12.3__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 (298) hide show
  1. {execsql2-2.12.2 → execsql2-2.12.3}/CHANGELOG.md +11 -0
  2. {execsql2-2.12.2 → execsql2-2.12.3}/PKG-INFO +1 -1
  3. {execsql2-2.12.2 → execsql2-2.12.3}/docs/about/divergence.md +6 -0
  4. {execsql2-2.12.2 → execsql2-2.12.3}/pyproject.toml +2 -2
  5. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/base.py +7 -0
  6. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/delimited.py +44 -2
  7. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/io_fileops.py +4 -0
  8. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/script/__init__.py +4 -0
  9. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/script/engine.py +55 -23
  10. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/script/variables.py +35 -2
  11. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_delimited.py +12 -3
  12. {execsql2-2.12.2 → execsql2-2.12.3}/uv.lock +1 -1
  13. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/dba.md +0 -0
  14. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/herald.md +0 -0
  15. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/inspector.md +0 -0
  16. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/liaison.md +0 -0
  17. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/oracle.md +0 -0
  18. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/patcher.md +0 -0
  19. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/qa.md +0 -0
  20. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/agents/scribe.md +0 -0
  21. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/code-oracle.md +0 -0
  22. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/migrate.md +0 -0
  23. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/review-changes.md +0 -0
  24. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/test-module.md +0 -0
  25. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/update-changelog.md +0 -0
  26. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/commands/where-is.md +0 -0
  27. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/project_context.md +0 -0
  28. {execsql2-2.12.2 → execsql2-2.12.3}/.claude/state/status.md +0 -0
  29. {execsql2-2.12.2 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  30. {execsql2-2.12.2 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  31. {execsql2-2.12.2 → execsql2-2.12.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  32. {execsql2-2.12.2 → execsql2-2.12.3}/.github/workflows/ci-cd.yml +0 -0
  33. {execsql2-2.12.2 → execsql2-2.12.3}/.gitignore +0 -0
  34. {execsql2-2.12.2 → execsql2-2.12.3}/.pre-commit-config.yaml +0 -0
  35. {execsql2-2.12.2 → execsql2-2.12.3}/.pre-commit-hooks.yaml +0 -0
  36. {execsql2-2.12.2 → execsql2-2.12.3}/.python-version +0 -0
  37. {execsql2-2.12.2 → execsql2-2.12.3}/.readthedocs.yaml +0 -0
  38. {execsql2-2.12.2 → execsql2-2.12.3}/CLAUDE.md +0 -0
  39. {execsql2-2.12.2 → execsql2-2.12.3}/CONTRIBUTING.md +0 -0
  40. {execsql2-2.12.2 → execsql2-2.12.3}/LICENSE.txt +0 -0
  41. {execsql2-2.12.2 → execsql2-2.12.3}/NOTICE +0 -0
  42. {execsql2-2.12.2 → execsql2-2.12.3}/README.md +0 -0
  43. {execsql2-2.12.2 → execsql2-2.12.3}/SECURITY.md +0 -0
  44. {execsql2-2.12.2 → execsql2-2.12.3}/docs/about/contributors.md +0 -0
  45. {execsql2-2.12.2 → execsql2-2.12.3}/docs/about/copyright.md +0 -0
  46. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/cli.md +0 -0
  47. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/db.md +0 -0
  48. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/exporters.md +0 -0
  49. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/importers.md +0 -0
  50. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/index.md +0 -0
  51. {execsql2-2.12.2 → execsql2-2.12.3}/docs/api/metacommands.md +0 -0
  52. {execsql2-2.12.2 → execsql2-2.12.3}/docs/dev/adding_db_adapters.md +0 -0
  53. {execsql2-2.12.2 → execsql2-2.12.3}/docs/dev/adding_exporters.md +0 -0
  54. {execsql2-2.12.2 → execsql2-2.12.3}/docs/dev/adding_importers.md +0 -0
  55. {execsql2-2.12.2 → execsql2-2.12.3}/docs/dev/adding_metacommands.md +0 -0
  56. {execsql2-2.12.2 → execsql2-2.12.3}/docs/dev/architecture.md +0 -0
  57. {execsql2-2.12.2 → execsql2-2.12.3}/docs/getting-started/installation.md +0 -0
  58. {execsql2-2.12.2 → execsql2-2.12.3}/docs/getting-started/requirements.md +0 -0
  59. {execsql2-2.12.2 → execsql2-2.12.3}/docs/getting-started/syntax.md +0 -0
  60. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/debugging.md +0 -0
  61. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/documentation.md +0 -0
  62. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/encoding.md +0 -0
  63. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/examples.md +0 -0
  64. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/formatter.md +0 -0
  65. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/logging.md +0 -0
  66. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/sql_syntax.md +0 -0
  67. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/usage.md +0 -0
  68. {execsql2-2.12.2 → execsql2-2.12.3}/docs/guides/using_scripts.md +0 -0
  69. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/Compare_planets.png +0 -0
  70. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/actions.png +0 -0
  71. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/actions2.png +0 -0
  72. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/checkboxes.png +0 -0
  73. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/connect.b64 +0 -0
  74. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/connect.png +0 -0
  75. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/create_conf.png +0 -0
  76. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/data_error1_screenshot.jpg +0 -0
  77. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/entry_form.png +0 -0
  78. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/execsql_console.png +0 -0
  79. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/execsql_logo_01.png +0 -0
  80. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/fatals.png +0 -0
  81. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/logo_small.png +0 -0
  82. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/pause_terminal.png +0 -0
  83. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/pause_terminal_sm.b64 +0 -0
  84. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/pause_terminal_sm.png +0 -0
  85. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/prompt_compare.png +0 -0
  86. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/set_build_commands.jpg +0 -0
  87. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/unit_conversions.b64 +0 -0
  88. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/unit_conversions_029.png +0 -0
  89. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/unmatched.png +0 -0
  90. {execsql2-2.12.2 → execsql2-2.12.3}/docs/images/vim_execsql_highlight.png +0 -0
  91. {execsql2-2.12.2 → execsql2-2.12.3}/docs/index.md +0 -0
  92. {execsql2-2.12.2 → execsql2-2.12.3}/docs/reference/configuration.md +0 -0
  93. {execsql2-2.12.2 → execsql2-2.12.3}/docs/reference/metacommands.md +0 -0
  94. {execsql2-2.12.2 → execsql2-2.12.3}/docs/reference/security.md +0 -0
  95. {execsql2-2.12.2 → execsql2-2.12.3}/docs/reference/substitution_vars.md +0 -0
  96. {execsql2-2.12.2 → execsql2-2.12.3}/extras/vscode-execsql/README.md +0 -0
  97. {execsql2-2.12.2 → execsql2-2.12.3}/extras/vscode-execsql/package.json +0 -0
  98. {execsql2-2.12.2 → execsql2-2.12.3}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  99. {execsql2-2.12.2 → execsql2-2.12.3}/justfile +0 -0
  100. {execsql2-2.12.2 → execsql2-2.12.3}/scripts/generate_vscode_grammar.py +0 -0
  101. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/__init__.py +0 -0
  102. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/__main__.py +0 -0
  103. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/cli/__init__.py +0 -0
  104. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/cli/dsn.py +0 -0
  105. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/cli/help.py +0 -0
  106. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/cli/lint.py +0 -0
  107. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/cli/run.py +0 -0
  108. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/config.py +0 -0
  109. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/constants.py +0 -0
  110. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/__init__.py +0 -0
  111. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/access.py +0 -0
  112. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/dsn.py +0 -0
  113. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/duckdb.py +0 -0
  114. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/factory.py +0 -0
  115. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/firebird.py +0 -0
  116. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/mysql.py +0 -0
  117. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/oracle.py +0 -0
  118. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/postgres.py +0 -0
  119. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/sqlite.py +0 -0
  120. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/db/sqlserver.py +0 -0
  121. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/debug/__init__.py +0 -0
  122. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/debug/repl.py +0 -0
  123. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exceptions.py +0 -0
  124. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/__init__.py +0 -0
  125. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/base.py +0 -0
  126. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/duckdb.py +0 -0
  127. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/feather.py +0 -0
  128. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/html.py +0 -0
  129. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/json.py +0 -0
  130. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/latex.py +0 -0
  131. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/markdown.py +0 -0
  132. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/ods.py +0 -0
  133. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/parquet.py +0 -0
  134. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/pretty.py +0 -0
  135. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/protocol.py +0 -0
  136. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/raw.py +0 -0
  137. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/sqlite.py +0 -0
  138. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/templates.py +0 -0
  139. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/values.py +0 -0
  140. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/xls.py +0 -0
  141. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/xlsx.py +0 -0
  142. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/xml.py +0 -0
  143. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/yaml.py +0 -0
  144. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/exporters/zip.py +0 -0
  145. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/format.py +0 -0
  146. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/gui/__init__.py +0 -0
  147. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/gui/base.py +0 -0
  148. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/gui/console.py +0 -0
  149. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/gui/desktop.py +0 -0
  150. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/gui/tui.py +0 -0
  151. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/__init__.py +0 -0
  152. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/base.py +0 -0
  153. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/csv.py +0 -0
  154. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/feather.py +0 -0
  155. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/ods.py +0 -0
  156. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/importers/xls.py +0 -0
  157. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/__init__.py +0 -0
  158. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/conditions.py +0 -0
  159. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/connect.py +0 -0
  160. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/control.py +0 -0
  161. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/data.py +0 -0
  162. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/debug.py +0 -0
  163. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/dispatch.py +0 -0
  164. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/io.py +0 -0
  165. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/io_export.py +0 -0
  166. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/io_import.py +0 -0
  167. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/io_write.py +0 -0
  168. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/prompt.py +0 -0
  169. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/script_ext.py +0 -0
  170. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/metacommands/system.py +0 -0
  171. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/models.py +0 -0
  172. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/parser.py +0 -0
  173. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/py.typed +0 -0
  174. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/script/control.py +0 -0
  175. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/state.py +0 -0
  176. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/types.py +0 -0
  177. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/__init__.py +0 -0
  178. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/auth.py +0 -0
  179. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/crypto.py +0 -0
  180. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/datetime.py +0 -0
  181. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/errors.py +0 -0
  182. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/fileio.py +0 -0
  183. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/gui.py +0 -0
  184. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/mail.py +0 -0
  185. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/numeric.py +0 -0
  186. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/regex.py +0 -0
  187. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/strings.py +0 -0
  188. {execsql2-2.12.2 → execsql2-2.12.3}/src/execsql/utils/timer.py +0 -0
  189. {execsql2-2.12.2 → execsql2-2.12.3}/templates/README.md +0 -0
  190. {execsql2-2.12.2 → execsql2-2.12.3}/templates/config_settings.sqlite +0 -0
  191. {execsql2-2.12.2 → execsql2-2.12.3}/templates/example_config_prompt.sql +0 -0
  192. {execsql2-2.12.2 → execsql2-2.12.3}/templates/execsql.conf +0 -0
  193. {execsql2-2.12.2 → execsql2-2.12.3}/templates/make_config_db.sql +0 -0
  194. {execsql2-2.12.2 → execsql2-2.12.3}/templates/md_compare.sql +0 -0
  195. {execsql2-2.12.2 → execsql2-2.12.3}/templates/md_glossary.sql +0 -0
  196. {execsql2-2.12.2 → execsql2-2.12.3}/templates/md_upsert.sql +0 -0
  197. {execsql2-2.12.2 → execsql2-2.12.3}/templates/pg_compare.sql +0 -0
  198. {execsql2-2.12.2 → execsql2-2.12.3}/templates/pg_glossary.sql +0 -0
  199. {execsql2-2.12.2 → execsql2-2.12.3}/templates/pg_upsert.sql +0 -0
  200. {execsql2-2.12.2 → execsql2-2.12.3}/templates/script_template.sql +0 -0
  201. {execsql2-2.12.2 → execsql2-2.12.3}/templates/ss_compare.sql +0 -0
  202. {execsql2-2.12.2 → execsql2-2.12.3}/templates/ss_glossary.sql +0 -0
  203. {execsql2-2.12.2 → execsql2-2.12.3}/templates/ss_upsert.sql +0 -0
  204. {execsql2-2.12.2 → execsql2-2.12.3}/tests/__init__.py +0 -0
  205. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/__init__.py +0 -0
  206. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_cli.py +0 -0
  207. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_cli_e2e.py +0 -0
  208. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_cli_run.py +0 -0
  209. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_lint.py +0 -0
  210. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_ping.py +0 -0
  211. {execsql2-2.12.2 → execsql2-2.12.3}/tests/cli/test_profile.py +0 -0
  212. {execsql2-2.12.2 → execsql2-2.12.3}/tests/conftest.py +0 -0
  213. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/__init__.py +0 -0
  214. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_base.py +0 -0
  215. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_duckdb.py +0 -0
  216. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_factory.py +0 -0
  217. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_postgres.py +0 -0
  218. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_sqlite.py +0 -0
  219. {execsql2-2.12.2 → execsql2-2.12.3}/tests/db/test_sqlite_extra.py +0 -0
  220. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/__init__.py +0 -0
  221. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_base.py +0 -0
  222. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_db.py +0 -0
  223. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_duckdb_exporter.py +0 -0
  224. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_exporters.py +0 -0
  225. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_feather.py +0 -0
  226. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_html_latex.py +0 -0
  227. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_json.py +0 -0
  228. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_markdown.py +0 -0
  229. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_ods.py +0 -0
  230. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_parquet.py +0 -0
  231. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_sqlite_exporter.py +0 -0
  232. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_templates.py +0 -0
  233. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_xls_xlsx.py +0 -0
  234. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_xlsx.py +0 -0
  235. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_xml.py +0 -0
  236. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_yaml.py +0 -0
  237. {execsql2-2.12.2 → execsql2-2.12.3}/tests/exporters/test_zip.py +0 -0
  238. {execsql2-2.12.2 → execsql2-2.12.3}/tests/gui/__init__.py +0 -0
  239. {execsql2-2.12.2 → execsql2-2.12.3}/tests/gui/test_backends.py +0 -0
  240. {execsql2-2.12.2 → execsql2-2.12.3}/tests/importers/__init__.py +0 -0
  241. {execsql2-2.12.2 → execsql2-2.12.3}/tests/importers/test_csv_importer.py +0 -0
  242. {execsql2-2.12.2 → execsql2-2.12.3}/tests/importers/test_feather_importer.py +0 -0
  243. {execsql2-2.12.2 → execsql2-2.12.3}/tests/importers/test_ods_importer.py +0 -0
  244. {execsql2-2.12.2 → execsql2-2.12.3}/tests/importers/test_xls_importer.py +0 -0
  245. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/__init__.py +0 -0
  246. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/conftest.py +0 -0
  247. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/test_dsn.py +0 -0
  248. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/test_duckdb.py +0 -0
  249. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/test_mysql.py +0 -0
  250. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/test_postgres.py +0 -0
  251. {execsql2-2.12.2 → execsql2-2.12.3}/tests/integration/test_sqlite.py +0 -0
  252. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/__init__.py +0 -0
  253. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_assert.py +0 -0
  254. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_breakpoint.py +0 -0
  255. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_connect.py +0 -0
  256. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_io_export.py +0 -0
  257. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_io_import.py +0 -0
  258. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands.py +0 -0
  259. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_connect.py +0 -0
  260. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_data.py +0 -0
  261. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_extended.py +0 -0
  262. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  263. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io.py +0 -0
  264. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  265. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  266. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system.py +0 -0
  267. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  268. {execsql2-2.12.2 → execsql2-2.12.3}/tests/metacommands/test_row_count.py +0 -0
  269. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_config.py +0 -0
  270. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_config_data.py +0 -0
  271. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_constants.py +0 -0
  272. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_engine.py +0 -0
  273. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_error_messages.py +0 -0
  274. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_exceptions.py +0 -0
  275. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_format.py +0 -0
  276. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_mail.py +0 -0
  277. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_models.py +0 -0
  278. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_package.py +0 -0
  279. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_parser.py +0 -0
  280. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_registry.py +0 -0
  281. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_script.py +0 -0
  282. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_state.py +0 -0
  283. {execsql2-2.12.2 → execsql2-2.12.3}/tests/test_types.py +0 -0
  284. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/__init__.py +0 -0
  285. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_auth.py +0 -0
  286. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_auth_extra.py +0 -0
  287. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_crypto.py +0 -0
  288. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_datetime.py +0 -0
  289. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_errors.py +0 -0
  290. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_errors_extra.py +0 -0
  291. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_fileio.py +0 -0
  292. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_fileio_extra.py +0 -0
  293. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_numeric.py +0 -0
  294. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_regex.py +0 -0
  295. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_strings.py +0 -0
  296. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_timer.py +0 -0
  297. {execsql2-2.12.2 → execsql2-2.12.3}/tests/utils/test_timer_extra.py +0 -0
  298. {execsql2-2.12.2 → execsql2-2.12.3}/zensical.toml +0 -0
@@ -13,6 +13,17 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.12.3] - 2026-04-02
17
+
18
+ ### Changed
19
+
20
+ - Performance: split `set_system_vars()` into static (once per script + on CONNECT/CHDIR) and dynamic (per statement) — eliminates ~14 redundant `add_substitution` calls and 2 `Path.resolve()` filesystem syscalls per statement.
21
+ - Performance: `$RANDOM` and `$UUID` are now lazy — computed only when actually referenced in a statement, not generated unconditionally for every statement.
22
+ - Performance: `LineDelimiter.delimited()` caches `quote_all_text` at construction time instead of reading `_state.conf` via module proxy on every row during export.
23
+ - Performance: CSV/TSV import uses Python's `csv` module as a fast path for standard delimited formats (comma, tab, semicolon, pipe) with doubled-quote escaping. Falls back to the character-at-a-time parser for non-standard formats (space-delimiter collapsing, escape characters).
24
+
25
+ ______________________________________________________________________
26
+
16
27
  ## [2.12.2] - 2026-04-02
17
28
 
18
29
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.12.2
3
+ Version: 2.12.3
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
@@ -163,6 +163,12 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
163
163
 
164
164
  - **Cycle detection** — `substitute_vars()` raises an error after 100 iterations to prevent infinite loops when variables reference each other cyclically. Upstream had no protection.
165
165
  - **O(1) substitution** — Variable substitution uses a single combined regex and dict lookup instead of O(V) per-variable regex passes. Behavior is identical; performance is improved.
166
+ - **Lazy `$RANDOM`/`$UUID`** — These system variables are now computed on first access rather than generated unconditionally for every statement. Behavior is identical when referenced; scripts that never reference them skip the computation entirely.
167
+ - **Static/dynamic system var split** — System substitution variables are split into static (set once per script and refreshed on CONNECT/CHDIR) and dynamic (refreshed per statement). Eliminates redundant `Path.resolve()` syscalls and database pool lookups per statement.
168
+
169
+ ### CSV Import
170
+
171
+ - **Fast-path CSV reader** — Standard delimited imports (comma, tab, semicolon, pipe with doubled-quote escaping) now use Python's `csv` module. Non-standard formats (space-delimiter collapsing, escape characters) fall back to the original character-at-a-time parser.
166
172
 
167
173
  ### Database Adapters
168
174
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.12.2"
7
+ version = "2.12.3"
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" }
@@ -159,7 +159,7 @@ skip-magic-trailing-comma = false
159
159
  line-ending = "auto"
160
160
 
161
161
  [tool.bumpversion]
162
- current_version = "2.12.2"
162
+ current_version = "2.12.3"
163
163
  commit = true
164
164
  commit_args = "--no-verify"
165
165
  tag = true
@@ -686,6 +686,13 @@ class DatabasePool:
686
686
  )
687
687
  self.pool[db_alias].close()
688
688
  self.pool[db_alias] = db_obj
689
+ # Refresh static system vars so $DB_NAME, $DB_USER, etc. reflect the new connection.
690
+ try:
691
+ from execsql.script.engine import set_static_system_vars
692
+
693
+ set_static_system_vars()
694
+ except Exception:
695
+ pass # Engine not yet initialized (early startup).
689
696
 
690
697
  def aliases(self) -> list[str]:
691
698
  """Return a list of all currently registered database aliases."""
@@ -40,6 +40,7 @@ class LineDelimiter:
40
40
  self.delimiter = delim
41
41
  self.joinchar = delim if delim else ""
42
42
  self.quotechar = quote
43
+ self.quote_all_text = _state.conf.quote_all_text if _state.conf else False
43
44
  if quote:
44
45
  if escchar:
45
46
  self.quotedquote = escchar + quote
@@ -50,13 +51,12 @@ class LineDelimiter:
50
51
 
51
52
  def delimited(self, datarow: Any, add_newline: bool = True) -> str:
52
53
  """Format a sequence of values as a single delimited text line."""
53
- conf = _state.conf
54
54
  if self.quotechar:
55
55
  d_row = []
56
56
  for e in datarow:
57
57
  if isinstance(e, str):
58
58
  if (
59
- conf.quote_all_text
59
+ self.quote_all_text
60
60
  or (self.quotechar in e)
61
61
  or (self.delimiter is not None and self.delimiter in e)
62
62
  or ("\n" in e)
@@ -609,10 +609,52 @@ class CsvFile(EncodedFile):
609
609
  raise ErrInfo("error", other_msg=", ".join(self.parse_errors))
610
610
  return elements
611
611
 
612
+ def _can_use_fast_csv_reader(self) -> bool:
613
+ """Return True if the detected format is compatible with Python's csv module."""
614
+ # The csv module handles comma/tab delimiters with doubled-quote escaping.
615
+ # It cannot handle: space-delimiter collapsing, escape chars, or no delimiter.
616
+ if self.delimiter is None or self.delimiter == " ":
617
+ return False
618
+ return self.escapechar is None
619
+
612
620
  def reader(self) -> Any:
613
621
  """Yield parsed rows from the file as lists of field values."""
614
622
  conf = _state.conf
615
623
  self.evaluate_line_format()
624
+ if self._can_use_fast_csv_reader():
625
+ yield from self._fast_reader(conf)
626
+ else:
627
+ yield from self._slow_reader(conf)
628
+
629
+ def _fast_reader(self, conf: Any) -> Any:
630
+ """Read using Python's csv module (fast path for standard delimited formats)."""
631
+ import csv
632
+
633
+ f = self.openclean("rt")
634
+ try:
635
+ csv_reader = csv.reader(
636
+ f,
637
+ delimiter=self.delimiter,
638
+ quotechar=self.quotechar,
639
+ doublequote=True,
640
+ strict=False,
641
+ )
642
+ for elements in csv_reader:
643
+ if len(elements) == 0:
644
+ break
645
+ # Normalize empty strings to None for parity with the slow reader.
646
+ elements = [e if e != "" else None for e in elements]
647
+ if conf.del_empty_cols and len(self.blank_cols) > 0:
648
+ blanks = copy.copy(self.blank_cols)
649
+ while len(blanks) > 0:
650
+ b = blanks.pop()
651
+ del elements[b]
652
+ yield elements
653
+ finally:
654
+ f.close()
655
+
656
+ def _slow_reader(self, conf: Any) -> Any:
657
+ """Read using the character-at-a-time state machine (fallback for non-standard formats)."""
616
658
  f = self.openclean("rt")
617
659
  line_no = 0
618
660
  try:
@@ -241,6 +241,10 @@ def x_cd(**kwargs: Any) -> None:
241
241
  os.chdir(new_dir)
242
242
  script, lno = current_script_line()
243
243
  _state.exec_log.log_status_info(f"Current directory changed to {new_dir} at line {lno} of {script}")
244
+ if _state.subvars is not None:
245
+ from execsql.script.engine import set_static_system_vars
246
+
247
+ set_static_system_vars()
244
248
  return None
245
249
 
246
250
 
@@ -63,6 +63,8 @@ from execsql.script.engine import (
63
63
  read_sqlfile,
64
64
  read_sqlstring,
65
65
  runscripts,
66
+ set_dynamic_system_vars,
67
+ set_static_system_vars,
66
68
  set_system_vars,
67
69
  substitute_vars,
68
70
  )
@@ -86,6 +88,8 @@ __all__ = [
86
88
  "CommandListUntilLoop",
87
89
  "ScriptFile",
88
90
  "ScriptExecSpec",
91
+ "set_dynamic_system_vars",
92
+ "set_static_system_vars",
89
93
  "set_system_vars",
90
94
  "substitute_vars",
91
95
  "runscripts",
@@ -704,9 +704,45 @@ class ScriptExecSpec:
704
704
  # ---------------------------------------------------------------------------
705
705
 
706
706
 
707
- def set_system_vars() -> None:
708
- """Refresh all built-in system substitution variables (``$CURRENT_TIME``, ``$DB_NAME``, etc.)."""
709
- # (Re)define the system substitution variables that are not script-specific.
707
+ def set_static_system_vars() -> None:
708
+ """Set system substitution variables that only change on CONNECT or CHDIR.
709
+
710
+ Called once before the execution loop. These values are expensive to compute
711
+ (filesystem syscalls, database pool lookups) but rarely change — only on
712
+ ``CONNECT``, ``USE``, or ``CHDIR`` metacommands. The ``runscripts()`` loop
713
+ calls this once up front; metacommand handlers that change the connection or
714
+ working directory should call it again afterward.
715
+ """
716
+ import random
717
+
718
+ cwd = str(Path(".").resolve())
719
+ _state.subvars.add_substitution("$CURRENT_DIR", cwd)
720
+ _state.subvars.add_substitution("$CURRENT_PATH", cwd + os.sep)
721
+ _state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
722
+ db = _state.dbs.current()
723
+ _state.subvars.add_substitution("$DB_USER", db.user if db.user else "")
724
+ _state.subvars.add_substitution(
725
+ "$DB_SERVER",
726
+ db.server_name if db.server_name else "",
727
+ )
728
+ _state.subvars.add_substitution("$DB_NAME", db.db_name)
729
+ _state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
730
+ _state.subvars.add_substitution("$VERSION1", str(_state.primary_vno))
731
+ _state.subvars.add_substitution("$VERSION2", str(_state.secondary_vno))
732
+ _state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
733
+ # Register lazy providers for $RANDOM and $UUID — computed only when referenced.
734
+ _state.subvars.register_lazy("$random", lambda: str(random.random()))
735
+ _state.subvars.register_lazy("$uuid", lambda: str(uuid.uuid4()))
736
+
737
+
738
+ def set_dynamic_system_vars() -> None:
739
+ """Refresh system substitution variables that change every statement.
740
+
741
+ Called once per statement in the execution loop. Includes cheap boolean-to-string
742
+ conversions for halt states and autocommit (which can change on any CONFIG or
743
+ AUTOCOMMIT metacommand) plus ``$TIMER`` and lazy-variable cache reset.
744
+ """
745
+ # Halt/config state vars — cheap to set, can change on any CONFIG metacommand.
710
746
  _state.subvars.add_substitution("$CANCEL_HALT_STATE", "ON" if _state.status.cancel_halt else "OFF")
711
747
  _state.subvars.add_substitution("$ERROR_HALT_STATE", "ON" if _state.status.halt_on_err else "OFF")
712
748
  _state.subvars.add_substitution(
@@ -718,27 +754,22 @@ def set_system_vars() -> None:
718
754
  "ON" if _state.conf.gui_wait_on_error_halt else "OFF",
719
755
  )
720
756
  _state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
721
- # $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
722
- _state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
723
- _state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
724
- _state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
725
757
  db = _state.dbs.current()
726
758
  _state.subvars.add_substitution("$AUTOCOMMIT_STATE", "ON" if db.autocommit else "OFF")
759
+ # $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
727
760
  _state.subvars.add_substitution("$TIMER", str(datetime.timedelta(seconds=_state.timer.elapsed())))
728
- _state.subvars.add_substitution("$DB_USER", db.user if db.user else "")
729
- _state.subvars.add_substitution(
730
- "$DB_SERVER",
731
- db.server_name if db.server_name else "",
732
- )
733
- _state.subvars.add_substitution("$DB_NAME", db.db_name)
734
- _state.subvars.add_substitution("$DB_NEED_PWD", "TRUE" if db.need_passwd else "FALSE")
735
- import random
761
+ _state.subvars.clear_lazy_cache()
736
762
 
737
- _state.subvars.add_substitution("$RANDOM", str(random.random()))
738
- _state.subvars.add_substitution("$UUID", str(uuid.uuid4()))
739
- _state.subvars.add_substitution("$VERSION1", str(_state.primary_vno))
740
- _state.subvars.add_substitution("$VERSION2", str(_state.secondary_vno))
741
- _state.subvars.add_substitution("$VERSION3", str(_state.tertiary_vno))
763
+
764
+ def set_system_vars() -> None:
765
+ """Refresh all built-in system substitution variables.
766
+
767
+ Convenience wrapper that calls both :func:`set_static_system_vars` and
768
+ :func:`set_dynamic_system_vars`. Retained for backward compatibility with
769
+ tests and any external callers.
770
+ """
771
+ set_static_system_vars()
772
+ set_dynamic_system_vars()
742
773
 
743
774
 
744
775
  _MAX_SUBSTITUTION_DEPTH = 100
@@ -779,11 +810,12 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
779
810
 
780
811
  def runscripts() -> None:
781
812
  """Drive execution until the command-list stack is empty."""
782
- # Repeatedly run the next statement from the script at the top of the
783
- # command list stack until there are no more statements.
813
+ # Set static vars once before the loop; they are refreshed by metacommand
814
+ # handlers (CONNECT, CONFIG, AUTOCOMMIT, CHDIR) when state changes.
815
+ set_static_system_vars()
784
816
  while len(_state.commandliststack) > 0:
785
817
  current_cmds = _state.commandliststack[-1]
786
- set_system_vars()
818
+ set_dynamic_system_vars()
787
819
  try:
788
820
  current_cmds.run_next()
789
821
  except StopIteration:
@@ -89,6 +89,7 @@ class SubVarSet:
89
89
  # compatibility with external code.
90
90
  def __init__(self) -> None:
91
91
  self._subs_dict: dict[str, Any] = {}
92
+ self._lazy_providers: dict[str, Any] = {}
92
93
  self.prefix_list: list[str] = ["$", "&", "@"]
93
94
  # Don't construct/compile on init because deepcopy() can't handle compiled regexes.
94
95
  self.var_rx = None
@@ -120,6 +121,30 @@ class SubVarSet:
120
121
  if not self.var_name_ok(varname.lower()):
121
122
  raise ErrInfo("error", other_msg=f"Invalid variable name ({varname}) in this context.")
122
123
 
124
+ def register_lazy(self, varname: str, provider: Any) -> None:
125
+ """Register a lazy variable whose value is computed on first access per cycle.
126
+
127
+ The *provider* callable is invoked only when the variable is actually
128
+ referenced (via :meth:`substitute`, :meth:`varvalue`, etc.). The result
129
+ is cached in ``_subs_dict`` until :meth:`clear_lazy_cache` is called.
130
+ """
131
+ self.check_var_name(varname)
132
+ self._lazy_providers[varname.lower()] = provider
133
+
134
+ def clear_lazy_cache(self) -> None:
135
+ """Remove materialized lazy values so they regenerate on next access."""
136
+ for key in self._lazy_providers:
137
+ self._subs_dict.pop(key, None)
138
+
139
+ def _materialize_lazy(self, varname: str) -> str | None:
140
+ """If *varname* has a lazy provider, invoke it, cache the result, and return it."""
141
+ provider = self._lazy_providers.get(varname)
142
+ if provider is not None:
143
+ value = str(provider())
144
+ self._subs_dict[varname] = value
145
+ return value
146
+ return None
147
+
123
148
  def remove_substitution(self, template_str: str) -> None:
124
149
  """Remove the variable named *template_str* from the substitution pool."""
125
150
  self.check_var_name(template_str)
@@ -143,7 +168,11 @@ class SubVarSet:
143
168
  def varvalue(self, varname: str) -> str | None:
144
169
  """Return the value of *varname*, or ``None`` if it is not defined."""
145
170
  self.check_var_name(varname)
146
- return self._subs_dict.get(varname.lower())
171
+ key = varname.lower()
172
+ val = self._subs_dict.get(key)
173
+ if val is None and key in self._lazy_providers:
174
+ return self._materialize_lazy(key)
175
+ return val
147
176
 
148
177
  def increment_by(self, varname: str, numeric_increment: Any) -> None:
149
178
  self.check_var_name(varname)
@@ -165,13 +194,15 @@ class SubVarSet:
165
194
  def sub_exists(self, template_str: str) -> bool:
166
195
  """Return True if the variable named *template_str* is defined."""
167
196
  self.check_var_name(template_str)
168
- return template_str.lower() in self._subs_dict
197
+ key = template_str.lower()
198
+ return key in self._subs_dict or key in self._lazy_providers
169
199
 
170
200
  def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
171
201
  """Return a new SubVarSet with this object's variables merged with other_subvars."""
172
202
  if other_subvars is not None:
173
203
  newsubs = SubVarSet()
174
204
  newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
205
+ newsubs._lazy_providers = {**self._lazy_providers, **other_subvars._lazy_providers}
175
206
  newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
176
207
  newsubs.compile_var_rx()
177
208
  return newsubs
@@ -201,6 +232,8 @@ class SubVarSet:
201
232
  m = self._TOKEN_RX.search(command_str)
202
233
  while m:
203
234
  varname = m.group("varname").lower()
235
+ if varname not in self._subs_dict and varname in self._lazy_providers:
236
+ self._materialize_lazy(varname)
204
237
  if varname in self._subs_dict:
205
238
  sub = self._subs_dict[varname]
206
239
  if sub is None:
@@ -616,12 +616,13 @@ class TestCsvFile:
616
616
  assert any("Alice" in str(r) for r in data_rows)
617
617
 
618
618
  def test_reader_reraises_errinfo_with_line_number(self, tmp_path):
619
- # Line 609: reader() wraps ErrInfo with line number context
619
+ # Tests the slow reader path error handling (line number annotation).
620
+ # Use an escape char to force the slow reader path.
620
621
  p = tmp_path / "bad.csv"
621
622
  # Write a CSV that will parse, but mock read_and_parse_line to raise ErrInfo
622
623
  p.write_text("id,name\n1,Alice\n", encoding="utf-8")
623
624
  cf = CsvFile(str(p), "utf-8")
624
- cf.lineformat(",", '"', None)
625
+ cf.lineformat(",", '"', "\\")
625
626
  with (
626
627
  patch.object(cf, "read_and_parse_line", side_effect=ErrInfo("error", other_msg="parse fail")),
627
628
  pytest.raises(ErrInfo) as exc_info,
@@ -899,7 +900,15 @@ class TestCsvLine:
899
900
 
900
901
 
901
902
  class TestReadAndParseLine:
902
- """Tests for read_and_parse_line() via full CsvFile.reader() pipeline."""
903
+ """Tests for read_and_parse_line() via full CsvFile.reader() pipeline.
904
+
905
+ Forces the slow (character-at-a-time) reader by patching _can_use_fast_csv_reader
906
+ so that these tests exercise the state machine, not Python's csv module.
907
+ """
908
+
909
+ @pytest.fixture(autouse=True)
910
+ def _force_slow_reader(self, monkeypatch):
911
+ monkeypatch.setattr(CsvFile, "_can_use_fast_csv_reader", lambda self: False)
903
912
 
904
913
  def _make_csvfile_from_content(self, content, tmp_path, delim=",", quote='"', escape=None):
905
914
  p = tmp_path / "test.csv"
@@ -657,7 +657,7 @@ wheels = [
657
657
 
658
658
  [[package]]
659
659
  name = "execsql2"
660
- version = "2.12.2"
660
+ version = "2.12.3"
661
661
  source = { editable = "." }
662
662
  dependencies = [
663
663
  { name = "rich" },
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes