execsql2 2.12.2__tar.gz → 2.12.5__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 (300) hide show
  1. {execsql2-2.12.2 → execsql2-2.12.5}/CHANGELOG.md +23 -0
  2. {execsql2-2.12.2 → execsql2-2.12.5}/PKG-INFO +4 -1
  3. {execsql2-2.12.2 → execsql2-2.12.5}/docs/about/divergence.md +13 -6
  4. {execsql2-2.12.2 → execsql2-2.12.5}/docs/getting-started/installation.md +1 -0
  5. {execsql2-2.12.2 → execsql2-2.12.5}/docs/reference/metacommands.md +116 -0
  6. {execsql2-2.12.2 → execsql2-2.12.5}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +1 -1
  7. {execsql2-2.12.2 → execsql2-2.12.5}/pyproject.toml +4 -2
  8. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/base.py +7 -0
  9. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/delimited.py +44 -2
  10. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/__init__.py +9 -0
  11. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/dispatch.py +30 -0
  12. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/io_fileops.py +4 -0
  13. execsql2-2.12.5/src/execsql/metacommands/upsert.py +448 -0
  14. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/script/__init__.py +4 -0
  15. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/script/engine.py +55 -23
  16. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/script/variables.py +35 -2
  17. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_cli.py +6 -1
  18. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_delimited.py +12 -3
  19. execsql2-2.12.5/tests/metacommands/test_pg_upsert.py +997 -0
  20. {execsql2-2.12.2 → execsql2-2.12.5}/uv.lock +30 -18
  21. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/dba.md +0 -0
  22. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/herald.md +0 -0
  23. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/inspector.md +0 -0
  24. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/liaison.md +0 -0
  25. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/oracle.md +0 -0
  26. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/patcher.md +0 -0
  27. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/qa.md +0 -0
  28. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/agents/scribe.md +0 -0
  29. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/code-oracle.md +0 -0
  30. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/migrate.md +0 -0
  31. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/review-changes.md +0 -0
  32. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/test-module.md +0 -0
  33. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/update-changelog.md +0 -0
  34. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/commands/where-is.md +0 -0
  35. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/project_context.md +0 -0
  36. {execsql2-2.12.2 → execsql2-2.12.5}/.claude/state/status.md +0 -0
  37. {execsql2-2.12.2 → execsql2-2.12.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  38. {execsql2-2.12.2 → execsql2-2.12.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  39. {execsql2-2.12.2 → execsql2-2.12.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  40. {execsql2-2.12.2 → execsql2-2.12.5}/.github/workflows/ci-cd.yml +0 -0
  41. {execsql2-2.12.2 → execsql2-2.12.5}/.gitignore +0 -0
  42. {execsql2-2.12.2 → execsql2-2.12.5}/.pre-commit-config.yaml +0 -0
  43. {execsql2-2.12.2 → execsql2-2.12.5}/.pre-commit-hooks.yaml +0 -0
  44. {execsql2-2.12.2 → execsql2-2.12.5}/.python-version +0 -0
  45. {execsql2-2.12.2 → execsql2-2.12.5}/.readthedocs.yaml +0 -0
  46. {execsql2-2.12.2 → execsql2-2.12.5}/CLAUDE.md +0 -0
  47. {execsql2-2.12.2 → execsql2-2.12.5}/CONTRIBUTING.md +0 -0
  48. {execsql2-2.12.2 → execsql2-2.12.5}/LICENSE.txt +0 -0
  49. {execsql2-2.12.2 → execsql2-2.12.5}/NOTICE +0 -0
  50. {execsql2-2.12.2 → execsql2-2.12.5}/README.md +0 -0
  51. {execsql2-2.12.2 → execsql2-2.12.5}/SECURITY.md +0 -0
  52. {execsql2-2.12.2 → execsql2-2.12.5}/docs/about/contributors.md +0 -0
  53. {execsql2-2.12.2 → execsql2-2.12.5}/docs/about/copyright.md +0 -0
  54. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/cli.md +0 -0
  55. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/db.md +0 -0
  56. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/exporters.md +0 -0
  57. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/importers.md +0 -0
  58. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/index.md +0 -0
  59. {execsql2-2.12.2 → execsql2-2.12.5}/docs/api/metacommands.md +0 -0
  60. {execsql2-2.12.2 → execsql2-2.12.5}/docs/dev/adding_db_adapters.md +0 -0
  61. {execsql2-2.12.2 → execsql2-2.12.5}/docs/dev/adding_exporters.md +0 -0
  62. {execsql2-2.12.2 → execsql2-2.12.5}/docs/dev/adding_importers.md +0 -0
  63. {execsql2-2.12.2 → execsql2-2.12.5}/docs/dev/adding_metacommands.md +0 -0
  64. {execsql2-2.12.2 → execsql2-2.12.5}/docs/dev/architecture.md +0 -0
  65. {execsql2-2.12.2 → execsql2-2.12.5}/docs/getting-started/requirements.md +0 -0
  66. {execsql2-2.12.2 → execsql2-2.12.5}/docs/getting-started/syntax.md +0 -0
  67. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/debugging.md +0 -0
  68. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/documentation.md +0 -0
  69. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/encoding.md +0 -0
  70. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/examples.md +0 -0
  71. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/formatter.md +0 -0
  72. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/logging.md +0 -0
  73. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/sql_syntax.md +0 -0
  74. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/usage.md +0 -0
  75. {execsql2-2.12.2 → execsql2-2.12.5}/docs/guides/using_scripts.md +0 -0
  76. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/Compare_planets.png +0 -0
  77. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/actions.png +0 -0
  78. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/actions2.png +0 -0
  79. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/checkboxes.png +0 -0
  80. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/connect.b64 +0 -0
  81. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/connect.png +0 -0
  82. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/create_conf.png +0 -0
  83. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/data_error1_screenshot.jpg +0 -0
  84. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/entry_form.png +0 -0
  85. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/execsql_console.png +0 -0
  86. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/execsql_logo_01.png +0 -0
  87. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/fatals.png +0 -0
  88. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/logo_small.png +0 -0
  89. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/pause_terminal.png +0 -0
  90. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/pause_terminal_sm.b64 +0 -0
  91. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/pause_terminal_sm.png +0 -0
  92. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/prompt_compare.png +0 -0
  93. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/set_build_commands.jpg +0 -0
  94. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/unit_conversions.b64 +0 -0
  95. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/unit_conversions_029.png +0 -0
  96. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/unmatched.png +0 -0
  97. {execsql2-2.12.2 → execsql2-2.12.5}/docs/images/vim_execsql_highlight.png +0 -0
  98. {execsql2-2.12.2 → execsql2-2.12.5}/docs/index.md +0 -0
  99. {execsql2-2.12.2 → execsql2-2.12.5}/docs/reference/configuration.md +0 -0
  100. {execsql2-2.12.2 → execsql2-2.12.5}/docs/reference/security.md +0 -0
  101. {execsql2-2.12.2 → execsql2-2.12.5}/docs/reference/substitution_vars.md +0 -0
  102. {execsql2-2.12.2 → execsql2-2.12.5}/extras/vscode-execsql/README.md +0 -0
  103. {execsql2-2.12.2 → execsql2-2.12.5}/extras/vscode-execsql/package.json +0 -0
  104. {execsql2-2.12.2 → execsql2-2.12.5}/justfile +0 -0
  105. {execsql2-2.12.2 → execsql2-2.12.5}/scripts/generate_vscode_grammar.py +0 -0
  106. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/__init__.py +0 -0
  107. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/__main__.py +0 -0
  108. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/cli/__init__.py +0 -0
  109. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/cli/dsn.py +0 -0
  110. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/cli/help.py +0 -0
  111. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/cli/lint.py +0 -0
  112. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/cli/run.py +0 -0
  113. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/config.py +0 -0
  114. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/constants.py +0 -0
  115. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/__init__.py +0 -0
  116. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/access.py +0 -0
  117. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/dsn.py +0 -0
  118. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/duckdb.py +0 -0
  119. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/factory.py +0 -0
  120. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/firebird.py +0 -0
  121. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/mysql.py +0 -0
  122. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/oracle.py +0 -0
  123. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/postgres.py +0 -0
  124. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/sqlite.py +0 -0
  125. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/db/sqlserver.py +0 -0
  126. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/debug/__init__.py +0 -0
  127. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/debug/repl.py +0 -0
  128. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exceptions.py +0 -0
  129. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/__init__.py +0 -0
  130. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/base.py +0 -0
  131. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/duckdb.py +0 -0
  132. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/feather.py +0 -0
  133. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/html.py +0 -0
  134. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/json.py +0 -0
  135. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/latex.py +0 -0
  136. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/markdown.py +0 -0
  137. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/ods.py +0 -0
  138. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/parquet.py +0 -0
  139. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/pretty.py +0 -0
  140. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/protocol.py +0 -0
  141. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/raw.py +0 -0
  142. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/sqlite.py +0 -0
  143. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/templates.py +0 -0
  144. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/values.py +0 -0
  145. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/xls.py +0 -0
  146. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/xlsx.py +0 -0
  147. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/xml.py +0 -0
  148. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/yaml.py +0 -0
  149. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/exporters/zip.py +0 -0
  150. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/format.py +0 -0
  151. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/gui/__init__.py +0 -0
  152. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/gui/base.py +0 -0
  153. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/gui/console.py +0 -0
  154. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/gui/desktop.py +0 -0
  155. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/gui/tui.py +0 -0
  156. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/__init__.py +0 -0
  157. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/base.py +0 -0
  158. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/csv.py +0 -0
  159. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/feather.py +0 -0
  160. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/ods.py +0 -0
  161. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/importers/xls.py +0 -0
  162. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/conditions.py +0 -0
  163. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/connect.py +0 -0
  164. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/control.py +0 -0
  165. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/data.py +0 -0
  166. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/debug.py +0 -0
  167. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/io.py +0 -0
  168. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/io_export.py +0 -0
  169. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/io_import.py +0 -0
  170. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/io_write.py +0 -0
  171. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/prompt.py +0 -0
  172. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/script_ext.py +0 -0
  173. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/metacommands/system.py +0 -0
  174. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/models.py +0 -0
  175. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/parser.py +0 -0
  176. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/py.typed +0 -0
  177. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/script/control.py +0 -0
  178. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/state.py +0 -0
  179. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/types.py +0 -0
  180. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/__init__.py +0 -0
  181. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/auth.py +0 -0
  182. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/crypto.py +0 -0
  183. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/datetime.py +0 -0
  184. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/errors.py +0 -0
  185. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/fileio.py +0 -0
  186. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/gui.py +0 -0
  187. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/mail.py +0 -0
  188. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/numeric.py +0 -0
  189. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/regex.py +0 -0
  190. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/strings.py +0 -0
  191. {execsql2-2.12.2 → execsql2-2.12.5}/src/execsql/utils/timer.py +0 -0
  192. {execsql2-2.12.2 → execsql2-2.12.5}/templates/README.md +0 -0
  193. {execsql2-2.12.2 → execsql2-2.12.5}/templates/config_settings.sqlite +0 -0
  194. {execsql2-2.12.2 → execsql2-2.12.5}/templates/example_config_prompt.sql +0 -0
  195. {execsql2-2.12.2 → execsql2-2.12.5}/templates/execsql.conf +0 -0
  196. {execsql2-2.12.2 → execsql2-2.12.5}/templates/make_config_db.sql +0 -0
  197. {execsql2-2.12.2 → execsql2-2.12.5}/templates/md_compare.sql +0 -0
  198. {execsql2-2.12.2 → execsql2-2.12.5}/templates/md_glossary.sql +0 -0
  199. {execsql2-2.12.2 → execsql2-2.12.5}/templates/md_upsert.sql +0 -0
  200. {execsql2-2.12.2 → execsql2-2.12.5}/templates/pg_compare.sql +0 -0
  201. {execsql2-2.12.2 → execsql2-2.12.5}/templates/pg_glossary.sql +0 -0
  202. {execsql2-2.12.2 → execsql2-2.12.5}/templates/pg_upsert.sql +0 -0
  203. {execsql2-2.12.2 → execsql2-2.12.5}/templates/script_template.sql +0 -0
  204. {execsql2-2.12.2 → execsql2-2.12.5}/templates/ss_compare.sql +0 -0
  205. {execsql2-2.12.2 → execsql2-2.12.5}/templates/ss_glossary.sql +0 -0
  206. {execsql2-2.12.2 → execsql2-2.12.5}/templates/ss_upsert.sql +0 -0
  207. {execsql2-2.12.2 → execsql2-2.12.5}/tests/__init__.py +0 -0
  208. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/__init__.py +0 -0
  209. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_cli_e2e.py +0 -0
  210. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_cli_run.py +0 -0
  211. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_lint.py +0 -0
  212. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_ping.py +0 -0
  213. {execsql2-2.12.2 → execsql2-2.12.5}/tests/cli/test_profile.py +0 -0
  214. {execsql2-2.12.2 → execsql2-2.12.5}/tests/conftest.py +0 -0
  215. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/__init__.py +0 -0
  216. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_base.py +0 -0
  217. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_duckdb.py +0 -0
  218. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_factory.py +0 -0
  219. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_postgres.py +0 -0
  220. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_sqlite.py +0 -0
  221. {execsql2-2.12.2 → execsql2-2.12.5}/tests/db/test_sqlite_extra.py +0 -0
  222. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/__init__.py +0 -0
  223. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_base.py +0 -0
  224. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_db.py +0 -0
  225. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_duckdb_exporter.py +0 -0
  226. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_exporters.py +0 -0
  227. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_feather.py +0 -0
  228. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_html_latex.py +0 -0
  229. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_json.py +0 -0
  230. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_markdown.py +0 -0
  231. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_ods.py +0 -0
  232. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_parquet.py +0 -0
  233. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_sqlite_exporter.py +0 -0
  234. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_templates.py +0 -0
  235. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_xls_xlsx.py +0 -0
  236. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_xlsx.py +0 -0
  237. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_xml.py +0 -0
  238. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_yaml.py +0 -0
  239. {execsql2-2.12.2 → execsql2-2.12.5}/tests/exporters/test_zip.py +0 -0
  240. {execsql2-2.12.2 → execsql2-2.12.5}/tests/gui/__init__.py +0 -0
  241. {execsql2-2.12.2 → execsql2-2.12.5}/tests/gui/test_backends.py +0 -0
  242. {execsql2-2.12.2 → execsql2-2.12.5}/tests/importers/__init__.py +0 -0
  243. {execsql2-2.12.2 → execsql2-2.12.5}/tests/importers/test_csv_importer.py +0 -0
  244. {execsql2-2.12.2 → execsql2-2.12.5}/tests/importers/test_feather_importer.py +0 -0
  245. {execsql2-2.12.2 → execsql2-2.12.5}/tests/importers/test_ods_importer.py +0 -0
  246. {execsql2-2.12.2 → execsql2-2.12.5}/tests/importers/test_xls_importer.py +0 -0
  247. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/__init__.py +0 -0
  248. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/conftest.py +0 -0
  249. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/test_dsn.py +0 -0
  250. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/test_duckdb.py +0 -0
  251. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/test_mysql.py +0 -0
  252. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/test_postgres.py +0 -0
  253. {execsql2-2.12.2 → execsql2-2.12.5}/tests/integration/test_sqlite.py +0 -0
  254. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/__init__.py +0 -0
  255. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_assert.py +0 -0
  256. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_breakpoint.py +0 -0
  257. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_connect.py +0 -0
  258. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_io_export.py +0 -0
  259. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_io_import.py +0 -0
  260. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands.py +0 -0
  261. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_connect.py +0 -0
  262. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_data.py +0 -0
  263. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_extended.py +0 -0
  264. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  265. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_io.py +0 -0
  266. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  267. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  268. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_system.py +0 -0
  269. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  270. {execsql2-2.12.2 → execsql2-2.12.5}/tests/metacommands/test_row_count.py +0 -0
  271. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_config.py +0 -0
  272. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_config_data.py +0 -0
  273. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_constants.py +0 -0
  274. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_engine.py +0 -0
  275. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_error_messages.py +0 -0
  276. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_exceptions.py +0 -0
  277. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_format.py +0 -0
  278. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_mail.py +0 -0
  279. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_models.py +0 -0
  280. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_package.py +0 -0
  281. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_parser.py +0 -0
  282. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_registry.py +0 -0
  283. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_script.py +0 -0
  284. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_state.py +0 -0
  285. {execsql2-2.12.2 → execsql2-2.12.5}/tests/test_types.py +0 -0
  286. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/__init__.py +0 -0
  287. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_auth.py +0 -0
  288. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_auth_extra.py +0 -0
  289. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_crypto.py +0 -0
  290. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_datetime.py +0 -0
  291. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_errors.py +0 -0
  292. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_errors_extra.py +0 -0
  293. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_fileio.py +0 -0
  294. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_fileio_extra.py +0 -0
  295. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_numeric.py +0 -0
  296. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_regex.py +0 -0
  297. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_strings.py +0 -0
  298. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_timer.py +0 -0
  299. {execsql2-2.12.2 → execsql2-2.12.5}/tests/utils/test_timer_extra.py +0 -0
  300. {execsql2-2.12.2 → execsql2-2.12.5}/zensical.toml +0 -0
@@ -13,6 +13,29 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.12.5] - 2026-04-03
17
+
18
+ ______________________________________________________________________
19
+
20
+ ## [2.12.4] - 2026-04-03
21
+
22
+ ### Added
23
+
24
+ - New `PG_UPSERT` metacommand for QA-checked, FK-dependency-ordered upserts from a staging schema to a base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency (`pip install execsql2[upsert]`). Three modes: full pipeline (`PG_UPSERT FROM ... TO ... TABLES ...`), QA-only (`PG_UPSERT QA ...`), and schema check (`PG_UPSERT CHECK ...`). Supports `METHOD`, `COMMIT`, `INTERACTIVE`, `COMPACT`, `EXCLUDE`, `EXCLUDE_NULL`, and `LOGFILE` keywords. Sets 12 `$PG_UPSERT_*` substitution variables after execution.
25
+
26
+ ______________________________________________________________________
27
+
28
+ ## [2.12.3] - 2026-04-02
29
+
30
+ ### Changed
31
+
32
+ - 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.
33
+ - Performance: `$RANDOM` and `$UUID` are now lazy — computed only when actually referenced in a statement, not generated unconditionally for every statement.
34
+ - Performance: `LineDelimiter.delimited()` caches `quote_all_text` at construction time instead of reading `_state.conf` via module proxy on every row during export.
35
+ - 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).
36
+
37
+ ______________________________________________________________________
38
+
16
39
  ## [2.12.2] - 2026-04-02
17
40
 
18
41
  ### Added
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.12.2
3
+ Version: 2.12.5
4
4
  Summary: Runs a SQL script against a PostgreSQL, SQLite, MariaDB/MySQL, DuckDB, Firebird, MS-Access, MS-SQL-Server, or Oracle database, or an ODBC DSN. Provides metacommands to import and export data, copy data between databases, conditionally execute SQL and metacommands, and dynamically alter SQL and metacommands with substitution variables.
5
5
  Project-URL: Homepage, https://execsql2.readthedocs.io
6
6
  Project-URL: Repository, https://github.com/geocoug/execsql
@@ -51,6 +51,7 @@ Requires-Dist: keyring; extra == 'all'
51
51
  Requires-Dist: odfpy; extra == 'all'
52
52
  Requires-Dist: openpyxl; extra == 'all'
53
53
  Requires-Dist: oracledb; extra == 'all'
54
+ Requires-Dist: pg-upsert>=1.17.0; extra == 'all'
54
55
  Requires-Dist: polars; extra == 'all'
55
56
  Requires-Dist: psycopg2-binary; extra == 'all'
56
57
  Requires-Dist: pymysql; extra == 'all'
@@ -107,6 +108,8 @@ Provides-Extra: oracle
107
108
  Requires-Dist: oracledb; extra == 'oracle'
108
109
  Provides-Extra: postgres
109
110
  Requires-Dist: psycopg2-binary; extra == 'postgres'
111
+ Provides-Extra: upsert
112
+ Requires-Dist: pg-upsert>=1.17.0; extra == 'upsert'
110
113
  Description-Content-Type: text/markdown
111
114
 
112
115
  > [!NOTE]
@@ -41,12 +41,13 @@ ______________________________________________________________________
41
41
 
42
42
  ### Metacommands
43
43
 
44
- | Metacommand | Description |
45
- | ---------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
- | `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
47
- | `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details. |
48
- | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
49
- | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
44
+ | Metacommand | Description |
45
+ | ---------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
46
+ | `ASSERT` | Evaluate a condition and raise an error (halting the script) if it is false. Supports all IF conditions. Optional quoted failure message. Skipped in false IF blocks. |
47
+ | `BREAKPOINT` | Pause script execution and drop into an interactive debug REPL. See [Debugging](#debugging) below for full details. |
48
+ | `CONFIG SHOW_PROGRESS` | Enable the Rich progress bar for IMPORT operations at runtime. |
49
+ | `CONFIG LOG_SQL` | Enable SQL query audit logging — writes executed SQL to the log file. |
50
+ | `PG_UPSERT` | QA-checked, FK-dependency-ordered upserts from staging to base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency. Three modes: full pipeline, QA-only, and schema check. |
50
51
 
51
52
  ### Conditional Tests
52
53
 
@@ -163,6 +164,12 @@ All 33 mutable runtime globals in `state.py` have been consolidated into a `Runt
163
164
 
164
165
  - **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
166
  - **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.
167
+ - **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.
168
+ - **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.
169
+
170
+ ### CSV Import
171
+
172
+ - **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
173
 
167
174
  ### Database Adapters
168
175
 
@@ -25,6 +25,7 @@ pip install "execsql2[duckdb]" # DuckDB
25
25
  pip install "execsql2[mssql]" # MS SQL Server / ODBC
26
26
  pip install "execsql2[formats]" # ODS, Excel, Jinja2, Feather, Parquet, HDF5
27
27
  pip install "execsql2[auth]" # OS keyring integration
28
+ pip install "execsql2[upsert]" # PG_UPSERT metacommand (pg-upsert)
28
29
  pip install "execsql2[all-db]" # All database drivers
29
30
  pip install "execsql2[all]" # Everything
30
31
  ```
@@ -2066,6 +2066,122 @@ If the "HALT" action is taken, either as a result of user input or as a result o
2066
2066
  Double quotes (as shown above), apostrophes, or square brackets can be used to delimit the text.
2067
2067
 
2068
2068
 
2069
+ ## PG_UPSERT { #pg_upsert }
2070
+
2071
+ ```
2072
+ PG_UPSERT FROM <staging_schema> TO <base_schema> TABLES <table1>, <table2> [options]
2073
+ PG_UPSERT QA FROM <staging_schema> TO <base_schema> TABLES <table1>, <table2> [options]
2074
+ PG_UPSERT CHECK FROM <staging_schema> TO <base_schema> TABLES <table1>, <table2>
2075
+ ```
2076
+
2077
+ Performs QA-checked, FK-dependency-ordered upserts from a staging schema to a base schema on PostgreSQL. Integrates [pg-upsert](https://pg-upsert.readthedocs.io/) as an optional dependency.
2078
+
2079
+ **Requires:** `pip install execsql2[upsert]`
2080
+
2081
+ **Requires:** A PostgreSQL connection. Raises an error if the current DBMS is not PostgreSQL.
2082
+
2083
+ ### Modes
2084
+
2085
+ - **Full pipeline** (`PG_UPSERT FROM ... TO ... TABLES ...`): Runs all QA checks, then upserts data (update + insert by default), with optional commit.
2086
+ - **QA only** (`PG_UPSERT QA FROM ... TO ... TABLES ...`): Runs all QA checks without upserting. Never commits.
2087
+ - **Schema check** (`PG_UPSERT CHECK FROM ... TO ... TABLES ...`): Checks column existence and type compatibility only. Never commits.
2088
+
2089
+ ### Optional keywords
2090
+
2091
+ Keywords can appear in any order after the table list.
2092
+
2093
+ | Keyword | Description |
2094
+ |---------|-------------|
2095
+ | `METHOD upsert\|update\|insert` | Upsert strategy. `upsert` (default) updates existing + inserts new rows. `update` only updates. `insert` only inserts. Full mode only. |
2096
+ | `COMMIT` | Commit changes if QA passes. Without it, changes are rolled back (dry-run). Full mode only. |
2097
+ | `EXCLUDE col1, col2` | Columns to skip during upsert. |
2098
+ | `EXCLUDE_NULL col1, col2` | Columns to skip in null QA checks. |
2099
+ | `INTERACTIVE` | Enable pg-upsert's interactive UI dialogs (tkinter or textual) for reviewing QA failures. Without it, runs non-interactively. |
2100
+ | `COMPACT` | Use compact grid format for QA summary instead of detailed per-table panels. |
2101
+ | `LOGFILE <path>` | Append pg-upsert's plain-text log output to the given file. Supports quoted paths for spaces: `LOGFILE "path/to/log.txt"`. |
2102
+
2103
+ ### Substitution variables
2104
+
2105
+ Set after every `PG_UPSERT` execution:
2106
+
2107
+ | Variable | Type | Description |
2108
+ |----------|------|-------------|
2109
+ | `$PG_UPSERT_QA_PASSED` | TRUE/FALSE | Did all QA checks pass? |
2110
+ | `$PG_UPSERT_ROWS_UPDATED` | integer | Total rows updated across all tables |
2111
+ | `$PG_UPSERT_ROWS_INSERTED` | integer | Total rows inserted across all tables |
2112
+ | `$PG_UPSERT_COMMITTED` | TRUE/FALSE | Were changes committed? |
2113
+ | `$PG_UPSERT_STAGING_SCHEMA` | string | Staging schema name used |
2114
+ | `$PG_UPSERT_BASE_SCHEMA` | string | Base schema name used |
2115
+ | `$PG_UPSERT_TABLES` | string | Comma-separated list of table names processed |
2116
+ | `$PG_UPSERT_METHOD` | string | Upsert method used |
2117
+ | `$PG_UPSERT_DURATION` | float | Elapsed time in seconds |
2118
+ | `$PG_UPSERT_STARTED_AT` | string | ISO 8601 start timestamp |
2119
+ | `$PG_UPSERT_FINISHED_AT` | string | ISO 8601 end timestamp |
2120
+ | `$PG_UPSERT_RESULT_JSON` | JSON string | Full result for detailed inspection |
2121
+
2122
+ !!! note "Using `$PG_UPSERT_RESULT_JSON` with WRITE"
2123
+ The JSON value is stored as compact single-line JSON. Because it contains double quotes (`"`), square brackets (`[]`), and apostrophes may appear in data, use tilde or backtick delimiters with WRITE:
2124
+
2125
+ ```sql
2126
+ -- !x! WRITE ~!!$PG_UPSERT_RESULT_JSON!!~
2127
+ -- !x! WRITE `!!$PG_UPSERT_RESULT_JSON!!`
2128
+ ```
2129
+
2130
+ ### Temporary objects
2131
+
2132
+ pg-upsert creates temporary tables and views (all prefixed `ups_`) that persist after the metacommand completes. Users can query these for debugging and inspection — for example, `SELECT * FROM ups_control` shows per-table QA results and row counts.
2133
+
2134
+ For the full list of temporary objects and their schemas, see the [pg-upsert Temporary Objects Reference](https://pg-upsert.readthedocs.io/en/latest/architecture/#temporary-objects-reference).
2135
+
2136
+ ### Examples
2137
+
2138
+ ```sql
2139
+ -- Full pipeline: QA + upsert + commit
2140
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors COMMIT
2141
+
2142
+ -- Dry run (no commit) with update-only method
2143
+ -- !x! PG_UPSERT FROM staging TO public TABLES books METHOD update
2144
+
2145
+ -- QA only with interactive review
2146
+ -- !x! PG_UPSERT QA FROM staging TO public TABLES books, authors INTERACTIVE
2147
+
2148
+ -- Schema check only
2149
+ -- !x! PG_UPSERT CHECK FROM staging TO public TABLES books, authors
2150
+
2151
+ -- Log pg-upsert output to a file
2152
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors LOGFILE "upsert.log" COMMIT
2153
+
2154
+ -- Use substitution variables after upsert
2155
+ -- !x! PG_UPSERT FROM staging TO public TABLES books COMMIT
2156
+ -- !x! WRITE "Updated !!$PG_UPSERT_ROWS_UPDATED!! rows, inserted !!$PG_UPSERT_ROWS_INSERTED!! in !!$PG_UPSERT_DURATION!! seconds"
2157
+
2158
+ -- Inspect pg-upsert's control table after a run
2159
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors, genres COMMIT
2160
+ -- !x! EXPORT ups_control TO stdout AS txt
2161
+
2162
+ -- Export QA results to a CSV file for review
2163
+ -- !x! PG_UPSERT QA FROM staging TO public TABLES books, authors
2164
+ -- !x! EXPORT ups_control TO "qa_results.csv" AS csv
2165
+
2166
+ -- Check for type mismatches between schemas and display them
2167
+ -- !x! PG_UPSERT CHECK FROM staging TO public TABLES books, authors
2168
+ -- !x! EXPORT ups_type_mismatches TO stdout AS txt
2169
+
2170
+ -- Conditional logic based on QA results
2171
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors
2172
+ -- !x! IF !!$PG_UPSERT_QA_PASSED!! = TRUE
2173
+ -- !x! WRITE "All QA checks passed — proceeding with commit"
2174
+ -- !x! ELSE
2175
+ -- !x! WRITE "QA failed — inspect ups_control for details"
2176
+ -- !x! EXPORT ups_control TO stdout AS txt
2177
+ -- !x! ENDIF
2178
+
2179
+ -- Write the full JSON result using tilde delimiters (JSON contains " and [])
2180
+ -- !x! PG_UPSERT FROM staging TO public TABLES books COMMIT
2181
+ -- !x! WRITE ~!!$PG_UPSERT_RESULT_JSON!!~
2182
+ ```
2183
+
2184
+
2069
2185
  ## PG_VACUUM
2070
2186
 
2071
2187
  ```
@@ -93,7 +93,7 @@
93
93
  },
94
94
  "action-keywords": {
95
95
  "comment": "sub, write, execute script, export, etc.",
96
- "match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|breakpoint|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
96
+ "match": "(?i)\\b(reset\\s+dialog_canceled|write\\s+create_table|export_metadata|pg_upsert\\s+check|sub_querystring|execute\\s+script|append\\s+script|extend\\s+script|reset\\s+counter|export\\s+query|pg_upsert\\s+qa|sub_tempfile|write\\s+script|import_file|set\\s+counter|sub_decrypt|sub_encrypt|autocommit|breakpoint|copy\\s+query|disconnect|select_sub|sub_append|system_cmd|pg_upsert|pg_vacuum|sub_empty|sub_local|connect|include|max_int|rm_file|subdata|sub_add|sub_ini|assert|export|import|rm_sub|debug|email|serve|write|copy|log|run|sub|use|zip|cd)\\b",
97
97
  "name": "keyword.other.execsql"
98
98
  },
99
99
  "config-event-keywords": {
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.12.2"
7
+ version = "2.12.5"
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" }
@@ -58,6 +58,7 @@ odbc = ["pyodbc"]
58
58
  # Feature bundles
59
59
  formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
60
60
  auth = ["keyring"]
61
+ upsert = ["pg-upsert>=1.17.0"]
61
62
  # Convenience groups
62
63
  all-db = [
63
64
  "psycopg2-binary",
@@ -71,6 +72,7 @@ all = [
71
72
  "execsql2[all-db]",
72
73
  "execsql2[formats]",
73
74
  "execsql2[auth]",
75
+ "execsql2[upsert]",
74
76
  ]
75
77
  dev = [
76
78
  "build>=1.2.2.post1",
@@ -159,7 +161,7 @@ skip-magic-trailing-comma = false
159
161
  line-ending = "auto"
160
162
 
161
163
  [tool.bumpversion]
162
- current_version = "2.12.2"
164
+ current_version = "2.12.5"
163
165
  commit = true
164
166
  commit_args = "--no-verify"
165
167
  tag = true
@@ -686,6 +686,13 @@ class DatabasePool:
686
686
  )
687
687
  self.pool[db_alias].close()
688
688
  self.pool[db_alias] = db_obj
689
+ # Refresh static system vars so $DB_NAME, $DB_USER, etc. reflect the new connection.
690
+ try:
691
+ from execsql.script.engine import set_static_system_vars
692
+
693
+ set_static_system_vars()
694
+ except Exception:
695
+ pass # Engine not yet initialized (early startup).
689
696
 
690
697
  def aliases(self) -> list[str]:
691
698
  """Return a list of all currently registered database aliases."""
@@ -40,6 +40,7 @@ class LineDelimiter:
40
40
  self.delimiter = delim
41
41
  self.joinchar = delim if delim else ""
42
42
  self.quotechar = quote
43
+ self.quote_all_text = _state.conf.quote_all_text if _state.conf else False
43
44
  if quote:
44
45
  if escchar:
45
46
  self.quotedquote = escchar + quote
@@ -50,13 +51,12 @@ class LineDelimiter:
50
51
 
51
52
  def delimited(self, datarow: Any, add_newline: bool = True) -> str:
52
53
  """Format a sequence of values as a single delimited text line."""
53
- conf = _state.conf
54
54
  if self.quotechar:
55
55
  d_row = []
56
56
  for e in datarow:
57
57
  if isinstance(e, str):
58
58
  if (
59
- conf.quote_all_text
59
+ self.quote_all_text
60
60
  or (self.quotechar in e)
61
61
  or (self.delimiter is not None and self.delimiter in e)
62
62
  or ("\n" in e)
@@ -609,10 +609,52 @@ class CsvFile(EncodedFile):
609
609
  raise ErrInfo("error", other_msg=", ".join(self.parse_errors))
610
610
  return elements
611
611
 
612
+ def _can_use_fast_csv_reader(self) -> bool:
613
+ """Return True if the detected format is compatible with Python's csv module."""
614
+ # The csv module handles comma/tab delimiters with doubled-quote escaping.
615
+ # It cannot handle: space-delimiter collapsing, escape chars, or no delimiter.
616
+ if self.delimiter is None or self.delimiter == " ":
617
+ return False
618
+ return self.escapechar is None
619
+
612
620
  def reader(self) -> Any:
613
621
  """Yield parsed rows from the file as lists of field values."""
614
622
  conf = _state.conf
615
623
  self.evaluate_line_format()
624
+ if self._can_use_fast_csv_reader():
625
+ yield from self._fast_reader(conf)
626
+ else:
627
+ yield from self._slow_reader(conf)
628
+
629
+ def _fast_reader(self, conf: Any) -> Any:
630
+ """Read using Python's csv module (fast path for standard delimited formats)."""
631
+ import csv
632
+
633
+ f = self.openclean("rt")
634
+ try:
635
+ csv_reader = csv.reader(
636
+ f,
637
+ delimiter=self.delimiter,
638
+ quotechar=self.quotechar,
639
+ doublequote=True,
640
+ strict=False,
641
+ )
642
+ for elements in csv_reader:
643
+ if len(elements) == 0:
644
+ break
645
+ # Normalize empty strings to None for parity with the slow reader.
646
+ elements = [e if e != "" else None for e in elements]
647
+ if conf.del_empty_cols and len(self.blank_cols) > 0:
648
+ blanks = copy.copy(self.blank_cols)
649
+ while len(blanks) > 0:
650
+ b = blanks.pop()
651
+ del elements[b]
652
+ yield elements
653
+ finally:
654
+ f.close()
655
+
656
+ def _slow_reader(self, conf: Any) -> Any:
657
+ """Read using the character-at-a-time state machine (fallback for non-standard formats)."""
616
658
  f = self.openclean("rt")
617
659
  line_no = 0
618
660
  try:
@@ -168,6 +168,11 @@ from execsql.metacommands.script_ext import (
168
168
  x_extendscript_sql,
169
169
  x_executescript,
170
170
  )
171
+ from execsql.metacommands.upsert import (
172
+ x_pg_upsert,
173
+ x_pg_upsert_check,
174
+ x_pg_upsert_qa,
175
+ )
171
176
  from execsql.metacommands.system import (
172
177
  x_system_cmd,
173
178
  x_email,
@@ -401,6 +406,10 @@ __all__ = [
401
406
  "x_write_warnings",
402
407
  "x_gui_level",
403
408
  "x_execute",
409
+ # upsert handlers
410
+ "x_pg_upsert",
411
+ "x_pg_upsert_check",
412
+ "x_pg_upsert_qa",
404
413
  # regex helpers
405
414
  "ins_rxs",
406
415
  "ins_quoted_rx",
@@ -168,6 +168,11 @@ from execsql.metacommands.script_ext import (
168
168
  x_extendscript_metacommand,
169
169
  x_extendscript_sql,
170
170
  )
171
+ from execsql.metacommands.upsert import (
172
+ x_pg_upsert,
173
+ x_pg_upsert_check,
174
+ x_pg_upsert_qa,
175
+ )
171
176
  from execsql.metacommands.system import (
172
177
  x_cancel_halt,
173
178
  x_cancel_halt_email,
@@ -2142,6 +2147,31 @@ def build_dispatch_table() -> MetaCommandList:
2142
2147
  x_write,
2143
2148
  )
2144
2149
 
2150
+ # ------------------------------------------------------------------
2151
+ # PG_UPSERT — pg-upsert integration (optional dependency)
2152
+ # ------------------------------------------------------------------
2153
+ # Order matters: CHECK and QA patterns must precede the general pattern
2154
+ # so that "PG_UPSERT CHECK ..." and "PG_UPSERT QA ..." are matched
2155
+ # before "PG_UPSERT FROM ...".
2156
+ mcl.add(
2157
+ r"^\s*PG_UPSERT\s+CHECK\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$",
2158
+ x_pg_upsert_check,
2159
+ description="PG_UPSERT CHECK",
2160
+ category="action",
2161
+ )
2162
+ mcl.add(
2163
+ r"^\s*PG_UPSERT\s+QA\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$",
2164
+ x_pg_upsert_qa,
2165
+ description="PG_UPSERT QA",
2166
+ category="action",
2167
+ )
2168
+ mcl.add(
2169
+ r"^\s*PG_UPSERT\s+FROM\s+(?P<staging_schema>\S+)\s+TO\s+(?P<base_schema>\S+)\s+TABLES\s+(?P<tail>.+)$",
2170
+ x_pg_upsert,
2171
+ description="PG_UPSERT",
2172
+ category="action",
2173
+ )
2174
+
2145
2175
  # ------------------------------------------------------------------
2146
2176
  # SUB (top-level variable assignment — kept near end so more specific
2147
2177
  # SUB_* patterns above take precedence)
@@ -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