execsql2 2.12.0__tar.gz → 2.12.2__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/.claude/agents/liaison.md +107 -0
  2. {execsql2-2.12.0 → execsql2-2.12.2}/CHANGELOG.md +28 -0
  3. {execsql2-2.12.0 → execsql2-2.12.2}/CLAUDE.md +1 -0
  4. {execsql2-2.12.0 → execsql2-2.12.2}/PKG-INFO +2 -1
  5. {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/divergence.md +1 -0
  6. {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/installation.md +4 -0
  7. {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/metacommands.md +3 -1
  8. {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/security.md +59 -1
  9. {execsql2-2.12.0 → execsql2-2.12.2}/pyproject.toml +3 -2
  10. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/base.py +37 -23
  11. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exceptions.py +2 -0
  12. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/control.py +1 -1
  13. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/engine.py +13 -4
  14. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/variables.py +2 -25
  15. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_base.py +5 -5
  16. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_assert.py +14 -1
  17. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_engine.py +3 -4
  18. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_exceptions.py +1 -0
  19. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_script.py +12 -16
  20. {execsql2-2.12.0 → execsql2-2.12.2}/uv.lock +1 -1
  21. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/dba.md +0 -0
  22. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/herald.md +0 -0
  23. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/inspector.md +0 -0
  24. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/oracle.md +0 -0
  25. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/patcher.md +0 -0
  26. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/qa.md +0 -0
  27. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/agents/scribe.md +0 -0
  28. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/code-oracle.md +0 -0
  29. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/migrate.md +0 -0
  30. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/review-changes.md +0 -0
  31. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/test-module.md +0 -0
  32. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/update-changelog.md +0 -0
  33. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/commands/where-is.md +0 -0
  34. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/project_context.md +0 -0
  35. {execsql2-2.12.0 → execsql2-2.12.2}/.claude/state/status.md +0 -0
  36. {execsql2-2.12.0 → execsql2-2.12.2}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  37. {execsql2-2.12.0 → execsql2-2.12.2}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  38. {execsql2-2.12.0 → execsql2-2.12.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  39. {execsql2-2.12.0 → execsql2-2.12.2}/.github/workflows/ci-cd.yml +0 -0
  40. {execsql2-2.12.0 → execsql2-2.12.2}/.gitignore +0 -0
  41. {execsql2-2.12.0 → execsql2-2.12.2}/.pre-commit-config.yaml +0 -0
  42. {execsql2-2.12.0 → execsql2-2.12.2}/.pre-commit-hooks.yaml +0 -0
  43. {execsql2-2.12.0 → execsql2-2.12.2}/.python-version +0 -0
  44. {execsql2-2.12.0 → execsql2-2.12.2}/.readthedocs.yaml +0 -0
  45. {execsql2-2.12.0 → execsql2-2.12.2}/CONTRIBUTING.md +0 -0
  46. {execsql2-2.12.0 → execsql2-2.12.2}/LICENSE.txt +0 -0
  47. {execsql2-2.12.0 → execsql2-2.12.2}/NOTICE +0 -0
  48. {execsql2-2.12.0 → execsql2-2.12.2}/README.md +0 -0
  49. {execsql2-2.12.0 → execsql2-2.12.2}/SECURITY.md +0 -0
  50. {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/contributors.md +0 -0
  51. {execsql2-2.12.0 → execsql2-2.12.2}/docs/about/copyright.md +0 -0
  52. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/cli.md +0 -0
  53. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/db.md +0 -0
  54. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/exporters.md +0 -0
  55. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/importers.md +0 -0
  56. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/index.md +0 -0
  57. {execsql2-2.12.0 → execsql2-2.12.2}/docs/api/metacommands.md +0 -0
  58. {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_db_adapters.md +0 -0
  59. {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_exporters.md +0 -0
  60. {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_importers.md +0 -0
  61. {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/adding_metacommands.md +0 -0
  62. {execsql2-2.12.0 → execsql2-2.12.2}/docs/dev/architecture.md +0 -0
  63. {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/requirements.md +0 -0
  64. {execsql2-2.12.0 → execsql2-2.12.2}/docs/getting-started/syntax.md +0 -0
  65. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/debugging.md +0 -0
  66. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/documentation.md +0 -0
  67. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/encoding.md +0 -0
  68. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/examples.md +0 -0
  69. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/formatter.md +0 -0
  70. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/logging.md +0 -0
  71. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/sql_syntax.md +0 -0
  72. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/usage.md +0 -0
  73. {execsql2-2.12.0 → execsql2-2.12.2}/docs/guides/using_scripts.md +0 -0
  74. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/Compare_planets.png +0 -0
  75. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/actions.png +0 -0
  76. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/actions2.png +0 -0
  77. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/checkboxes.png +0 -0
  78. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/connect.b64 +0 -0
  79. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/connect.png +0 -0
  80. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/create_conf.png +0 -0
  81. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/data_error1_screenshot.jpg +0 -0
  82. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/entry_form.png +0 -0
  83. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/execsql_console.png +0 -0
  84. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/execsql_logo_01.png +0 -0
  85. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/fatals.png +0 -0
  86. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/logo_small.png +0 -0
  87. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal.png +0 -0
  88. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal_sm.b64 +0 -0
  89. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/pause_terminal_sm.png +0 -0
  90. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/prompt_compare.png +0 -0
  91. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/set_build_commands.jpg +0 -0
  92. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unit_conversions.b64 +0 -0
  93. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unit_conversions_029.png +0 -0
  94. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/unmatched.png +0 -0
  95. {execsql2-2.12.0 → execsql2-2.12.2}/docs/images/vim_execsql_highlight.png +0 -0
  96. {execsql2-2.12.0 → execsql2-2.12.2}/docs/index.md +0 -0
  97. {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/configuration.md +0 -0
  98. {execsql2-2.12.0 → execsql2-2.12.2}/docs/reference/substitution_vars.md +0 -0
  99. {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/README.md +0 -0
  100. {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/package.json +0 -0
  101. {execsql2-2.12.0 → execsql2-2.12.2}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  102. {execsql2-2.12.0 → execsql2-2.12.2}/justfile +0 -0
  103. {execsql2-2.12.0 → execsql2-2.12.2}/scripts/generate_vscode_grammar.py +0 -0
  104. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/__init__.py +0 -0
  105. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/__main__.py +0 -0
  106. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/__init__.py +0 -0
  107. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/dsn.py +0 -0
  108. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/help.py +0 -0
  109. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/lint.py +0 -0
  110. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/cli/run.py +0 -0
  111. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/config.py +0 -0
  112. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/constants.py +0 -0
  113. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/__init__.py +0 -0
  114. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/access.py +0 -0
  115. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/dsn.py +0 -0
  116. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/duckdb.py +0 -0
  117. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/factory.py +0 -0
  118. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/firebird.py +0 -0
  119. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/mysql.py +0 -0
  120. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/oracle.py +0 -0
  121. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/postgres.py +0 -0
  122. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/sqlite.py +0 -0
  123. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/db/sqlserver.py +0 -0
  124. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/debug/__init__.py +0 -0
  125. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/debug/repl.py +0 -0
  126. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/__init__.py +0 -0
  127. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/base.py +0 -0
  128. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/delimited.py +0 -0
  129. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/duckdb.py +0 -0
  130. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/feather.py +0 -0
  131. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/html.py +0 -0
  132. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/json.py +0 -0
  133. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/latex.py +0 -0
  134. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/markdown.py +0 -0
  135. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/ods.py +0 -0
  136. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/parquet.py +0 -0
  137. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/pretty.py +0 -0
  138. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/protocol.py +0 -0
  139. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/raw.py +0 -0
  140. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/sqlite.py +0 -0
  141. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/templates.py +0 -0
  142. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/values.py +0 -0
  143. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xls.py +0 -0
  144. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xlsx.py +0 -0
  145. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/xml.py +0 -0
  146. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/yaml.py +0 -0
  147. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/exporters/zip.py +0 -0
  148. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/format.py +0 -0
  149. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/__init__.py +0 -0
  150. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/base.py +0 -0
  151. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/console.py +0 -0
  152. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/desktop.py +0 -0
  153. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/gui/tui.py +0 -0
  154. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/__init__.py +0 -0
  155. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/base.py +0 -0
  156. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/csv.py +0 -0
  157. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/feather.py +0 -0
  158. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/ods.py +0 -0
  159. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/importers/xls.py +0 -0
  160. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/__init__.py +0 -0
  161. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/conditions.py +0 -0
  162. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/connect.py +0 -0
  163. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/data.py +0 -0
  164. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/debug.py +0 -0
  165. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/dispatch.py +0 -0
  166. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io.py +0 -0
  167. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_export.py +0 -0
  168. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_fileops.py +0 -0
  169. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_import.py +0 -0
  170. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/io_write.py +0 -0
  171. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/prompt.py +0 -0
  172. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/script_ext.py +0 -0
  173. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/metacommands/system.py +0 -0
  174. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/models.py +0 -0
  175. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/parser.py +0 -0
  176. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/py.typed +0 -0
  177. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/__init__.py +0 -0
  178. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/script/control.py +0 -0
  179. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/state.py +0 -0
  180. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/types.py +0 -0
  181. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/__init__.py +0 -0
  182. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/auth.py +0 -0
  183. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/crypto.py +0 -0
  184. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/datetime.py +0 -0
  185. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/errors.py +0 -0
  186. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/fileio.py +0 -0
  187. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/gui.py +0 -0
  188. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/mail.py +0 -0
  189. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/numeric.py +0 -0
  190. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/regex.py +0 -0
  191. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/strings.py +0 -0
  192. {execsql2-2.12.0 → execsql2-2.12.2}/src/execsql/utils/timer.py +0 -0
  193. {execsql2-2.12.0 → execsql2-2.12.2}/templates/README.md +0 -0
  194. {execsql2-2.12.0 → execsql2-2.12.2}/templates/config_settings.sqlite +0 -0
  195. {execsql2-2.12.0 → execsql2-2.12.2}/templates/example_config_prompt.sql +0 -0
  196. {execsql2-2.12.0 → execsql2-2.12.2}/templates/execsql.conf +0 -0
  197. {execsql2-2.12.0 → execsql2-2.12.2}/templates/make_config_db.sql +0 -0
  198. {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_compare.sql +0 -0
  199. {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_glossary.sql +0 -0
  200. {execsql2-2.12.0 → execsql2-2.12.2}/templates/md_upsert.sql +0 -0
  201. {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_compare.sql +0 -0
  202. {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_glossary.sql +0 -0
  203. {execsql2-2.12.0 → execsql2-2.12.2}/templates/pg_upsert.sql +0 -0
  204. {execsql2-2.12.0 → execsql2-2.12.2}/templates/script_template.sql +0 -0
  205. {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_compare.sql +0 -0
  206. {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_glossary.sql +0 -0
  207. {execsql2-2.12.0 → execsql2-2.12.2}/templates/ss_upsert.sql +0 -0
  208. {execsql2-2.12.0 → execsql2-2.12.2}/tests/__init__.py +0 -0
  209. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/__init__.py +0 -0
  210. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli.py +0 -0
  211. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli_e2e.py +0 -0
  212. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_cli_run.py +0 -0
  213. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_lint.py +0 -0
  214. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_ping.py +0 -0
  215. {execsql2-2.12.0 → execsql2-2.12.2}/tests/cli/test_profile.py +0 -0
  216. {execsql2-2.12.0 → execsql2-2.12.2}/tests/conftest.py +0 -0
  217. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/__init__.py +0 -0
  218. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_duckdb.py +0 -0
  219. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_factory.py +0 -0
  220. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_postgres.py +0 -0
  221. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_sqlite.py +0 -0
  222. {execsql2-2.12.0 → execsql2-2.12.2}/tests/db/test_sqlite_extra.py +0 -0
  223. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/__init__.py +0 -0
  224. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_base.py +0 -0
  225. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_db.py +0 -0
  226. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_delimited.py +0 -0
  227. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_duckdb_exporter.py +0 -0
  228. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_exporters.py +0 -0
  229. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_feather.py +0 -0
  230. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_html_latex.py +0 -0
  231. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_json.py +0 -0
  232. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_markdown.py +0 -0
  233. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_ods.py +0 -0
  234. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_parquet.py +0 -0
  235. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_sqlite_exporter.py +0 -0
  236. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_templates.py +0 -0
  237. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xls_xlsx.py +0 -0
  238. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xlsx.py +0 -0
  239. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_xml.py +0 -0
  240. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_yaml.py +0 -0
  241. {execsql2-2.12.0 → execsql2-2.12.2}/tests/exporters/test_zip.py +0 -0
  242. {execsql2-2.12.0 → execsql2-2.12.2}/tests/gui/__init__.py +0 -0
  243. {execsql2-2.12.0 → execsql2-2.12.2}/tests/gui/test_backends.py +0 -0
  244. {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/__init__.py +0 -0
  245. {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_csv_importer.py +0 -0
  246. {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_feather_importer.py +0 -0
  247. {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_ods_importer.py +0 -0
  248. {execsql2-2.12.0 → execsql2-2.12.2}/tests/importers/test_xls_importer.py +0 -0
  249. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/__init__.py +0 -0
  250. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/conftest.py +0 -0
  251. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_dsn.py +0 -0
  252. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_duckdb.py +0 -0
  253. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_mysql.py +0 -0
  254. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_postgres.py +0 -0
  255. {execsql2-2.12.0 → execsql2-2.12.2}/tests/integration/test_sqlite.py +0 -0
  256. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/__init__.py +0 -0
  257. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_breakpoint.py +0 -0
  258. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_connect.py +0 -0
  259. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_io_export.py +0 -0
  260. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_io_import.py +0 -0
  261. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands.py +0 -0
  262. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_connect.py +0 -0
  263. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_data.py +0 -0
  264. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_extended.py +0 -0
  265. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  266. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_io.py +0 -0
  267. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  268. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  269. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_system.py +0 -0
  270. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  271. {execsql2-2.12.0 → execsql2-2.12.2}/tests/metacommands/test_row_count.py +0 -0
  272. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_config.py +0 -0
  273. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_config_data.py +0 -0
  274. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_constants.py +0 -0
  275. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_error_messages.py +0 -0
  276. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_format.py +0 -0
  277. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_mail.py +0 -0
  278. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_models.py +0 -0
  279. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_package.py +0 -0
  280. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_parser.py +0 -0
  281. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_registry.py +0 -0
  282. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_state.py +0 -0
  283. {execsql2-2.12.0 → execsql2-2.12.2}/tests/test_types.py +0 -0
  284. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/__init__.py +0 -0
  285. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_auth.py +0 -0
  286. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_auth_extra.py +0 -0
  287. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_crypto.py +0 -0
  288. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_datetime.py +0 -0
  289. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_errors.py +0 -0
  290. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_errors_extra.py +0 -0
  291. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_fileio.py +0 -0
  292. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_fileio_extra.py +0 -0
  293. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_numeric.py +0 -0
  294. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_regex.py +0 -0
  295. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_strings.py +0 -0
  296. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_timer.py +0 -0
  297. {execsql2-2.12.0 → execsql2-2.12.2}/tests/utils/test_timer_extra.py +0 -0
  298. {execsql2-2.12.0 → execsql2-2.12.2}/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,34 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.12.2] - 2026-04-02
17
+
18
+ ### Added
19
+
20
+ - 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.
21
+
22
+ ### Changed
23
+
24
+ - `ASSERT` failures now report `**** Assertion failed.` instead of `**** Error in metacommand.` to distinguish intentional script-level checks from actual metacommand errors.
25
+
26
+ ______________________________________________________________________
27
+
28
+ ## [2.12.1] - 2026-04-02
29
+
30
+ ### Changed
31
+
32
+ - Performance: removed dead `_compiled_patterns` dict from `SubVarSet` — eliminated 3 unused regex compilations per `add_substitution` call (~20 calls per statement in typical scripts).
33
+ - Performance: cached `source_dir` and `source_name` on `ScriptCmd` at construction time — eliminated per-statement `Path.resolve()` filesystem calls.
34
+ - Performance: `select_rowdict()` now uses batched `fetchmany()` instead of row-at-a-time `fetchone()`, matching `select_rowsource()` behavior for template exports.
35
+ - Performance: removed redundant `$CURRENT_TIME` set in `set_system_vars()` — now set once per statement in `run_and_increment()`.
36
+ - Performance: removed no-op `copy.copy()` on immutable string in `substitute_vars()`.
37
+
38
+ ### Fixed
39
+
40
+ - Fixed cursor leak in `select_rowsource()` — generator now closes the cursor in a `finally` block when exhausted or abandoned.
41
+
42
+ ______________________________________________________________________
43
+
16
44
  ## [2.12.0] - 2026-04-01
17
45
 
18
46
  ### Added
@@ -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.0
3
+ Version: 2.12.2
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>
@@ -174,6 +174,7 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
174
174
 
175
175
  - **Exception hierarchy** — All custom exceptions inherit from `ExecSqlError`, enabling `except ExecSqlError` to catch any execsql-originated error.
176
176
  - **Exception chaining** — All `raise` statements inside `except` blocks preserve the original traceback via `from`.
177
+ - **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
178
 
178
179
  ______________________________________________________________________
179
180
 
@@ -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.0"
7
+ version = "2.12.2"
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.0"
162
+ current_version = "2.12.2"
162
163
  commit = true
163
164
  commit_args = "--no-verify"
164
165
  tag = true
@@ -234,18 +234,22 @@ class Database(ABC):
234
234
  pass # Non-critical: some drivers lack rowcount support.
235
235
 
236
236
  def decode_row() -> Generator:
237
- while True:
238
- rows = curs.fetchmany()
239
- if not rows:
240
- break
241
- else:
242
- for row in rows:
243
- if self.encoding:
244
- yield [
245
- c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
246
- ]
247
- else:
248
- yield row
237
+ try:
238
+ while True:
239
+ rows = curs.fetchmany()
240
+ if not rows:
241
+ break
242
+ else:
243
+ for row in rows:
244
+ if self.encoding:
245
+ yield [
246
+ c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c
247
+ for c in row
248
+ ]
249
+ else:
250
+ yield row
251
+ finally:
252
+ curs.close()
249
253
 
250
254
  return [d[0] for d in curs.description], decode_row()
251
255
 
@@ -253,6 +257,10 @@ class Database(ABC):
253
257
  """Execute *sql* and return ``(column_names, row_iterator)`` where each row is a ``dict``."""
254
258
  # Return an iterable that yields dictionaries of row data
255
259
  curs = self.cursor()
260
+ try:
261
+ curs.arraysize = _state.conf.export_row_buffer
262
+ except Exception:
263
+ pass # Non-critical: not all drivers support arraysize.
256
264
  try:
257
265
  curs.execute(sql)
258
266
  except Exception:
@@ -264,18 +272,24 @@ class Database(ABC):
264
272
  pass # Non-critical: some drivers lack rowcount support.
265
273
  hdrs = [d[0] for d in curs.description]
266
274
 
267
- def dict_row() -> dict | None:
268
- row = curs.fetchone()
269
- if row:
270
- if self.encoding:
271
- r = [c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row]
272
- else:
273
- r = row
274
- return dict(zip(hdrs, r))
275
- else:
276
- return None
275
+ def dict_rows() -> Generator:
276
+ try:
277
+ while True:
278
+ rows = curs.fetchmany()
279
+ if not rows:
280
+ break
281
+ for row in rows:
282
+ if self.encoding:
283
+ r = [
284
+ c.decode(self.encoding, "backslashreplace") if isinstance(c, bytes) else c for c in row
285
+ ]
286
+ else:
287
+ r = row
288
+ yield dict(zip(hdrs, r))
289
+ finally:
290
+ curs.close()
277
291
 
278
- return hdrs, iter(dict_row, None)
292
+ return hdrs, dict_rows()
279
293
 
280
294
  def schema_exists(self, schema_name: str) -> bool:
281
295
  """Return ``True`` if *schema_name* exists in this database."""
@@ -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":
@@ -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:
@@ -397,6 +397,15 @@ class ScriptCmd:
397
397
  self.line_no = command_line_no
398
398
  self.command_type = command_type
399
399
  self.command = script_command
400
+ # MIGRATION NOTE: differs from monolith (execsql.py) — source_dir and source_name are
401
+ # resolved once at construction rather than on every statement execution. For absolute
402
+ # paths (the common case) the result is identical. For relative paths the value is
403
+ # anchored to the CWD at script-load time rather than at each statement's execution time;
404
+ # the original per-statement resolve could yield inconsistent values across statements of
405
+ # the same script if a CD metacommand ran between them.
406
+ _p = Path(command_source_name)
407
+ self.source_dir: str = str(_p.resolve().parent) + os.sep
408
+ self.source_name: str = _p.name
400
409
 
401
410
  def __repr__(self) -> str:
402
411
  return f"ScriptCmd({self.source!r}, {self.line_no!r}, {self.command_type!r}, {repr(self.command)!r})"
@@ -498,9 +507,9 @@ class CommandList:
498
507
  _state.subvars.add_substitution("$CURRENT_SCRIPT", cmditem.source)
499
508
  _state.subvars.add_substitution(
500
509
  "$CURRENT_SCRIPT_PATH",
501
- str(Path(cmditem.source).resolve().parent) + os.sep,
510
+ cmditem.source_dir,
502
511
  )
503
- _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", Path(cmditem.source).name)
512
+ _state.subvars.add_substitution("$CURRENT_SCRIPT_NAME", cmditem.source_name)
504
513
  _state.subvars.add_substitution("$CURRENT_SCRIPT_LINE", str(cmditem.line_no))
505
514
  _state.subvars.add_substitution("$SCRIPT_LINE", str(cmditem.line_no))
506
515
  if _state.step_mode:
@@ -709,7 +718,7 @@ def set_system_vars() -> None:
709
718
  "ON" if _state.conf.gui_wait_on_error_halt else "OFF",
710
719
  )
711
720
  _state.subvars.add_substitution("$CONSOLE_WAIT_WHEN_DONE_STATE", "ON" if _state.conf.gui_wait_on_exit else "OFF")
712
- _state.subvars.add_substitution("$CURRENT_TIME", datetime.datetime.now().strftime("%Y-%m-%d %H:%M"))
721
+ # $CURRENT_TIME is set per-statement in run_and_increment() for accuracy.
713
722
  _state.subvars.add_substitution("$CURRENT_DIR", str(Path(".").resolve()))
714
723
  _state.subvars.add_substitution("$CURRENT_PATH", str(Path(".").resolve()) + os.sep)
715
724
  _state.subvars.add_substitution("$CURRENT_ALIAS", _state.dbs.current_alias())
@@ -742,7 +751,7 @@ def substitute_vars(command_str: str, localvars: SubVarSet | None = None) -> str
742
751
  subs = _state.subvars.merge(localvars)
743
752
  else:
744
753
  subs = _state.subvars
745
- cmdstr = copy.copy(command_str)
754
+ cmdstr = command_str
746
755
  subs_made = True
747
756
  iterations = 0
748
757
  while subs_made:
@@ -89,7 +89,6 @@ class SubVarSet:
89
89
  # compatibility with external code.
90
90
  def __init__(self) -> None:
91
91
  self._subs_dict: dict[str, Any] = {}
92
- self._compiled_patterns: dict[str, tuple] = {}
93
92
  self.prefix_list: list[str] = ["$", "&", "@"]
94
93
  # Don't construct/compile on init because deepcopy() can't handle compiled regexes.
95
94
  self.var_rx = None
@@ -106,21 +105,6 @@ class SubVarSet:
106
105
  self._subs_dict = dict(value)
107
106
  else:
108
107
  self._subs_dict = dict(value)
109
- self._rebuild_all_patterns()
110
-
111
- def _compile_patterns_for(self, varname: str) -> tuple:
112
- """Compile and return the three regex patterns (plain, single-quoted, double-quoted) for *varname*."""
113
- match_escaped = "\\" + varname if varname[0] == "$" else varname
114
- pat = re.compile(f"!!{match_escaped}!!", re.I)
115
- patq = re.compile(f"!'!{match_escaped}!'!", re.I)
116
- patdq = re.compile(f'!"!{match_escaped}!"!', re.I)
117
- return (pat, patq, patdq)
118
-
119
- def _rebuild_all_patterns(self) -> None:
120
- """Rebuild compiled patterns for every variable currently stored."""
121
- self._compiled_patterns = {}
122
- for varname in self._subs_dict:
123
- self._compiled_patterns[varname] = self._compile_patterns_for(varname)
124
108
 
125
109
  def compile_var_rx(self) -> None:
126
110
  """Compile the variable-name validation regex from the current prefix list."""
@@ -141,14 +125,12 @@ class SubVarSet:
141
125
  self.check_var_name(template_str)
142
126
  old_sub = template_str.lower()
143
127
  self._subs_dict.pop(old_sub, None)
144
- self._compiled_patterns.pop(old_sub, None)
145
128
 
146
129
  def add_substitution(self, varname: str, repl_str: Any) -> None:
147
- """Add or overwrite a substitution variable, compiling its match patterns."""
130
+ """Add or overwrite a substitution variable."""
148
131
  self.check_var_name(varname)
149
132
  varname = varname.lower()
150
133
  self._subs_dict[varname] = repl_str
151
- self._compiled_patterns[varname] = self._compile_patterns_for(varname)
152
134
 
153
135
  def append_substitution(self, varname: str, repl_str: str) -> None:
154
136
  self.check_var_name(varname)
@@ -186,15 +168,10 @@ class SubVarSet:
186
168
  return template_str.lower() in self._subs_dict
187
169
 
188
170
  def merge(self, other_subvars: SubVarSet | None) -> SubVarSet:
189
- """Return a new SubVarSet with this object's variables merged with other_subvars.
190
-
191
- Copies dictionaries and pre-compiled patterns directly instead of
192
- re-adding variables one at a time, avoiding O(V) regex recompilation.
193
- """
171
+ """Return a new SubVarSet with this object's variables merged with other_subvars."""
194
172
  if other_subvars is not None:
195
173
  newsubs = SubVarSet()
196
174
  newsubs._subs_dict = {**self._subs_dict, **other_subvars._subs_dict}
197
- newsubs._compiled_patterns = {**self._compiled_patterns, **other_subvars._compiled_patterns}
198
175
  newsubs.prefix_list = list(set(self.prefix_list + other_subvars.prefix_list))
199
176
  newsubs.compile_var_rx()
200
177
  return newsubs
@@ -628,9 +628,9 @@ class TestSelectRowdictEncoding:
628
628
  mock_curs = MagicMock()
629
629
  mock_curs.description = [("name",)]
630
630
  mock_curs.rowcount = 1
631
- mock_curs.fetchone.side_effect = [
632
- (b"encoded value",),
633
- None,
631
+ mock_curs.fetchmany.side_effect = [
632
+ [(b"encoded value",)],
633
+ [],
634
634
  ]
635
635
 
636
636
  with patch.object(db, "cursor", return_value=mock_curs):
@@ -648,7 +648,7 @@ class TestSelectRowdictEncoding:
648
648
  mock_curs = MagicMock()
649
649
  mock_curs.description = [("score",)]
650
650
  mock_curs.rowcount = 1
651
- mock_curs.fetchone.side_effect = [(99,), None]
651
+ mock_curs.fetchmany.side_effect = [[(99,)], []]
652
652
 
653
653
  with patch.object(db, "cursor", return_value=mock_curs):
654
654
  _, it = db.select_rowdict("SELECT score FROM fake;")
@@ -664,7 +664,7 @@ class TestSelectRowdictEncoding:
664
664
  mock_curs = MagicMock()
665
665
  mock_curs.description = [("x",)]
666
666
  mock_curs.rowcount = 1
667
- mock_curs.fetchone.side_effect = [("plain",), None]
667
+ mock_curs.fetchmany.side_effect = [[("plain",)], []]
668
668
 
669
669
  with patch.object(db, "cursor", return_value=mock_curs):
670
670
  _, it = db.select_rowdict("SELECT x FROM fake;")
@@ -71,9 +71,22 @@ class TestXAssertRaisesOnFalseCondition:
71
71
  with (
72
72
  patch.object(_state, "xcmd_test", return_value=False),
73
73
  patch.object(_state, "exec_log", mock_log),
74
- pytest.raises(ErrInfo),
74
+ pytest.raises(ErrInfo) as exc_info,
75
75
  ):
76
76
  x_assert(condtest="ROWCOUNT > 0", message=None, metacommandline="ASSERT ROWCOUNT > 0")
77
+ assert exc_info.value.type == "assert"
78
+
79
+ def test_false_condition_eval_err_says_assertion_failed(self) -> None:
80
+ mock_log = _make_exec_log()
81
+ with (
82
+ patch.object(_state, "xcmd_test", return_value=False),
83
+ patch.object(_state, "exec_log", mock_log),
84
+ pytest.raises(ErrInfo) as exc_info,
85
+ ):
86
+ x_assert(condtest="ROWCOUNT > 0", message='"expected rows"', metacommandline="ASSERT ROWCOUNT > 0")
87
+ err_msg = exc_info.value.eval_err()
88
+ assert err_msg.startswith("**** Assertion failed.")
89
+ assert "expected rows" in err_msg
77
90
 
78
91
  def test_false_condition_with_double_quoted_message(self) -> None:
79
92
  mock_log = _make_exec_log()
@@ -706,7 +706,8 @@ class TestSetSystemVars:
706
706
  set_system_vars()
707
707
  assert _state.subvars.varvalue("$DB_USER") == "testuser"
708
708
 
709
- def test_populates_current_time(self, engine_state):
709
+ def test_current_time_not_set_by_set_system_vars(self, engine_state):
710
+ """$CURRENT_TIME is set per-statement in run_and_increment, not in set_system_vars."""
710
711
  pool = MagicMock()
711
712
  pool.current.return_value = self._make_db()
712
713
  pool.current_alias.return_value = "main"
@@ -715,9 +716,7 @@ class TestSetSystemVars:
715
716
  mock_timer.elapsed.return_value = 0
716
717
  _state.timer = mock_timer
717
718
  set_system_vars()
718
- t = _state.subvars.varvalue("$CURRENT_TIME")
719
- assert t is not None
720
- assert len(t) == 16 # "YYYY-MM-DD HH:MM"
719
+ assert _state.subvars.varvalue("$CURRENT_TIME") is None
721
720
 
722
721
  def test_populates_version_numbers(self, engine_state):
723
722
  pool = MagicMock()
@@ -83,6 +83,7 @@ class TestErrInfo:
83
83
  "err_type,expected_text",
84
84
  [
85
85
  ("db", "**** Error in SQL statement."),
86
+ ("assert", "**** Assertion failed."),
86
87
  ("cmd", "**** Error in metacommand."),
87
88
  ("log", "**** Error in logging."),
88
89
  ("error", "**** General error."),