execsql2 2.12.1__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.3/.claude/agents/liaison.md +107 -0
  2. {execsql2-2.12.1 → execsql2-2.12.3}/CHANGELOG.md +23 -0
  3. {execsql2-2.12.1 → execsql2-2.12.3}/CLAUDE.md +1 -0
  4. {execsql2-2.12.1 → execsql2-2.12.3}/PKG-INFO +2 -1
  5. {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/divergence.md +7 -0
  6. {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/installation.md +4 -0
  7. {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/metacommands.md +3 -1
  8. {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/security.md +59 -1
  9. {execsql2-2.12.1 → execsql2-2.12.3}/pyproject.toml +3 -2
  10. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/base.py +7 -0
  11. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exceptions.py +2 -0
  12. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/delimited.py +44 -2
  13. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/control.py +1 -1
  14. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_fileops.py +4 -0
  15. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/__init__.py +4 -0
  16. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/engine.py +55 -23
  17. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/variables.py +35 -2
  18. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_delimited.py +12 -3
  19. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_assert.py +14 -1
  20. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_exceptions.py +1 -0
  21. {execsql2-2.12.1 → execsql2-2.12.3}/uv.lock +1 -1
  22. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/dba.md +0 -0
  23. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/herald.md +0 -0
  24. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/inspector.md +0 -0
  25. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/oracle.md +0 -0
  26. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/patcher.md +0 -0
  27. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/qa.md +0 -0
  28. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/agents/scribe.md +0 -0
  29. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/code-oracle.md +0 -0
  30. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/migrate.md +0 -0
  31. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/review-changes.md +0 -0
  32. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/test-module.md +0 -0
  33. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/update-changelog.md +0 -0
  34. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/commands/where-is.md +0 -0
  35. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/project_context.md +0 -0
  36. {execsql2-2.12.1 → execsql2-2.12.3}/.claude/state/status.md +0 -0
  37. {execsql2-2.12.1 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  38. {execsql2-2.12.1 → execsql2-2.12.3}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  39. {execsql2-2.12.1 → execsql2-2.12.3}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  40. {execsql2-2.12.1 → execsql2-2.12.3}/.github/workflows/ci-cd.yml +0 -0
  41. {execsql2-2.12.1 → execsql2-2.12.3}/.gitignore +0 -0
  42. {execsql2-2.12.1 → execsql2-2.12.3}/.pre-commit-config.yaml +0 -0
  43. {execsql2-2.12.1 → execsql2-2.12.3}/.pre-commit-hooks.yaml +0 -0
  44. {execsql2-2.12.1 → execsql2-2.12.3}/.python-version +0 -0
  45. {execsql2-2.12.1 → execsql2-2.12.3}/.readthedocs.yaml +0 -0
  46. {execsql2-2.12.1 → execsql2-2.12.3}/CONTRIBUTING.md +0 -0
  47. {execsql2-2.12.1 → execsql2-2.12.3}/LICENSE.txt +0 -0
  48. {execsql2-2.12.1 → execsql2-2.12.3}/NOTICE +0 -0
  49. {execsql2-2.12.1 → execsql2-2.12.3}/README.md +0 -0
  50. {execsql2-2.12.1 → execsql2-2.12.3}/SECURITY.md +0 -0
  51. {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/contributors.md +0 -0
  52. {execsql2-2.12.1 → execsql2-2.12.3}/docs/about/copyright.md +0 -0
  53. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/cli.md +0 -0
  54. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/db.md +0 -0
  55. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/exporters.md +0 -0
  56. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/importers.md +0 -0
  57. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/index.md +0 -0
  58. {execsql2-2.12.1 → execsql2-2.12.3}/docs/api/metacommands.md +0 -0
  59. {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_db_adapters.md +0 -0
  60. {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_exporters.md +0 -0
  61. {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_importers.md +0 -0
  62. {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/adding_metacommands.md +0 -0
  63. {execsql2-2.12.1 → execsql2-2.12.3}/docs/dev/architecture.md +0 -0
  64. {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/requirements.md +0 -0
  65. {execsql2-2.12.1 → execsql2-2.12.3}/docs/getting-started/syntax.md +0 -0
  66. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/debugging.md +0 -0
  67. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/documentation.md +0 -0
  68. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/encoding.md +0 -0
  69. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/examples.md +0 -0
  70. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/formatter.md +0 -0
  71. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/logging.md +0 -0
  72. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/sql_syntax.md +0 -0
  73. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/usage.md +0 -0
  74. {execsql2-2.12.1 → execsql2-2.12.3}/docs/guides/using_scripts.md +0 -0
  75. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/Compare_planets.png +0 -0
  76. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/actions.png +0 -0
  77. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/actions2.png +0 -0
  78. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/checkboxes.png +0 -0
  79. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/connect.b64 +0 -0
  80. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/connect.png +0 -0
  81. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/create_conf.png +0 -0
  82. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/data_error1_screenshot.jpg +0 -0
  83. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/entry_form.png +0 -0
  84. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/execsql_console.png +0 -0
  85. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/execsql_logo_01.png +0 -0
  86. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/fatals.png +0 -0
  87. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/logo_small.png +0 -0
  88. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal.png +0 -0
  89. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal_sm.b64 +0 -0
  90. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/pause_terminal_sm.png +0 -0
  91. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/prompt_compare.png +0 -0
  92. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/set_build_commands.jpg +0 -0
  93. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unit_conversions.b64 +0 -0
  94. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unit_conversions_029.png +0 -0
  95. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/unmatched.png +0 -0
  96. {execsql2-2.12.1 → execsql2-2.12.3}/docs/images/vim_execsql_highlight.png +0 -0
  97. {execsql2-2.12.1 → execsql2-2.12.3}/docs/index.md +0 -0
  98. {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/configuration.md +0 -0
  99. {execsql2-2.12.1 → execsql2-2.12.3}/docs/reference/substitution_vars.md +0 -0
  100. {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/README.md +0 -0
  101. {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/package.json +0 -0
  102. {execsql2-2.12.1 → execsql2-2.12.3}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  103. {execsql2-2.12.1 → execsql2-2.12.3}/justfile +0 -0
  104. {execsql2-2.12.1 → execsql2-2.12.3}/scripts/generate_vscode_grammar.py +0 -0
  105. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/__init__.py +0 -0
  106. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/__main__.py +0 -0
  107. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/__init__.py +0 -0
  108. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/dsn.py +0 -0
  109. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/help.py +0 -0
  110. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/lint.py +0 -0
  111. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/cli/run.py +0 -0
  112. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/config.py +0 -0
  113. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/constants.py +0 -0
  114. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/__init__.py +0 -0
  115. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/access.py +0 -0
  116. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/dsn.py +0 -0
  117. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/duckdb.py +0 -0
  118. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/factory.py +0 -0
  119. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/firebird.py +0 -0
  120. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/mysql.py +0 -0
  121. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/oracle.py +0 -0
  122. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/postgres.py +0 -0
  123. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/sqlite.py +0 -0
  124. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/db/sqlserver.py +0 -0
  125. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/debug/__init__.py +0 -0
  126. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/debug/repl.py +0 -0
  127. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/__init__.py +0 -0
  128. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/base.py +0 -0
  129. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/duckdb.py +0 -0
  130. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/feather.py +0 -0
  131. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/html.py +0 -0
  132. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/json.py +0 -0
  133. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/latex.py +0 -0
  134. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/markdown.py +0 -0
  135. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/ods.py +0 -0
  136. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/parquet.py +0 -0
  137. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/pretty.py +0 -0
  138. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/protocol.py +0 -0
  139. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/raw.py +0 -0
  140. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/sqlite.py +0 -0
  141. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/templates.py +0 -0
  142. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/values.py +0 -0
  143. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xls.py +0 -0
  144. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xlsx.py +0 -0
  145. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/xml.py +0 -0
  146. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/yaml.py +0 -0
  147. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/exporters/zip.py +0 -0
  148. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/format.py +0 -0
  149. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/__init__.py +0 -0
  150. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/base.py +0 -0
  151. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/console.py +0 -0
  152. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/desktop.py +0 -0
  153. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/gui/tui.py +0 -0
  154. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/__init__.py +0 -0
  155. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/base.py +0 -0
  156. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/csv.py +0 -0
  157. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/feather.py +0 -0
  158. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/ods.py +0 -0
  159. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/importers/xls.py +0 -0
  160. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/__init__.py +0 -0
  161. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/conditions.py +0 -0
  162. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/connect.py +0 -0
  163. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/data.py +0 -0
  164. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/debug.py +0 -0
  165. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/dispatch.py +0 -0
  166. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io.py +0 -0
  167. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_export.py +0 -0
  168. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_import.py +0 -0
  169. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/io_write.py +0 -0
  170. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/prompt.py +0 -0
  171. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/script_ext.py +0 -0
  172. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/metacommands/system.py +0 -0
  173. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/models.py +0 -0
  174. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/parser.py +0 -0
  175. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/py.typed +0 -0
  176. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/script/control.py +0 -0
  177. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/state.py +0 -0
  178. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/types.py +0 -0
  179. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/__init__.py +0 -0
  180. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/auth.py +0 -0
  181. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/crypto.py +0 -0
  182. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/datetime.py +0 -0
  183. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/errors.py +0 -0
  184. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/fileio.py +0 -0
  185. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/gui.py +0 -0
  186. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/mail.py +0 -0
  187. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/numeric.py +0 -0
  188. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/regex.py +0 -0
  189. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/strings.py +0 -0
  190. {execsql2-2.12.1 → execsql2-2.12.3}/src/execsql/utils/timer.py +0 -0
  191. {execsql2-2.12.1 → execsql2-2.12.3}/templates/README.md +0 -0
  192. {execsql2-2.12.1 → execsql2-2.12.3}/templates/config_settings.sqlite +0 -0
  193. {execsql2-2.12.1 → execsql2-2.12.3}/templates/example_config_prompt.sql +0 -0
  194. {execsql2-2.12.1 → execsql2-2.12.3}/templates/execsql.conf +0 -0
  195. {execsql2-2.12.1 → execsql2-2.12.3}/templates/make_config_db.sql +0 -0
  196. {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_compare.sql +0 -0
  197. {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_glossary.sql +0 -0
  198. {execsql2-2.12.1 → execsql2-2.12.3}/templates/md_upsert.sql +0 -0
  199. {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_compare.sql +0 -0
  200. {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_glossary.sql +0 -0
  201. {execsql2-2.12.1 → execsql2-2.12.3}/templates/pg_upsert.sql +0 -0
  202. {execsql2-2.12.1 → execsql2-2.12.3}/templates/script_template.sql +0 -0
  203. {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_compare.sql +0 -0
  204. {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_glossary.sql +0 -0
  205. {execsql2-2.12.1 → execsql2-2.12.3}/templates/ss_upsert.sql +0 -0
  206. {execsql2-2.12.1 → execsql2-2.12.3}/tests/__init__.py +0 -0
  207. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/__init__.py +0 -0
  208. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli.py +0 -0
  209. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli_e2e.py +0 -0
  210. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_cli_run.py +0 -0
  211. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_lint.py +0 -0
  212. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_ping.py +0 -0
  213. {execsql2-2.12.1 → execsql2-2.12.3}/tests/cli/test_profile.py +0 -0
  214. {execsql2-2.12.1 → execsql2-2.12.3}/tests/conftest.py +0 -0
  215. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/__init__.py +0 -0
  216. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_base.py +0 -0
  217. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_duckdb.py +0 -0
  218. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_factory.py +0 -0
  219. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_postgres.py +0 -0
  220. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_sqlite.py +0 -0
  221. {execsql2-2.12.1 → execsql2-2.12.3}/tests/db/test_sqlite_extra.py +0 -0
  222. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/__init__.py +0 -0
  223. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_base.py +0 -0
  224. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_db.py +0 -0
  225. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_duckdb_exporter.py +0 -0
  226. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_exporters.py +0 -0
  227. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_feather.py +0 -0
  228. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_html_latex.py +0 -0
  229. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_json.py +0 -0
  230. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_markdown.py +0 -0
  231. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_ods.py +0 -0
  232. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_parquet.py +0 -0
  233. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_sqlite_exporter.py +0 -0
  234. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_templates.py +0 -0
  235. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xls_xlsx.py +0 -0
  236. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xlsx.py +0 -0
  237. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_xml.py +0 -0
  238. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_yaml.py +0 -0
  239. {execsql2-2.12.1 → execsql2-2.12.3}/tests/exporters/test_zip.py +0 -0
  240. {execsql2-2.12.1 → execsql2-2.12.3}/tests/gui/__init__.py +0 -0
  241. {execsql2-2.12.1 → execsql2-2.12.3}/tests/gui/test_backends.py +0 -0
  242. {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/__init__.py +0 -0
  243. {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_csv_importer.py +0 -0
  244. {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_feather_importer.py +0 -0
  245. {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_ods_importer.py +0 -0
  246. {execsql2-2.12.1 → execsql2-2.12.3}/tests/importers/test_xls_importer.py +0 -0
  247. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/__init__.py +0 -0
  248. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/conftest.py +0 -0
  249. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_dsn.py +0 -0
  250. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_duckdb.py +0 -0
  251. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_mysql.py +0 -0
  252. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_postgres.py +0 -0
  253. {execsql2-2.12.1 → execsql2-2.12.3}/tests/integration/test_sqlite.py +0 -0
  254. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/__init__.py +0 -0
  255. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_breakpoint.py +0 -0
  256. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_connect.py +0 -0
  257. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_io_export.py +0 -0
  258. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_io_import.py +0 -0
  259. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands.py +0 -0
  260. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_connect.py +0 -0
  261. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_data.py +0 -0
  262. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_extended.py +0 -0
  263. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  264. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io.py +0 -0
  265. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  266. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  267. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system.py +0 -0
  268. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  269. {execsql2-2.12.1 → execsql2-2.12.3}/tests/metacommands/test_row_count.py +0 -0
  270. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_config.py +0 -0
  271. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_config_data.py +0 -0
  272. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_constants.py +0 -0
  273. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_engine.py +0 -0
  274. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_error_messages.py +0 -0
  275. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_format.py +0 -0
  276. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_mail.py +0 -0
  277. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_models.py +0 -0
  278. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_package.py +0 -0
  279. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_parser.py +0 -0
  280. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_registry.py +0 -0
  281. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_script.py +0 -0
  282. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_state.py +0 -0
  283. {execsql2-2.12.1 → execsql2-2.12.3}/tests/test_types.py +0 -0
  284. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/__init__.py +0 -0
  285. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_auth.py +0 -0
  286. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_auth_extra.py +0 -0
  287. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_crypto.py +0 -0
  288. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_datetime.py +0 -0
  289. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_errors.py +0 -0
  290. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_errors_extra.py +0 -0
  291. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_fileio.py +0 -0
  292. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_fileio_extra.py +0 -0
  293. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_numeric.py +0 -0
  294. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_regex.py +0 -0
  295. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_strings.py +0 -0
  296. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_timer.py +0 -0
  297. {execsql2-2.12.1 → execsql2-2.12.3}/tests/utils/test_timer_extra.py +0 -0
  298. {execsql2-2.12.1 → execsql2-2.12.3}/zensical.toml +0 -0
@@ -0,0 +1,107 @@
1
+ ---
2
+ name: The Liaison
3
+ description: Integration specialist for pg-upsert and execsql2. Tracks the pg-upsert codebase (../pg-upsert), designs the optional dependency integration, and owns the UPSERT metacommand. Reads both codebases — writes only in execsql2.
4
+ model: sonnet
5
+ color: yellow
6
+ ---
7
+
8
+ You are a senior Python engineer who specializes in integrating the **pg-upsert** library with the **execsql2** project. You are the bridge between two codebases maintained by the same author:
9
+
10
+ - **execsql2** — `/Users/cgrant/GitHub/geocoug/execsql/` (this repo)
11
+ - **pg-upsert** — `/Users/cgrant/GitHub/geocoug/pg-upsert/` (sibling repo)
12
+
13
+ Your job is to keep the integration plan current, design the `UPSERT` metacommand, and ensure changes in either codebase don't break the integration path.
14
+
15
+ ## Expertise
16
+
17
+ You have deep, working knowledge of:
18
+
19
+ **pg-upsert internals:** The `PgUpsert` class (QA checks: null, PK, FK, check constraint validation), `PostgresDB` connection wrapper, the staging-to-base upsert workflow, topological table ordering, the `ups_control` temporary table, and the `psycopg2.sql` parameterized SQL generation patterns.
20
+
21
+ **execsql2 extension points:** The metacommand dispatch system (`MetaCommandList`, `build_dispatch_table()`, `x_*` handlers), `DatabasePool` connection management, substitution variables (`SubVarSet`), transaction/autocommit model, the `PostgresDatabase` adapter, and the existing upsert SQL templates in `templates/pg_upsert.sql`.
22
+
23
+ **Integration patterns:** Optional dependency detection (`importlib.util.find_spec`), connection sharing between libraries, adapter patterns for bridging different transaction models, and graceful degradation when an optional package is not installed.
24
+
25
+ ______________________________________________________________________
26
+
27
+ ## First Actions (always, before doing any work)
28
+
29
+ 1. **Read `.claude/project_context.md`** — load the execsql2 module layout and architectural overview.
30
+ 2. **Read your briefing** if one exists at `.claude/comms/briefings/liaison-*.md` — follow the DBA's specific instructions.
31
+ 3. **Read the integration plan** at `../pg-upsert/.claude/plans/refactor-and-execsql-integration.md` — this is the canonical plan. Understand the current phase and what's been decided.
32
+ 4. **Check pg-upsert's current state** — read `../pg-upsert/src/pg_upsert/upsert.py` and `../pg-upsert/src/pg_upsert/postgres.py` to understand the latest API surface. pg-upsert is under active refactoring; the API may have changed since the plan was written.
33
+ 5. **Check execsql2's metacommand system** — read `src/execsql/metacommands/dispatch.py` to understand registration patterns.
34
+
35
+ ______________________________________________________________________
36
+
37
+ ## Key Design Decisions (established)
38
+
39
+ These have been agreed upon. Do not revisit unless the human asks:
40
+
41
+ 1. **pg-upsert is an optional dependency** — execsql2 must work without it installed. The `UPSERT` metacommand raises a clear error if pg-upsert is missing.
42
+ 2. **Connection sharing** — the metacommand passes execsql's existing `psycopg2` connection to `PgUpsert(conn=...)`. No second connection.
43
+ 3. **Results map to substitution variables** — upsert stats (rows updated, inserted, QA errors) are exposed as `!!$UPSERT_*!!` substitution variables.
44
+ 4. **execsql's transaction model governs** — the metacommand respects execsql's `AUTOCOMMIT ON/OFF` state, not pg-upsert's `do_commit` flag.
45
+ 5. **No Tkinter dependency** — the interactive GUI is not used when called from execsql. QA results go to execsql's logging/debug REPL instead.
46
+
47
+ ______________________________________________________________________
48
+
49
+ ## pg-upsert API Surface (reference)
50
+
51
+ The integration depends on these pg-upsert interfaces. If any change, the integration plan must be updated.
52
+
53
+ ```python
54
+ # Core class — this is what the metacommand will call
55
+ PgUpsert(
56
+ conn=psycopg2_connection, # Shared from execsql
57
+ tables=["t1", "t2"],
58
+ staging_schema="staging",
59
+ base_schema="public",
60
+ do_commit=False, # execsql controls commits
61
+ interactive=False, # No GUI from execsql
62
+ upsert_method="upsert", # "upsert" | "update" | "insert"
63
+ exclude_cols=["col1"],
64
+ exclude_null_check_cols=["col2"],
65
+ )
66
+
67
+ # Methods the metacommand will call
68
+ .qa_all() # Run all QA checks, returns self
69
+ .upsert_all() # Run upsert on all tables, returns self
70
+ .commit() # Commit/rollback based on do_commit + qa_passed
71
+ .run() # qa_all() → upsert_all() → commit()
72
+
73
+ # State the metacommand will read
74
+ .qa_passed # bool — did all QA checks pass?
75
+ .control_table # str — name of temp table with per-table results
76
+ ```
77
+
78
+ ______________________________________________________________________
79
+
80
+ ## What to Produce
81
+
82
+ Depending on the task, you may produce:
83
+
84
+ 1. **Integration status reports** — what's changed in pg-upsert, what that means for execsql integration
85
+ 2. **Metacommand design** — regex patterns, handler signatures, SQL syntax for `UPSERT` metacommand
86
+ 3. **Implementation code** — `src/execsql/metacommands/upsert.py` and dispatch registration
87
+ 4. **Compatibility notes** — API changes in pg-upsert that require adaptation
88
+ 5. **Test specifications** — what the QA agent should test for the integration
89
+
90
+ ______________________________________________________________________
91
+
92
+ ## Syndicate Protocol
93
+
94
+ When working as part of the SQL Syndicate:
95
+
96
+ 1. Read your briefing from `.claude/comms/briefings/liaison-*.md`
97
+ 2. Do your research across both codebases
98
+ 3. Write your report to `.claude/comms/reports/liaison-{YYYY-MM-DD}.md`
99
+ 4. Write integration artifacts to `.claude/plans/` (for design) or `.claude/patches/` (for implementation)
100
+
101
+ ## Constraints
102
+
103
+ - **Writes only in execsql2.** You may read `../pg-upsert/` freely but never modify files there.
104
+ - **pg-upsert is a moving target.** Always re-read the current pg-upsert source before making claims about its API — don't rely on cached knowledge.
105
+ - **Optional means optional.** Never add pg-upsert to execsql2's required dependencies. Use `extras_require` / optional dependency groups.
106
+ - **Preserve existing upsert templates.** The SQL templates in `templates/pg_upsert.sql` must continue to work. The metacommand is an alternative, not a replacement.
107
+ - Follow all execsql2 project constraints from CLAUDE.md (ruff, Python 3.10+, coverage floor, changelog, docs, divergence tracking).
@@ -13,6 +13,29 @@ ______________________________________________________________________
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
+
27
+ ## [2.12.2] - 2026-04-02
28
+
29
+ ### Added
30
+
31
+ - Documentation: keyring setup guide for headless Linux servers (encrypted and plaintext file backends) in the Security reference page, with a cross-reference from the Installation page.
32
+
33
+ ### Changed
34
+
35
+ - `ASSERT` failures now report `**** Assertion failed.` instead of `**** Error in metacommand.` to distinguish intentional script-level checks from actual metacommand errors.
36
+
37
+ ______________________________________________________________________
38
+
16
39
  ## [2.12.1] - 2026-04-02
17
40
 
18
41
  ### Changed
@@ -22,6 +22,7 @@ A multi-agent system where specialized agents collaborate to improve, extend, de
22
22
  - `.claude/docs-drafts/` — Scribe's documentation drafts
23
23
  - `.claude/releases/` — Herald's release notes and changelog entries
24
24
  - `.claude/state/` — Shared state (current phase, active task, agent status)
25
+ - `../pg-upsert/.claude/plans/` — pg-upsert integration plan (read by The Liaison)
25
26
 
26
27
  ## Communication Protocol
27
28
 
@@ -1,7 +1,8 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.12.1
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
+ Project-URL: Homepage, https://execsql2.readthedocs.io
5
6
  Project-URL: Repository, https://github.com/geocoug/execsql
6
7
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
7
8
  Author-email: Dreas Nielsen <cortice@tutanota.com>
@@ -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
 
@@ -174,6 +180,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
174
180
 
175
181
  - **Exception hierarchy** — All custom exceptions inherit from `ExecSqlError`, enabling `except ExecSqlError` to catch any execsql-originated error.
176
182
  - **Exception chaining** — All `raise` statements inside `except` blocks preserve the original traceback via `from`.
183
+ - **ASSERT error type** — `ASSERT` failures now use a dedicated `"assert"` error type that produces `**** Assertion failed.` instead of `**** Error in metacommand.`. This distinguishes intentional script-level checks from actual metacommand errors. Upstream did not have `ASSERT`.
177
184
 
178
185
  ______________________________________________________________________
179
186
 
@@ -30,3 +30,7 @@ pip install "execsql2[all]" # Everything
30
30
  ```
31
31
 
32
32
  In addition to the *execsql* program itself, additional Python libraries may need to be installed to use *execsql* with specific types of DBMSs and spreadsheets. The additional libraries that may be needed are listed in the [Requirements](requirements.md#requirements) section.
33
+
34
+ !!! tip "Keyring on headless Linux"
35
+
36
+ If you install `execsql2[auth]` on a headless Linux server (no desktop environment), the keyring backend needs manual configuration. See [Keyring Platform Setup](../reference/security.md#keyring_setup) for instructions.
@@ -56,7 +56,9 @@ ASSERT <condition> "<failure message>"
56
56
  ASSERT <condition> '<failure message>'
57
57
  ```
58
58
 
59
- Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
59
+ Evaluates `<condition>` using the same expression engine as [IF](#if_cmd). If the condition is `True`, execution continues silently (and the result is written to the log). If the condition is `False`, an assertion error is raised with the provided failure message. If no message is supplied, the default message is `Assertion failed: <condition>`.
60
+
61
+ A failed assertion produces `**** Assertion failed.` (not `**** Error in metacommand.`) to make it clear that the script's own check caught a problem, not that execsql encountered an internal error.
60
62
 
61
63
  When [HALT_ON_METACOMMAND_ERROR](#config) is `ON` (the default), a failed assertion halts the script. When it is `OFF`, execution continues after the failure is logged.
62
64
 
@@ -29,7 +29,7 @@ When execsql needs a database password and none is stored or configured, it prom
29
29
 
30
30
  ### OS credential store (keyring)
31
31
 
32
- When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use. Supported stores are macOS Keychain, Windows Credential Manager, and Linux SecretService. Keyring service names follow the pattern:
32
+ When the optional `keyring` package is installed (`pip install execsql2[auth]`), execsql checks the OS credential store before prompting. After a successful interactive prompt, the password is automatically stored for future use. Keyring service names follow the pattern:
33
33
 
34
34
  ```text
35
35
  execsql/<db_type>/<server>/<database>
@@ -37,6 +37,64 @@ execsql/<db_type>/<server>/<database>
37
37
 
38
38
  To disable keyring integration, set `use_keyring = No` in the `[connect]` section of `execsql.conf`.
39
39
 
40
+ #### Platform setup { #keyring_setup }
41
+
42
+ **macOS** — Works out of the box. Keyring uses the macOS Keychain automatically.
43
+
44
+ **Windows** — Works out of the box. Keyring uses the Windows Credential Manager automatically.
45
+
46
+ **Linux (desktop with GNOME/KDE)** — Works out of the box if a SecretService provider (GNOME Keyring or KWallet) is running.
47
+
48
+ **Linux (headless / remote / server)** — No secret service is typically available, so keyring silently fails to store passwords. You need to configure a file-based backend. There are two options:
49
+
50
+ ##### Option A: Encrypted file backend (recommended) { #keyring_encrypted }
51
+
52
+ Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
53
+
54
+ ```bash
55
+ pip install keyrings.alt pycryptodome
56
+ mkdir -p ~/.config/python_keyring
57
+ cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
58
+ [backend]
59
+ default-keyring=keyrings.alt.file.EncryptedKeyring
60
+ EOF
61
+ ```
62
+
63
+ The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_pass.cfg`. You will be prompted for a master password once per session (e.g., when you first run execsql after logging in).
64
+
65
+ ##### Option B: Plaintext file backend (no prompts) { #keyring_plaintext }
66
+
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
+
69
+ ```bash
70
+ pip install keyrings.alt
71
+ mkdir -p ~/.config/python_keyring
72
+ cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
73
+ [backend]
74
+ default-keyring=keyrings.alt.file.PlaintextKeyring
75
+ EOF
76
+ ```
77
+
78
+ Passwords are stored at `~/.local/share/python_keyring/keyring_pass.cfg`. Restrict access to this file:
79
+
80
+ ```bash
81
+ chmod 600 ~/.local/share/python_keyring/keyring_pass.cfg
82
+ ```
83
+
84
+ !!! warning "Plaintext storage"
85
+
86
+ The plaintext backend stores passwords without encryption. Only use this on machines where the filesystem is already secured (encrypted disk, single-user access, restricted permissions). Prefer the encrypted backend when possible.
87
+
88
+ ##### Verifying keyring setup { #keyring_verify }
89
+
90
+ After configuring a backend, verify it works:
91
+
92
+ ```bash
93
+ python -c "import keyring; keyring.set_password('test', 'user', 'pw'); print(keyring.get_password('test', 'user'))"
94
+ ```
95
+
96
+ This should print `pw` without errors. If it does, the next time execsql prompts for a database password, it will store it and skip the prompt on future runs.
97
+
40
98
  ### `enc_password` in execsql.conf
41
99
 
42
100
  !!! warning "Obfuscation only — not encryption"
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.12.1"
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" }
@@ -91,6 +91,7 @@ execsql = "execsql.cli:_legacy_main"
91
91
  execsql-format = "execsql.format:main"
92
92
 
93
93
  [project.urls]
94
+ Homepage = "https://execsql2.readthedocs.io"
94
95
  Repository = "https://github.com/geocoug/execsql"
95
96
  Issues = "https://github.com/geocoug/execsql/issues"
96
97
 
@@ -158,7 +159,7 @@ skip-magic-trailing-comma = false
158
159
  line-ending = "auto"
159
160
 
160
161
  [tool.bumpversion]
161
- current_version = "2.12.1"
162
+ current_version = "2.12.3"
162
163
  commit = true
163
164
  commit_args = "--no-verify"
164
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."""
@@ -132,6 +132,8 @@ class ErrInfo(ExecSqlError):
132
132
 
133
133
  if self.type == "db":
134
134
  self.error_message = "**** Error in SQL statement."
135
+ elif self.type == "assert":
136
+ self.error_message = "**** Assertion failed."
135
137
  elif self.type == "cmd":
136
138
  self.error_message = "**** Error in metacommand."
137
139
  elif self.type == "log":
@@ -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:
@@ -66,7 +66,7 @@ def x_assert(**kwargs: Any) -> None:
66
66
  if _state.exec_log is not None:
67
67
  _state.exec_log.log_user_msg(f"ASSERT passed: {condition}")
68
68
  else:
69
- raise ErrInfo(type="cmd", other_msg=message)
69
+ raise ErrInfo(type="assert", other_msg=message)
70
70
 
71
71
 
72
72
  def x_if(**kwargs: Any) -> None:
@@ -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: