execsql2 2.14.1__tar.gz → 2.15.0__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 (312) hide show
  1. {execsql2-2.14.1 → execsql2-2.15.0}/CHANGELOG.md +20 -0
  2. {execsql2-2.14.1 → execsql2-2.15.0}/PKG-INFO +3 -3
  3. {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/divergence.md +8 -8
  4. {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/metacommands.md +11 -0
  5. {execsql2-2.14.1 → execsql2-2.15.0}/pyproject.toml +3 -3
  6. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/upsert.py +125 -17
  7. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/gui.py +2 -2
  8. {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/test_backends.py +28 -0
  9. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_pg_upsert.py +486 -0
  10. {execsql2-2.14.1 → execsql2-2.15.0}/uv.lock +5 -5
  11. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/dba.md +0 -0
  12. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/herald.md +0 -0
  13. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/inspector.md +0 -0
  14. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/liaison.md +0 -0
  15. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/oracle.md +0 -0
  16. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/patcher.md +0 -0
  17. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/qa.md +0 -0
  18. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/agents/scribe.md +0 -0
  19. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/code-oracle.md +0 -0
  20. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/migrate.md +0 -0
  21. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/review-changes.md +0 -0
  22. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/test-module.md +0 -0
  23. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/update-changelog.md +0 -0
  24. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/commands/where-is.md +0 -0
  25. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/project_context.md +0 -0
  26. {execsql2-2.14.1 → execsql2-2.15.0}/.claude/state/status.md +0 -0
  27. {execsql2-2.14.1 → execsql2-2.15.0}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  28. {execsql2-2.14.1 → execsql2-2.15.0}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  29. {execsql2-2.14.1 → execsql2-2.15.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  30. {execsql2-2.14.1 → execsql2-2.15.0}/.github/workflows/ci-cd.yml +0 -0
  31. {execsql2-2.14.1 → execsql2-2.15.0}/.gitignore +0 -0
  32. {execsql2-2.14.1 → execsql2-2.15.0}/.pre-commit-config.yaml +0 -0
  33. {execsql2-2.14.1 → execsql2-2.15.0}/.pre-commit-hooks.yaml +0 -0
  34. {execsql2-2.14.1 → execsql2-2.15.0}/.python-version +0 -0
  35. {execsql2-2.14.1 → execsql2-2.15.0}/.readthedocs.yaml +0 -0
  36. {execsql2-2.14.1 → execsql2-2.15.0}/CLAUDE.md +0 -0
  37. {execsql2-2.14.1 → execsql2-2.15.0}/CONTRIBUTING.md +0 -0
  38. {execsql2-2.14.1 → execsql2-2.15.0}/LICENSE.txt +0 -0
  39. {execsql2-2.14.1 → execsql2-2.15.0}/NOTICE +0 -0
  40. {execsql2-2.14.1 → execsql2-2.15.0}/README.md +0 -0
  41. {execsql2-2.14.1 → execsql2-2.15.0}/SECURITY.md +0 -0
  42. {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/contributors.md +0 -0
  43. {execsql2-2.14.1 → execsql2-2.15.0}/docs/about/copyright.md +0 -0
  44. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/cli.md +0 -0
  45. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/db.md +0 -0
  46. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/exporters.md +0 -0
  47. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/importers.md +0 -0
  48. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/index.md +0 -0
  49. {execsql2-2.14.1 → execsql2-2.15.0}/docs/api/metacommands.md +0 -0
  50. {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_db_adapters.md +0 -0
  51. {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_exporters.md +0 -0
  52. {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_importers.md +0 -0
  53. {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/adding_metacommands.md +0 -0
  54. {execsql2-2.14.1 → execsql2-2.15.0}/docs/dev/architecture.md +0 -0
  55. {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/installation.md +0 -0
  56. {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/requirements.md +0 -0
  57. {execsql2-2.14.1 → execsql2-2.15.0}/docs/getting-started/syntax.md +0 -0
  58. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/debugging.md +0 -0
  59. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/documentation.md +0 -0
  60. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/encoding.md +0 -0
  61. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/examples.md +0 -0
  62. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/formatter.md +0 -0
  63. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/logging.md +0 -0
  64. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/sql_syntax.md +0 -0
  65. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/usage.md +0 -0
  66. {execsql2-2.14.1 → execsql2-2.15.0}/docs/guides/using_scripts.md +0 -0
  67. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/Compare_planets.png +0 -0
  68. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/actions.png +0 -0
  69. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/actions2.png +0 -0
  70. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/checkboxes.png +0 -0
  71. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/connect.b64 +0 -0
  72. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/connect.png +0 -0
  73. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/create_conf.png +0 -0
  74. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/data_error1_screenshot.jpg +0 -0
  75. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/entry_form.png +0 -0
  76. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/execsql_console.png +0 -0
  77. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/execsql_logo_01.png +0 -0
  78. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/fatals.png +0 -0
  79. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/logo_small.png +0 -0
  80. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal.png +0 -0
  81. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal_sm.b64 +0 -0
  82. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/pause_terminal_sm.png +0 -0
  83. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/prompt_compare.png +0 -0
  84. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/set_build_commands.jpg +0 -0
  85. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unit_conversions.b64 +0 -0
  86. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unit_conversions_029.png +0 -0
  87. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/unmatched.png +0 -0
  88. {execsql2-2.14.1 → execsql2-2.15.0}/docs/images/vim_execsql_highlight.png +0 -0
  89. {execsql2-2.14.1 → execsql2-2.15.0}/docs/index.md +0 -0
  90. {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/configuration.md +0 -0
  91. {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/security.md +0 -0
  92. {execsql2-2.14.1 → execsql2-2.15.0}/docs/reference/substitution_vars.md +0 -0
  93. {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/README.md +0 -0
  94. {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/package.json +0 -0
  95. {execsql2-2.14.1 → execsql2-2.15.0}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  96. {execsql2-2.14.1 → execsql2-2.15.0}/justfile +0 -0
  97. {execsql2-2.14.1 → execsql2-2.15.0}/scripts/generate_vscode_grammar.py +0 -0
  98. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/__init__.py +0 -0
  99. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/__main__.py +0 -0
  100. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/__init__.py +0 -0
  101. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/dsn.py +0 -0
  102. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/help.py +0 -0
  103. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/lint.py +0 -0
  104. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/cli/run.py +0 -0
  105. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/config.py +0 -0
  106. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/constants.py +0 -0
  107. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/__init__.py +0 -0
  108. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/access.py +0 -0
  109. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/base.py +0 -0
  110. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/dsn.py +0 -0
  111. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/duckdb.py +0 -0
  112. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/factory.py +0 -0
  113. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/firebird.py +0 -0
  114. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/mysql.py +0 -0
  115. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/oracle.py +0 -0
  116. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/postgres.py +0 -0
  117. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/sqlite.py +0 -0
  118. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/db/sqlserver.py +0 -0
  119. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/debug/__init__.py +0 -0
  120. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/debug/repl.py +0 -0
  121. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exceptions.py +0 -0
  122. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/__init__.py +0 -0
  123. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/base.py +0 -0
  124. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/delimited.py +0 -0
  125. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/duckdb.py +0 -0
  126. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/feather.py +0 -0
  127. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/html.py +0 -0
  128. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/json.py +0 -0
  129. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/latex.py +0 -0
  130. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/markdown.py +0 -0
  131. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/ods.py +0 -0
  132. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/parquet.py +0 -0
  133. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/pretty.py +0 -0
  134. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/protocol.py +0 -0
  135. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/raw.py +0 -0
  136. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/sqlite.py +0 -0
  137. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/templates.py +0 -0
  138. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/values.py +0 -0
  139. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xls.py +0 -0
  140. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xlsx.py +0 -0
  141. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/xml.py +0 -0
  142. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/yaml.py +0 -0
  143. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/exporters/zip.py +0 -0
  144. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/format.py +0 -0
  145. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/__init__.py +0 -0
  146. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/base.py +0 -0
  147. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/console.py +0 -0
  148. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/desktop.py +0 -0
  149. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/gui/tui.py +0 -0
  150. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/__init__.py +0 -0
  151. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/base.py +0 -0
  152. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/csv.py +0 -0
  153. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/feather.py +0 -0
  154. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/json.py +0 -0
  155. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/ods.py +0 -0
  156. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/importers/xls.py +0 -0
  157. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/__init__.py +0 -0
  158. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/conditions.py +0 -0
  159. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/connect.py +0 -0
  160. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/control.py +0 -0
  161. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/data.py +0 -0
  162. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/debug.py +0 -0
  163. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/dispatch.py +0 -0
  164. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io.py +0 -0
  165. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_export.py +0 -0
  166. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_fileops.py +0 -0
  167. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_import.py +0 -0
  168. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/io_write.py +0 -0
  169. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/prompt.py +0 -0
  170. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/script_ext.py +0 -0
  171. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/metacommands/system.py +0 -0
  172. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/models.py +0 -0
  173. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/parser.py +0 -0
  174. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/py.typed +0 -0
  175. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/__init__.py +0 -0
  176. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/control.py +0 -0
  177. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/engine.py +0 -0
  178. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/script/variables.py +0 -0
  179. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/state.py +0 -0
  180. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/types.py +0 -0
  181. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/__init__.py +0 -0
  182. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/auth.py +0 -0
  183. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/crypto.py +0 -0
  184. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/datetime.py +0 -0
  185. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/errors.py +0 -0
  186. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/fileio.py +0 -0
  187. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/mail.py +0 -0
  188. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/numeric.py +0 -0
  189. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/regex.py +0 -0
  190. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/strings.py +0 -0
  191. {execsql2-2.14.1 → execsql2-2.15.0}/src/execsql/utils/timer.py +0 -0
  192. {execsql2-2.14.1 → execsql2-2.15.0}/templates/README.md +0 -0
  193. {execsql2-2.14.1 → execsql2-2.15.0}/templates/config_settings.sqlite +0 -0
  194. {execsql2-2.14.1 → execsql2-2.15.0}/templates/example_config_prompt.sql +0 -0
  195. {execsql2-2.14.1 → execsql2-2.15.0}/templates/execsql.conf +0 -0
  196. {execsql2-2.14.1 → execsql2-2.15.0}/templates/make_config_db.sql +0 -0
  197. {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_compare.sql +0 -0
  198. {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_glossary.sql +0 -0
  199. {execsql2-2.14.1 → execsql2-2.15.0}/templates/md_upsert.sql +0 -0
  200. {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_compare.sql +0 -0
  201. {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_glossary.sql +0 -0
  202. {execsql2-2.14.1 → execsql2-2.15.0}/templates/pg_upsert.sql +0 -0
  203. {execsql2-2.14.1 → execsql2-2.15.0}/templates/script_template.sql +0 -0
  204. {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_compare.sql +0 -0
  205. {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_glossary.sql +0 -0
  206. {execsql2-2.14.1 → execsql2-2.15.0}/templates/ss_upsert.sql +0 -0
  207. {execsql2-2.14.1 → execsql2-2.15.0}/tests/__init__.py +0 -0
  208. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/__init__.py +0 -0
  209. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli.py +0 -0
  210. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli_e2e.py +0 -0
  211. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_cli_run.py +0 -0
  212. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_lint.py +0 -0
  213. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_ping.py +0 -0
  214. {execsql2-2.14.1 → execsql2-2.15.0}/tests/cli/test_profile.py +0 -0
  215. {execsql2-2.14.1 → execsql2-2.15.0}/tests/conftest.py +0 -0
  216. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/__init__.py +0 -0
  217. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_base.py +0 -0
  218. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_duckdb.py +0 -0
  219. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_factory.py +0 -0
  220. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_postgres.py +0 -0
  221. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_sqlite.py +0 -0
  222. {execsql2-2.14.1 → execsql2-2.15.0}/tests/db/test_sqlite_extra.py +0 -0
  223. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/__init__.py +0 -0
  224. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_base.py +0 -0
  225. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_db.py +0 -0
  226. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_delimited.py +0 -0
  227. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_duckdb_exporter.py +0 -0
  228. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_exporters.py +0 -0
  229. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_feather.py +0 -0
  230. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_html_extended.py +0 -0
  231. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_html_latex.py +0 -0
  232. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_json.py +0 -0
  233. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_json_extended.py +0 -0
  234. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_latex_extended.py +0 -0
  235. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_markdown.py +0 -0
  236. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_ods.py +0 -0
  237. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_parquet.py +0 -0
  238. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_pretty_extended.py +0 -0
  239. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_raw_extended.py +0 -0
  240. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_sqlite_exporter.py +0 -0
  241. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_templates.py +0 -0
  242. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_templates_extended.py +0 -0
  243. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_values_extended.py +0 -0
  244. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xls_xlsx.py +0 -0
  245. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xlsx.py +0 -0
  246. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_xml.py +0 -0
  247. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_yaml.py +0 -0
  248. {execsql2-2.14.1 → execsql2-2.15.0}/tests/exporters/test_zip.py +0 -0
  249. {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/__init__.py +0 -0
  250. {execsql2-2.14.1 → execsql2-2.15.0}/tests/gui/test_compare_stats.py +0 -0
  251. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/__init__.py +0 -0
  252. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_base_extended.py +0 -0
  253. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_csv_importer.py +0 -0
  254. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_feather_importer.py +0 -0
  255. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_json_importer.py +0 -0
  256. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_ods_importer.py +0 -0
  257. {execsql2-2.14.1 → execsql2-2.15.0}/tests/importers/test_xls_importer.py +0 -0
  258. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/__init__.py +0 -0
  259. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/conftest.py +0 -0
  260. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_dsn.py +0 -0
  261. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_duckdb.py +0 -0
  262. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_mysql.py +0 -0
  263. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_postgres.py +0 -0
  264. {execsql2-2.14.1 → execsql2-2.15.0}/tests/integration/test_sqlite.py +0 -0
  265. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/__init__.py +0 -0
  266. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_assert.py +0 -0
  267. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_breakpoint.py +0 -0
  268. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_connect.py +0 -0
  269. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_io_export.py +0 -0
  270. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_io_import.py +0 -0
  271. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands.py +0 -0
  272. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_connect.py +0 -0
  273. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_data.py +0 -0
  274. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_extended.py +0 -0
  275. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  276. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_io.py +0 -0
  277. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  278. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  279. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_system.py +0 -0
  280. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  281. {execsql2-2.14.1 → execsql2-2.15.0}/tests/metacommands/test_row_count.py +0 -0
  282. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config.py +0 -0
  283. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config_data.py +0 -0
  284. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_config_extended.py +0 -0
  285. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_constants.py +0 -0
  286. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_engine.py +0 -0
  287. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_error_messages.py +0 -0
  288. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_exceptions.py +0 -0
  289. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_format.py +0 -0
  290. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_mail.py +0 -0
  291. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_models.py +0 -0
  292. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_package.py +0 -0
  293. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_parser.py +0 -0
  294. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_registry.py +0 -0
  295. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_script.py +0 -0
  296. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_state.py +0 -0
  297. {execsql2-2.14.1 → execsql2-2.15.0}/tests/test_types.py +0 -0
  298. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/__init__.py +0 -0
  299. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_auth.py +0 -0
  300. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_auth_extra.py +0 -0
  301. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_crypto.py +0 -0
  302. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_datetime.py +0 -0
  303. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_errors.py +0 -0
  304. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_errors_extra.py +0 -0
  305. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_fileio.py +0 -0
  306. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_fileio_extra.py +0 -0
  307. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_numeric.py +0 -0
  308. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_regex.py +0 -0
  309. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_strings.py +0 -0
  310. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_timer.py +0 -0
  311. {execsql2-2.14.1 → execsql2-2.15.0}/tests/utils/test_timer_extra.py +0 -0
  312. {execsql2-2.14.1 → execsql2-2.15.0}/zensical.toml +0 -0
@@ -13,8 +13,28 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.15.0] - 2026-04-09
17
+
18
+ ### Added
19
+
20
+ - `PG_UPSERT` metacommand: new `EXPORT_FAILURES <dir>`, `EXPORT_FORMAT csv|json|xlsx`, and `EXPORT_MAX_ROWS <n>` keywords that write a "fix sheet" of failing QA rows — one row per unique violating staging row with a consolidated `_issues` column — to CSV, JSON, or XLSX. Works in all three modes (full pipeline, QA-only, schema check) and runs even when QA fails. New `$PG_UPSERT_EXPORT_PATH` substitution variable holds the directory written. A user-visible message reporting the export directory and format is emitted to both the console and the execsql log after every export.
21
+
22
+ ### Changed
23
+
24
+ - `[upsert]` extra now requires `pg-upsert>=1.21.0` (up from `>=1.20.0`) for the fix-sheet export feature.
25
+
26
+ ### Fixed
27
+
28
+ - `PROMPT MESSAGE ... CREDENTIALS <user_var> <pw_var>` no longer crashes in console-fallback mode with `TypeError: get_password() missing 2 required positional arguments: 'database_name' and 'user_name'`. The fallback now uses `getpass.getpass()` to read the password, matching the intent (keyring-aware `auth.get_password()` is for CONNECT, not for bare credential prompts).
29
+
30
+ ______________________________________________________________________
31
+
16
32
  ## [2.14.1] - 2026-04-07
17
33
 
34
+ ### Fixed
35
+
36
+ - Fix Windows CI: use `zf.namelist()[0]` instead of path for zip entry lookup.
37
+
18
38
  ______________________________________________________________________
19
39
 
20
40
  ## [2.14.0] - 2026-04-07
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.14.1
3
+ Version: 2.15.0
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,7 +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.20.0; extra == 'all'
54
+ Requires-Dist: pg-upsert>=1.21.0; extra == 'all'
55
55
  Requires-Dist: polars; extra == 'all'
56
56
  Requires-Dist: psycopg2-binary; extra == 'all'
57
57
  Requires-Dist: pymysql; extra == 'all'
@@ -109,7 +109,7 @@ Requires-Dist: oracledb; extra == 'oracle'
109
109
  Provides-Extra: postgres
110
110
  Requires-Dist: psycopg2-binary; extra == 'postgres'
111
111
  Provides-Extra: upsert
112
- Requires-Dist: pg-upsert>=1.20.0; extra == 'upsert'
112
+ Requires-Dist: pg-upsert>=1.21.0; extra == 'upsert'
113
113
  Description-Content-Type: text/markdown
114
114
 
115
115
  > [!NOTE]
@@ -41,14 +41,14 @@ ______________________________________________________________________
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. |
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. |
51
- | `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
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. Supports `EXPORT_FAILURES`, `EXPORT_FORMAT`, and `EXPORT_MAX_ROWS` keywords to write a "fix sheet" of failing QA rows to CSV/JSON/XLSX (requires `pg-upsert>=1.21.0`). |
51
+ | `IMPORT … FROM JSON` | Import a JSON file (array of objects or NDJSON) into a database table. Nested objects are flattened with dot-separated column names; arrays are stored as JSON strings. |
52
52
 
53
53
  ### Conditional Tests
54
54
 
@@ -2127,6 +2127,9 @@ Keywords can appear in any order after the table list.
2127
2127
  | `COMPACT` | Use compact grid format for QA summary instead of detailed per-table panels. |
2128
2128
  | `LOGFILE <path>` | Append pg-upsert's plain-text log output to the given file. Supports quoted paths for spaces: `LOGFILE "path/to/log.txt"`. |
2129
2129
  | `CLEANUP` | Drop all `ups_*` temporary tables and views after execution. Without it, temp objects persist for inspection (default). |
2130
+ | `EXPORT_FAILURES <dir>` | Write a "fix sheet" of failing QA rows into `<dir>` (directory is created if missing). One row per unique violating staging row, with an `_issues` column summarizing every problem on that row. Supports quoted paths for spaces. Export runs even when QA fails — that is the whole point. A confirmation message (`PG_UPSERT: exported QA failures to <dir> (<format>)`) is written to the terminal, the execsql log, and the `LOGFILE` target if one is given. |
2131
+ | `EXPORT_FORMAT csv\|json\|xlsx` | Fix sheet format when `EXPORT_FAILURES` is given. `csv` (default) writes one file per table; `json` writes a single nested file; `xlsx` writes a single workbook with one sheet per table (requires `openpyxl`). |
2132
+ | `EXPORT_MAX_ROWS <n>` | Maximum rows to capture per check per table for the fix sheet. Default `1000`. Only meaningful with `EXPORT_FAILURES`. |
2130
2133
 
2131
2134
  ### Substitution variables
2132
2135
 
@@ -2150,6 +2153,7 @@ Set after every `PG_UPSERT` execution:
2150
2153
  | `$PG_UPSERT_TABLE_QA_PASSED` | TRUE/FALSE | QA result for the current table (updated per table) |
2151
2154
  | `$PG_UPSERT_TABLE_ROWS_UPDATED` | integer | Rows updated for the current table (updated per table) |
2152
2155
  | `$PG_UPSERT_TABLE_ROWS_INSERTED` | integer | Rows inserted for the current table (updated per table) |
2156
+ | `$PG_UPSERT_EXPORT_PATH` | string | Directory path that the QA fix sheet was written to, or empty if `EXPORT_FAILURES` was not given or nothing was exported. |
2153
2157
 
2154
2158
  !!! note "Using `$PG_UPSERT_RESULT_JSON` with WRITE"
2155
2159
  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:
@@ -2211,6 +2215,13 @@ For the full list of temporary objects and their schemas, see the [pg-upsert Tem
2211
2215
  -- Write the full JSON result using tilde delimiters (JSON contains " and [])
2212
2216
  -- !x! PG_UPSERT FROM staging TO public TABLES books COMMIT
2213
2217
  -- !x! WRITE ~!!$PG_UPSERT_RESULT_JSON!!~
2218
+
2219
+ -- Export failing QA rows to an Excel fix sheet (one sheet per table)
2220
+ -- !x! PG_UPSERT QA FROM staging TO public TABLES books, authors EXPORT_FAILURES "qa_failures/" EXPORT_FORMAT xlsx
2221
+
2222
+ -- Full pipeline with a CSV fix sheet cap of 50 rows per check per table
2223
+ -- !x! PG_UPSERT FROM staging TO public TABLES books, authors EXPORT_FAILURES /tmp/fix EXPORT_MAX_ROWS 50 COMMIT
2224
+ -- !x! WRITE "Fix sheet: !!$PG_UPSERT_EXPORT_PATH!!"
2214
2225
  ```
2215
2226
 
2216
2227
 
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.14.1"
7
+ version = "2.15.0"
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,7 +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.20.0"]
61
+ upsert = ["pg-upsert>=1.21.0"]
62
62
  # Convenience groups
63
63
  all-db = [
64
64
  "psycopg2-binary",
@@ -161,7 +161,7 @@ skip-magic-trailing-comma = false
161
161
  line-ending = "auto"
162
162
 
163
163
  [tool.bumpversion]
164
- current_version = "2.14.1"
164
+ current_version = "2.15.0"
165
165
  commit = true
166
166
  commit_args = "--no-verify"
167
167
  tag = true
@@ -26,11 +26,11 @@ from execsql.utils.errors import exception_desc
26
26
 
27
27
  _KW_METHOD = re.compile(r"\bMETHOD\s+(upsert|update|insert)\b", re.IGNORECASE)
28
28
  _KW_EXCLUDE = re.compile(
29
- r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP)\b|\s*$)",
29
+ r"\bEXCLUDE\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
30
30
  re.IGNORECASE,
31
31
  )
32
32
  _KW_EXCLUDE_NULL = re.compile(
33
- r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP)\b|\s*$)",
33
+ r"\bEXCLUDE_NULL\s+([\w\s,]+?)(?=\s+(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b|\s*$)",
34
34
  re.IGNORECASE,
35
35
  )
36
36
  _KW_COMMIT = re.compile(r"\bCOMMIT\b", re.IGNORECASE)
@@ -38,13 +38,21 @@ _KW_INTERACTIVE = re.compile(r"\bINTERACTIVE\b", re.IGNORECASE)
38
38
  _KW_COMPACT = re.compile(r"\bCOMPACT\b", re.IGNORECASE)
39
39
  _KW_CLEANUP = re.compile(r"\bCLEANUP\b", re.IGNORECASE)
40
40
  _KW_LOGFILE = re.compile(r"""\bLOGFILE\s+(?:"([^"]+)"|'([^']+)'|(\S+))""", re.IGNORECASE)
41
+ _KW_EXPORT_FAILURES = re.compile(
42
+ r"""\bEXPORT_FAILURES\s+(?:"([^"]+)"|'([^']+)'|(\S+))""",
43
+ re.IGNORECASE,
44
+ )
45
+ _KW_EXPORT_FORMAT = re.compile(r"\bEXPORT_FORMAT\s+(\S+)", re.IGNORECASE)
46
+ _KW_EXPORT_MAX_ROWS = re.compile(r"\bEXPORT_MAX_ROWS\s+(\S+)", re.IGNORECASE)
41
47
 
42
48
  # All recognized keywords — used to split table names from options.
43
49
  _ALL_KEYWORDS = re.compile(
44
- r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP)\b",
50
+ r"\b(?:METHOD|COMMIT|INTERACTIVE|COMPACT|EXCLUDE_NULL|EXCLUDE|LOGFILE|CLEANUP|EXPORT_FAILURES|EXPORT_FORMAT|EXPORT_MAX_ROWS)\b",
45
51
  re.IGNORECASE,
46
52
  )
47
53
 
54
+ _VALID_EXPORT_FORMATS = ("csv", "json", "xlsx")
55
+
48
56
 
49
57
  def _parse_tables_and_options(tail: str) -> dict[str, Any]:
50
58
  """Parse the trailing text after ``TABLES`` into table names and options.
@@ -90,6 +98,40 @@ def _parse_tables_and_options(tail: str) -> dict[str, Any]:
90
98
  if m:
91
99
  logfile = m.group(1) or m.group(2) or m.group(3)
92
100
 
101
+ export_failures: str | None = None
102
+ m = _KW_EXPORT_FAILURES.search(opts_part)
103
+ if m:
104
+ export_failures = m.group(1) or m.group(2) or m.group(3)
105
+
106
+ export_format = "csv"
107
+ m = _KW_EXPORT_FORMAT.search(opts_part)
108
+ if m:
109
+ fmt = m.group(1).lower()
110
+ if fmt not in _VALID_EXPORT_FORMATS:
111
+ raise ErrInfo(
112
+ "cmd",
113
+ other_msg=(
114
+ f"PG_UPSERT: unsupported EXPORT_FORMAT {m.group(1)!r}. "
115
+ f"Supported: {', '.join(_VALID_EXPORT_FORMATS)}"
116
+ ),
117
+ )
118
+ export_format = fmt
119
+
120
+ export_max_rows = 1000
121
+ m = _KW_EXPORT_MAX_ROWS.search(opts_part)
122
+ if m:
123
+ raw = m.group(1)
124
+ try:
125
+ val = int(raw)
126
+ if val <= 0:
127
+ raise ValueError
128
+ except ValueError as exc:
129
+ raise ErrInfo(
130
+ "cmd",
131
+ other_msg=(f"PG_UPSERT: EXPORT_MAX_ROWS must be a positive integer, got {raw!r}"),
132
+ ) from exc
133
+ export_max_rows = val
134
+
93
135
  return {
94
136
  "tables": tables,
95
137
  "method": method,
@@ -100,6 +142,9 @@ def _parse_tables_and_options(tail: str) -> dict[str, Any]:
100
142
  "exclude_null_check_cols": exclude_null,
101
143
  "logfile": logfile,
102
144
  "cleanup": bool(_KW_CLEANUP.search(opts_part)),
145
+ "export_failures": export_failures,
146
+ "export_format": export_format,
147
+ "export_max_rows": export_max_rows,
103
148
  }
104
149
 
105
150
 
@@ -157,6 +202,9 @@ def _set_subvars(result: Any) -> None:
157
202
  sv("$PG_UPSERT_STARTED_AT", result.started_at)
158
203
  sv("$PG_UPSERT_FINISHED_AT", result.finished_at)
159
204
  sv("$PG_UPSERT_RESULT_JSON", json.dumps(result.to_dict(), separators=(",", ":")))
205
+ # Default export path subvar to empty; _export_failures_if_requested
206
+ # will overwrite it with the actual path if an export was produced.
207
+ sv("$PG_UPSERT_EXPORT_PATH", "")
160
208
 
161
209
 
162
210
  def _qa_failure_msg(result: Any) -> str:
@@ -241,20 +289,26 @@ def _create_pgupsert(
241
289
  if _state.conf:
242
290
  ui_mode = _state.conf.gui_framework
243
291
 
244
- ups = PgUpsert(
245
- conn=db.conn,
246
- staging_schema=staging_schema,
247
- base_schema=base_schema,
248
- tables=opts["tables"],
249
- do_commit=opts["commit"],
250
- interactive=opts["interactive"],
251
- compact=opts["compact"],
252
- upsert_method=opts["method"],
253
- exclude_cols=opts["exclude_cols"],
254
- exclude_null_check_cols=opts["exclude_null_check_cols"],
255
- ui_mode=ui_mode,
256
- callback=_make_callback(),
257
- )
292
+ kwargs: dict[str, Any] = {
293
+ "conn": db.conn,
294
+ "staging_schema": staging_schema,
295
+ "base_schema": base_schema,
296
+ "tables": opts["tables"],
297
+ "do_commit": opts["commit"],
298
+ "interactive": opts["interactive"],
299
+ "compact": opts["compact"],
300
+ "upsert_method": opts["method"],
301
+ "exclude_cols": opts["exclude_cols"],
302
+ "exclude_null_check_cols": opts["exclude_null_check_cols"],
303
+ "ui_mode": ui_mode,
304
+ "callback": _make_callback(),
305
+ }
306
+ # Only pass fix-sheet capture args when an export was requested, so the
307
+ # metacommand stays compatible with any pg-upsert build that lacks them.
308
+ if opts.get("export_failures"):
309
+ kwargs["capture_detail_rows"] = True
310
+ kwargs["max_export_rows"] = opts.get("export_max_rows", 1000)
311
+ ups = PgUpsert(**kwargs)
258
312
  return ups
259
313
 
260
314
 
@@ -328,6 +382,57 @@ def _run_with_autocommit_guard(db: Any, fn: Any) -> Any:
328
382
  db.autocommit_on()
329
383
 
330
384
 
385
+ def _export_failures_if_requested(
386
+ result: Any,
387
+ opts: dict[str, Any],
388
+ metacommandline: str | None,
389
+ ) -> None:
390
+ """Export a QA fix sheet if EXPORT_FAILURES was given in the metacommand.
391
+
392
+ Always called after ``_set_subvars(result)`` so ``$PG_UPSERT_EXPORT_PATH``
393
+ is initialized to empty first, then overwritten here on a successful
394
+ export. Called even when QA failed — that's the whole point of the
395
+ fix sheet.
396
+ """
397
+ path = opts.get("export_failures")
398
+ if not path:
399
+ return
400
+ fmt = opts["export_format"]
401
+ try:
402
+ exported = result.export_failures(path, fmt=fmt)
403
+ except Exception as exc:
404
+ raise ErrInfo(
405
+ "exception",
406
+ exception_msg=exception_desc(),
407
+ other_msg=(f"PG_UPSERT failed to export failure sheet to {path}"),
408
+ ) from exc
409
+ _state.subvars.add_substitution(
410
+ "$PG_UPSERT_EXPORT_PATH",
411
+ str(exported) if exported else "",
412
+ )
413
+ if exported:
414
+ msg = f"PG_UPSERT: exported QA failures to {exported} ({fmt})"
415
+ else:
416
+ msg = f"PG_UPSERT: no QA failures to export (EXPORT_FAILURES {path} skipped)"
417
+ _state.exec_log.log_user_msg(msg)
418
+ try:
419
+ _state.output.write(msg + "\n")
420
+ except Exception:
421
+ # Output sink may be unavailable in some contexts (e.g. tests);
422
+ # the log message above is sufficient in that case.
423
+ pass
424
+ # Tee to the LOGFILE keyword target if one was given, matching how the
425
+ # pg-upsert display output is routed there via _FileWriterHandler.
426
+ logfile = opts.get("logfile")
427
+ if logfile:
428
+ try:
429
+ from execsql.utils.fileio import filewriter_write
430
+
431
+ filewriter_write(logfile, msg + "\n")
432
+ except Exception:
433
+ pass
434
+
435
+
331
436
  def _handle_pg_upsert_errors(fn: Any, metacommandline: str | None) -> Any:
332
437
  """Run *fn*, translating pg-upsert exceptions to ErrInfo."""
333
438
  from pg_upsert import UserCancelledError
@@ -381,6 +486,7 @@ def x_pg_upsert(**kwargs: Any) -> None:
381
486
  _detach_log_handlers(loggers, handlers, prev_levels)
382
487
 
383
488
  _set_subvars(result)
489
+ _export_failures_if_requested(result, opts, metacommandline)
384
490
  if opts.get("cleanup"):
385
491
  ups.cleanup()
386
492
 
@@ -420,6 +526,7 @@ def x_pg_upsert_qa(**kwargs: Any) -> None:
420
526
 
421
527
  result = _build_result_from_qa_errors(ups)
422
528
  _set_subvars(result)
529
+ _export_failures_if_requested(result, opts, metacommandline)
423
530
  if opts.get("cleanup"):
424
531
  ups.cleanup()
425
532
 
@@ -462,6 +569,7 @@ def x_pg_upsert_check(**kwargs: Any) -> None:
462
569
 
463
570
  result = _build_result_from_qa_errors(ups)
464
571
  _set_subvars(result)
572
+ _export_failures_if_requested(result, opts, metacommandline)
465
573
  if opts.get("cleanup"):
466
574
  ups.cleanup()
467
575
 
@@ -498,10 +498,10 @@ def gui_credentials(
498
498
  cmd:
499
499
  The originating metacommand line (for logging only).
500
500
  """
501
+ import getpass as _getpass
501
502
  import queue as _queue
502
503
 
503
504
  import execsql.state as _state
504
- from execsql.utils.auth import get_password
505
505
 
506
506
  gui_level = _state.conf.gui_level if _state.conf else 0
507
507
  if gui_level > 0 and _state.gui_manager_thread is not None and _state.gui_manager_thread.is_alive():
@@ -515,7 +515,7 @@ def gui_credentials(
515
515
  if message:
516
516
  print(message, file=sys.stderr)
517
517
  uname = input("Username: ")
518
- passwd = get_password(f"Password for {uname}: ")
518
+ passwd = _getpass.getpass(f"Password for {uname}: ")
519
519
 
520
520
  if username:
521
521
  _state.subvars.add_substitution(username, uname)
@@ -808,6 +808,34 @@ class TestGuiPublicAPI:
808
808
  monkeypatch.setattr("builtins.input", lambda *a: next(inputs))
809
809
  assert get_yn("?") is False
810
810
 
811
+ def test_gui_credentials_console_fallback(self, monkeypatch):
812
+ """gui_credentials must not call auth.get_password (which needs
813
+ dbms/database/user kwargs). Regression for the
814
+ 'get_password() missing 2 required positional arguments' bug.
815
+ """
816
+ from unittest.mock import MagicMock
817
+
818
+ import execsql.state as _state
819
+ from execsql.utils.gui import gui_credentials
820
+
821
+ # Force the fallback branch: no GUI manager thread.
822
+ monkeypatch.setattr(_state, "gui_manager_thread", None)
823
+ conf = MagicMock()
824
+ conf.gui_level = 0
825
+ monkeypatch.setattr(_state, "conf", conf)
826
+
827
+ subvars = MagicMock()
828
+ monkeypatch.setattr(_state, "subvars", subvars)
829
+
830
+ monkeypatch.setattr("builtins.input", lambda *a: "alice")
831
+ monkeypatch.setattr("getpass.getpass", lambda *a, **kw: "s3cret")
832
+
833
+ gui_credentials(message="Log in", username="$U", pwtext="$P")
834
+
835
+ calls = {c[0][0]: c[0][1] for c in subvars.add_substitution.call_args_list}
836
+ assert calls["$U"] == "alice"
837
+ assert calls["$P"] == "s3cret"
838
+
811
839
  def test_pause_no_countdown(self, monkeypatch):
812
840
  from execsql.utils.gui import pause
813
841