execsql2 2.4.0__tar.gz → 2.4.1__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 (266) hide show
  1. {execsql2-2.4.0 → execsql2-2.4.1}/.github/workflows/ci-cd.yml +54 -2
  2. {execsql2-2.4.0 → execsql2-2.4.1}/CHANGELOG.md +12 -0
  3. {execsql2-2.4.0 → execsql2-2.4.1}/PKG-INFO +1 -1
  4. {execsql2-2.4.0 → execsql2-2.4.1}/pyproject.toml +2 -2
  5. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/cli/run.py +11 -5
  6. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/factory.py +10 -5
  7. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/mysql.py +14 -1
  8. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/base.py +3 -3
  9. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/csv.py +5 -5
  10. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/feather.py +4 -4
  11. execsql2-2.4.1/tests/integration/conftest.py +36 -0
  12. execsql2-2.4.1/tests/integration/test_dsn.py +169 -0
  13. execsql2-2.4.0/tests/test_integration_duckdb.py → execsql2-2.4.1/tests/integration/test_duckdb.py +33 -58
  14. execsql2-2.4.1/tests/integration/test_mysql.py +415 -0
  15. execsql2-2.4.1/tests/integration/test_postgres.py +414 -0
  16. execsql2-2.4.0/tests/test_integration.py → execsql2-2.4.1/tests/integration/test_sqlite.py +52 -76
  17. execsql2-2.4.1/tests/utils/__init__.py +0 -0
  18. {execsql2-2.4.0 → execsql2-2.4.1}/uv.lock +1 -1
  19. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/dba.md +0 -0
  20. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/herald.md +0 -0
  21. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/inspector.md +0 -0
  22. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/oracle.md +0 -0
  23. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/patcher.md +0 -0
  24. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/qa.md +0 -0
  25. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/agents/scribe.md +0 -0
  26. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/code-oracle.md +0 -0
  27. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/migrate.md +0 -0
  28. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/review-changes.md +0 -0
  29. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/test-module.md +0 -0
  30. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/update-changelog.md +0 -0
  31. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/commands/where-is.md +0 -0
  32. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/project_context.md +0 -0
  33. {execsql2-2.4.0 → execsql2-2.4.1}/.claude/state/status.md +0 -0
  34. {execsql2-2.4.0 → execsql2-2.4.1}/.gitignore +0 -0
  35. {execsql2-2.4.0 → execsql2-2.4.1}/.pre-commit-config.yaml +0 -0
  36. {execsql2-2.4.0 → execsql2-2.4.1}/.python-version +0 -0
  37. {execsql2-2.4.0 → execsql2-2.4.1}/.readthedocs.yaml +0 -0
  38. {execsql2-2.4.0 → execsql2-2.4.1}/CLAUDE.md +0 -0
  39. {execsql2-2.4.0 → execsql2-2.4.1}/CONTRIBUTING.md +0 -0
  40. {execsql2-2.4.0 → execsql2-2.4.1}/LICENSE.txt +0 -0
  41. {execsql2-2.4.0 → execsql2-2.4.1}/NOTICE +0 -0
  42. {execsql2-2.4.0 → execsql2-2.4.1}/README.md +0 -0
  43. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/cli.md +0 -0
  44. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/db.md +0 -0
  45. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/exporters.md +0 -0
  46. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/importers.md +0 -0
  47. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/index.md +0 -0
  48. {execsql2-2.4.0 → execsql2-2.4.1}/docs/api/metacommands.md +0 -0
  49. {execsql2-2.4.0 → execsql2-2.4.1}/docs/change_log.md +0 -0
  50. {execsql2-2.4.0 → execsql2-2.4.1}/docs/configuration.md +0 -0
  51. {execsql2-2.4.0 → execsql2-2.4.1}/docs/contributors.md +0 -0
  52. {execsql2-2.4.0 → execsql2-2.4.1}/docs/copyright.md +0 -0
  53. {execsql2-2.4.0 → execsql2-2.4.1}/docs/debugging.md +0 -0
  54. {execsql2-2.4.0 → execsql2-2.4.1}/docs/dev/adding_db_adapters.md +0 -0
  55. {execsql2-2.4.0 → execsql2-2.4.1}/docs/dev/adding_exporters.md +0 -0
  56. {execsql2-2.4.0 → execsql2-2.4.1}/docs/dev/adding_importers.md +0 -0
  57. {execsql2-2.4.0 → execsql2-2.4.1}/docs/dev/adding_metacommands.md +0 -0
  58. {execsql2-2.4.0 → execsql2-2.4.1}/docs/documentation.md +0 -0
  59. {execsql2-2.4.0 → execsql2-2.4.1}/docs/encoding.md +0 -0
  60. {execsql2-2.4.0 → execsql2-2.4.1}/docs/examples.md +0 -0
  61. {execsql2-2.4.0 → execsql2-2.4.1}/docs/formatter.md +0 -0
  62. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/Compare_planets.png +0 -0
  63. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/actions.png +0 -0
  64. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/actions2.png +0 -0
  65. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/checkboxes.png +0 -0
  66. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/connect.b64 +0 -0
  67. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/connect.png +0 -0
  68. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/create_conf.png +0 -0
  69. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/data_error1_screenshot.jpg +0 -0
  70. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/entry_form.png +0 -0
  71. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/execsql_console.png +0 -0
  72. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/execsql_logo_01.png +0 -0
  73. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/fatals.png +0 -0
  74. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/logo_small.png +0 -0
  75. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/pause_terminal.png +0 -0
  76. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/pause_terminal_sm.b64 +0 -0
  77. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/pause_terminal_sm.png +0 -0
  78. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/prompt_compare.png +0 -0
  79. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/set_build_commands.jpg +0 -0
  80. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/unit_conversions.b64 +0 -0
  81. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/unit_conversions_029.png +0 -0
  82. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/unmatched.png +0 -0
  83. {execsql2-2.4.0 → execsql2-2.4.1}/docs/images/vim_execsql_highlight.png +0 -0
  84. {execsql2-2.4.0 → execsql2-2.4.1}/docs/index.md +0 -0
  85. {execsql2-2.4.0 → execsql2-2.4.1}/docs/installation.md +0 -0
  86. {execsql2-2.4.0 → execsql2-2.4.1}/docs/logging.md +0 -0
  87. {execsql2-2.4.0 → execsql2-2.4.1}/docs/metacommands.md +0 -0
  88. {execsql2-2.4.0 → execsql2-2.4.1}/docs/requirements.md +0 -0
  89. {execsql2-2.4.0 → execsql2-2.4.1}/docs/security.md +0 -0
  90. {execsql2-2.4.0 → execsql2-2.4.1}/docs/sql_syntax.md +0 -0
  91. {execsql2-2.4.0 → execsql2-2.4.1}/docs/substitution_vars.md +0 -0
  92. {execsql2-2.4.0 → execsql2-2.4.1}/docs/syntax.md +0 -0
  93. {execsql2-2.4.0 → execsql2-2.4.1}/docs/usage.md +0 -0
  94. {execsql2-2.4.0 → execsql2-2.4.1}/docs/using_scripts.md +0 -0
  95. {execsql2-2.4.0 → execsql2-2.4.1}/extras/vscode-execsql/README.md +0 -0
  96. {execsql2-2.4.0 → execsql2-2.4.1}/extras/vscode-execsql/package.json +0 -0
  97. {execsql2-2.4.0 → execsql2-2.4.1}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  98. {execsql2-2.4.0 → execsql2-2.4.1}/justfile +0 -0
  99. {execsql2-2.4.0 → execsql2-2.4.1}/scripts/generate_vscode_grammar.py +0 -0
  100. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/__init__.py +0 -0
  101. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/__main__.py +0 -0
  102. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/cli/__init__.py +0 -0
  103. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/cli/dsn.py +0 -0
  104. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/cli/help.py +0 -0
  105. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/config.py +0 -0
  106. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/constants.py +0 -0
  107. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/__init__.py +0 -0
  108. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/access.py +0 -0
  109. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/base.py +0 -0
  110. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/dsn.py +0 -0
  111. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/duckdb.py +0 -0
  112. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/firebird.py +0 -0
  113. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/oracle.py +0 -0
  114. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/postgres.py +0 -0
  115. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/sqlite.py +0 -0
  116. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/db/sqlserver.py +0 -0
  117. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exceptions.py +0 -0
  118. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/__init__.py +0 -0
  119. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/base.py +0 -0
  120. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/delimited.py +0 -0
  121. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/duckdb.py +0 -0
  122. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/feather.py +0 -0
  123. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/html.py +0 -0
  124. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/json.py +0 -0
  125. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/latex.py +0 -0
  126. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/ods.py +0 -0
  127. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/parquet.py +0 -0
  128. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/pretty.py +0 -0
  129. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/raw.py +0 -0
  130. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/sqlite.py +0 -0
  131. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/templates.py +0 -0
  132. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/values.py +0 -0
  133. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/xls.py +0 -0
  134. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/xml.py +0 -0
  135. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/exporters/zip.py +0 -0
  136. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/format.py +0 -0
  137. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/gui/__init__.py +0 -0
  138. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/gui/base.py +0 -0
  139. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/gui/console.py +0 -0
  140. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/gui/desktop.py +0 -0
  141. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/gui/tui.py +0 -0
  142. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/__init__.py +0 -0
  143. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/ods.py +0 -0
  144. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/importers/xls.py +0 -0
  145. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/__init__.py +0 -0
  146. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/conditions.py +0 -0
  147. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/connect.py +0 -0
  148. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/control.py +0 -0
  149. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/data.py +0 -0
  150. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/debug.py +0 -0
  151. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/dispatch.py +0 -0
  152. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/io.py +0 -0
  153. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/io_export.py +0 -0
  154. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/io_fileops.py +0 -0
  155. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/io_import.py +0 -0
  156. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/io_write.py +0 -0
  157. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/prompt.py +0 -0
  158. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/script_ext.py +0 -0
  159. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/metacommands/system.py +0 -0
  160. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/models.py +0 -0
  161. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/parser.py +0 -0
  162. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/py.typed +0 -0
  163. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/script/__init__.py +0 -0
  164. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/script/control.py +0 -0
  165. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/script/engine.py +0 -0
  166. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/script/variables.py +0 -0
  167. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/state.py +0 -0
  168. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/types.py +0 -0
  169. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/__init__.py +0 -0
  170. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/auth.py +0 -0
  171. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/crypto.py +0 -0
  172. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/datetime.py +0 -0
  173. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/errors.py +0 -0
  174. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/fileio.py +0 -0
  175. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/gui.py +0 -0
  176. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/mail.py +0 -0
  177. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/numeric.py +0 -0
  178. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/regex.py +0 -0
  179. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/strings.py +0 -0
  180. {execsql2-2.4.0 → execsql2-2.4.1}/src/execsql/utils/timer.py +0 -0
  181. {execsql2-2.4.0 → execsql2-2.4.1}/templates/README.md +0 -0
  182. {execsql2-2.4.0 → execsql2-2.4.1}/templates/config_settings.sqlite +0 -0
  183. {execsql2-2.4.0 → execsql2-2.4.1}/templates/example_config_prompt.sql +0 -0
  184. {execsql2-2.4.0 → execsql2-2.4.1}/templates/execsql.conf +0 -0
  185. {execsql2-2.4.0 → execsql2-2.4.1}/templates/make_config_db.sql +0 -0
  186. {execsql2-2.4.0 → execsql2-2.4.1}/templates/md_compare.sql +0 -0
  187. {execsql2-2.4.0 → execsql2-2.4.1}/templates/md_glossary.sql +0 -0
  188. {execsql2-2.4.0 → execsql2-2.4.1}/templates/md_upsert.sql +0 -0
  189. {execsql2-2.4.0 → execsql2-2.4.1}/templates/pg_compare.sql +0 -0
  190. {execsql2-2.4.0 → execsql2-2.4.1}/templates/pg_glossary.sql +0 -0
  191. {execsql2-2.4.0 → execsql2-2.4.1}/templates/pg_upsert.sql +0 -0
  192. {execsql2-2.4.0 → execsql2-2.4.1}/templates/script_template.sql +0 -0
  193. {execsql2-2.4.0 → execsql2-2.4.1}/templates/ss_compare.sql +0 -0
  194. {execsql2-2.4.0 → execsql2-2.4.1}/templates/ss_glossary.sql +0 -0
  195. {execsql2-2.4.0 → execsql2-2.4.1}/templates/ss_upsert.sql +0 -0
  196. {execsql2-2.4.0 → execsql2-2.4.1}/tests/__init__.py +0 -0
  197. {execsql2-2.4.0 → execsql2-2.4.1}/tests/conftest.py +0 -0
  198. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/__init__.py +0 -0
  199. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/test_base.py +0 -0
  200. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/test_duckdb.py +0 -0
  201. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/test_factory.py +0 -0
  202. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/test_postgres.py +0 -0
  203. {execsql2-2.4.0 → execsql2-2.4.1}/tests/db/test_sqlite.py +0 -0
  204. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/__init__.py +0 -0
  205. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_base.py +0 -0
  206. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_db.py +0 -0
  207. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_delimited.py +0 -0
  208. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_duckdb_exporter.py +0 -0
  209. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_exporters.py +0 -0
  210. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_feather.py +0 -0
  211. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_html_latex.py +0 -0
  212. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_json.py +0 -0
  213. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_ods.py +0 -0
  214. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_parquet.py +0 -0
  215. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_sqlite_exporter.py +0 -0
  216. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_templates.py +0 -0
  217. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_xls_xlsx.py +0 -0
  218. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_xml.py +0 -0
  219. {execsql2-2.4.0 → execsql2-2.4.1}/tests/exporters/test_zip.py +0 -0
  220. {execsql2-2.4.0 → execsql2-2.4.1}/tests/gui/__init__.py +0 -0
  221. {execsql2-2.4.0 → execsql2-2.4.1}/tests/gui/test_backends.py +0 -0
  222. {execsql2-2.4.0 → execsql2-2.4.1}/tests/importers/__init__.py +0 -0
  223. {execsql2-2.4.0 → execsql2-2.4.1}/tests/importers/test_csv_importer.py +0 -0
  224. {execsql2-2.4.0 → execsql2-2.4.1}/tests/importers/test_feather_importer.py +0 -0
  225. {execsql2-2.4.0 → execsql2-2.4.1}/tests/importers/test_ods_importer.py +0 -0
  226. {execsql2-2.4.0 → execsql2-2.4.1}/tests/importers/test_xls_importer.py +0 -0
  227. {execsql2-2.4.0/tests/metacommands → execsql2-2.4.1/tests/integration}/__init__.py +0 -0
  228. {execsql2-2.4.0/tests/utils → execsql2-2.4.1/tests/metacommands}/__init__.py +0 -0
  229. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands.py +0 -0
  230. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_connect.py +0 -0
  231. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_data.py +0 -0
  232. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_extended.py +0 -0
  233. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  234. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_io.py +0 -0
  235. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  236. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  237. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_system.py +0 -0
  238. {execsql2-2.4.0 → execsql2-2.4.1}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  239. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_cli.py +0 -0
  240. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_config.py +0 -0
  241. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_config_data.py +0 -0
  242. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_constants.py +0 -0
  243. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_exceptions.py +0 -0
  244. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_format.py +0 -0
  245. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_mail.py +0 -0
  246. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_models.py +0 -0
  247. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_package.py +0 -0
  248. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_parser.py +0 -0
  249. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_registry.py +0 -0
  250. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_script.py +0 -0
  251. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_state.py +0 -0
  252. {execsql2-2.4.0 → execsql2-2.4.1}/tests/test_types.py +0 -0
  253. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_auth.py +0 -0
  254. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_auth_extra.py +0 -0
  255. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_crypto.py +0 -0
  256. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_datetime.py +0 -0
  257. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_errors.py +0 -0
  258. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_errors_extra.py +0 -0
  259. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_fileio.py +0 -0
  260. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_fileio_extra.py +0 -0
  261. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_numeric.py +0 -0
  262. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_regex.py +0 -0
  263. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_strings.py +0 -0
  264. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_timer.py +0 -0
  265. {execsql2-2.4.0 → execsql2-2.4.1}/tests/utils/test_timer_extra.py +0 -0
  266. {execsql2-2.4.0 → execsql2-2.4.1}/zensical.toml +0 -0
@@ -51,7 +51,6 @@ jobs:
51
51
  python -m pip install --upgrade pip
52
52
  python -m pip install tox
53
53
  python -m pip install ".[dev]"
54
- python -m pip install ".[formats]"
55
54
  - name: Install tkinter (Ubuntu)
56
55
  if: runner.os == 'Linux'
57
56
  run: sudo apt-get install -y python3-tk
@@ -67,10 +66,63 @@ jobs:
67
66
  token: ${{ secrets.CODECOV_TOKEN }}
68
67
  files: coverage.xml
69
68
 
69
+ integration-tests:
70
+ name: integration-tests-py${{ matrix.python-version }}
71
+ runs-on: ubuntu-latest
72
+ strategy:
73
+ fail-fast: false
74
+ matrix:
75
+ python-version:
76
+ - "3.13"
77
+ permissions:
78
+ contents: read
79
+ services:
80
+ postgres:
81
+ image: postgres:16
82
+ env:
83
+ POSTGRES_USER: execsql
84
+ POSTGRES_PASSWORD: execsql
85
+ POSTGRES_DB: execsql_test
86
+ ports:
87
+ - 5432:5432
88
+ options: >-
89
+ --health-cmd pg_isready
90
+ --health-interval 10s
91
+ --health-timeout 5s
92
+ --health-retries 5
93
+ mysql:
94
+ image: mysql:8
95
+ env:
96
+ MYSQL_ROOT_PASSWORD: root
97
+ MYSQL_USER: execsql
98
+ MYSQL_PASSWORD: execsql
99
+ MYSQL_DATABASE: execsql_test
100
+ ports:
101
+ - 3306:3306
102
+ options: >-
103
+ --health-cmd "mysqladmin ping"
104
+ --health-interval 10s
105
+ --health-timeout 5s
106
+ --health-retries 5
107
+ steps:
108
+ - name: Check out repository code
109
+ uses: actions/checkout@v4
110
+ - name: Setup Python ${{ matrix.python-version }}
111
+ uses: actions/setup-python@v5
112
+ with:
113
+ python-version: ${{ matrix.python-version }}
114
+ - name: Install dependencies
115
+ run: |
116
+ python -m pip install --upgrade pip
117
+ python -m pip install ".[dev,postgres,mysql]"
118
+ - name: Run integration tests
119
+ run: |
120
+ python -m pytest tests/integration/test_postgres.py tests/integration/test_mysql.py -v --override-ini="addopts="
121
+
70
122
  build:
71
123
  name: Build distribution 📦
72
124
  runs-on: ubuntu-latest
73
- needs: [tests]
125
+ needs: [tests, integration-tests]
74
126
  if: startsWith(github.ref, 'refs/tags/v')
75
127
  permissions:
76
128
  contents: read
@@ -13,6 +13,10 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.4.1] - 2026-03-30
17
+
18
+ ______________________________________________________________________
19
+
16
20
  ## [2.4.0] - 2026-03-30
17
21
 
18
22
  ### Changed
@@ -29,11 +33,19 @@ ______________________________________________________________________
29
33
 
30
34
  - Python 3.14 support — added to CI matrix, tox environments, and PyPI classifiers.
31
35
  - `formats` extra included in `dev` dependencies so ODS/Excel/Jinja2 tests run without manual installation.
36
+ - PostgreSQL integration tests (9 tests) — full lifecycle via `--dsn` connection strings.
37
+ - MySQL/MariaDB integration tests (9 tests, 1 xfail for pre-existing import adapter bug).
38
+ - `docker-compose.yml` for local PostgreSQL and MySQL test databases.
39
+ - CI integration test job with GitHub Actions services (PostgreSQL 16, MySQL 8).
32
40
  - Roadmap items in `templates/README.md` for integrating execsql-compare and execsql-upsert documentation into the main docs site.
33
41
 
34
42
  ### Fixed
35
43
 
36
44
  - Fix odfpy import — `import of` corrected to `import odf as of` in `exporters/ods.py` and test skip guards. ODS export was broken since the modular refactor.
45
+ - Pass `--dsn` password through to all database backends (MySQL, SQL Server, Oracle, Firebird, DSN). Previously only PostgreSQL received the password from connection strings.
46
+ - Fix importer error reporting — `exception_info()` (returns tuple) replaced with `exception_desc()` (returns string) in 6 call sites across `importers/base.py`, `importers/csv.py`, and `importers/feather.py`. This caused `AttributeError: 'tuple' has no attribute 'replace'` on any import failure.
47
+ - Map Python encoding names to MySQL charset names in `LOAD DATA LOCAL INFILE` (e.g., `utf-8` → `utf8mb4`). Previously caused `Unknown character set` errors on MySQL imports.
48
+ - `--dsn` now overrides conf-file connection settings (server, database, user, port). Previously conf-file values took precedence, silently ignoring the DSN.
37
49
 
38
50
  ______________________________________________________________________
39
51
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.4.0
3
+ Version: 2.4.1
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: Repository, https://github.com/geocoug/execsql
6
6
  Project-URL: Issues, https://github.com/geocoug/execsql/issues
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.4.0"
7
+ version = "2.4.1"
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" }
@@ -155,7 +155,7 @@ skip-magic-trailing-comma = false
155
155
  line-ending = "auto"
156
156
 
157
157
  [tool.bumpversion]
158
- current_version = "2.4.0"
158
+ current_version = "2.4.1"
159
159
  commit = true
160
160
  commit_args = "--no-verify"
161
161
  tag = true
@@ -141,18 +141,19 @@ def _run(
141
141
  raise SystemExit(1)
142
142
  db_type = db_type or parsed_dsn["db_type"]
143
143
  conf.db_type = db_type
144
- if parsed_dsn["server"] and not conf.server:
144
+ # DSN values override conf-file values — the CLI flag is explicit.
145
+ if parsed_dsn["server"]:
145
146
  conf.server = parsed_dsn["server"]
146
- if parsed_dsn["db"] and not conf.db:
147
+ if parsed_dsn["db"]:
147
148
  conf.db = parsed_dsn["db"]
148
- if parsed_dsn["db_file"] and not conf.db_file:
149
+ if parsed_dsn["db_file"]:
149
150
  conf.db_file = parsed_dsn["db_file"]
150
- if parsed_dsn["user"] and not user:
151
+ if parsed_dsn["user"]:
151
152
  user = parsed_dsn["user"]
152
153
  if parsed_dsn["password"]:
153
154
  conf.db_password = parsed_dsn["password"]
154
155
  conf.passwd_prompt = False
155
- if parsed_dsn["port"] and not port:
156
+ if parsed_dsn["port"]:
156
157
  port = parsed_dsn["port"]
157
158
 
158
159
  # Apply CLI options over config-file values
@@ -538,6 +539,7 @@ def _connect_initial_db(conf: ConfigData):
538
539
  pw_needed=conf.passwd_prompt,
539
540
  port=conf.port,
540
541
  encoding=conf.db_encoding,
542
+ password=getattr(conf, "db_password", None),
541
543
  )
542
544
  elif conf.db_type == "l":
543
545
  if conf.db_file is None:
@@ -553,6 +555,7 @@ def _connect_initial_db(conf: ConfigData):
553
555
  pw_needed=conf.passwd_prompt,
554
556
  port=conf.port,
555
557
  encoding=conf.db_encoding,
558
+ password=getattr(conf, "db_password", None),
556
559
  )
557
560
  elif conf.db_type == "k":
558
561
  if conf.db_file is None:
@@ -568,6 +571,7 @@ def _connect_initial_db(conf: ConfigData):
568
571
  pw_needed=conf.passwd_prompt,
569
572
  port=conf.port,
570
573
  encoding=conf.db_encoding,
574
+ password=getattr(conf, "db_password", None),
571
575
  )
572
576
  elif conf.db_type == "f":
573
577
  return db_Firebird(
@@ -577,6 +581,7 @@ def _connect_initial_db(conf: ConfigData):
577
581
  pw_needed=conf.passwd_prompt,
578
582
  port=conf.port,
579
583
  encoding=conf.db_encoding,
584
+ password=getattr(conf, "db_password", None),
580
585
  )
581
586
  elif conf.db_type == "d":
582
587
  return db_Dsn(
@@ -584,6 +589,7 @@ def _connect_initial_db(conf: ConfigData):
584
589
  user=conf.username,
585
590
  pw_needed=conf.passwd_prompt,
586
591
  encoding=conf.db_encoding,
592
+ password=getattr(conf, "db_password", None),
587
593
  )
588
594
  else:
589
595
  from execsql.utils.errors import fatal_error
@@ -93,9 +93,10 @@ def db_SqlServer(
93
93
  pw_needed: bool = True,
94
94
  port: int | None = None,
95
95
  encoding: str | None = None,
96
+ password: str | None = None,
96
97
  ) -> SqlServerDatabase:
97
98
  """Open a Microsoft SQL Server connection via pyodbc."""
98
- return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding)
99
+ return SqlServerDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
99
100
 
100
101
 
101
102
  def db_MySQL(
@@ -105,9 +106,10 @@ def db_MySQL(
105
106
  pw_needed: bool = True,
106
107
  port: int | None = None,
107
108
  encoding: str | None = None,
109
+ password: str | None = None,
108
110
  ) -> MySQLDatabase:
109
111
  """Open a MySQL or MariaDB connection via pymysql."""
110
- return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding)
112
+ return MySQLDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
111
113
 
112
114
 
113
115
  def db_DuckDB(
@@ -136,9 +138,10 @@ def db_Oracle(
136
138
  pw_needed: bool = True,
137
139
  port: int | None = None,
138
140
  encoding: str | None = None,
141
+ password: str | None = None,
139
142
  ) -> OracleDatabase:
140
143
  """Open an Oracle database connection via cx_Oracle (python-oracledb)."""
141
- return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding)
144
+ return OracleDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
142
145
 
143
146
 
144
147
  def db_Firebird(
@@ -148,9 +151,10 @@ def db_Firebird(
148
151
  pw_needed: bool = True,
149
152
  port: int | None = None,
150
153
  encoding: str | None = None,
154
+ password: str | None = None,
151
155
  ) -> FirebirdDatabase:
152
156
  """Open a Firebird database connection via the firebird-driver package."""
153
- return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding)
157
+ return FirebirdDatabase(server_name, database_name, user, pw_needed, port, encoding, password=password)
154
158
 
155
159
 
156
160
  def db_Dsn(
@@ -158,6 +162,7 @@ def db_Dsn(
158
162
  user: str | None = None,
159
163
  pw_needed: bool = True,
160
164
  encoding: str | None = None,
165
+ password: str | None = None,
161
166
  ) -> DsnDatabase:
162
167
  """Open a connection to any ODBC data source registered under *dsn_name*."""
163
- return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding)
168
+ return DsnDatabase(dsn_name=dsn_name, user_name=user, need_passwd=pw_needed, encoding=encoding, password=password)
@@ -18,6 +18,18 @@ import execsql.state as _state
18
18
 
19
19
  __all__ = ["MySQLDatabase"]
20
20
 
21
+ # Map Python encoding names to MySQL CHARACTER SET names for LOAD DATA INFILE.
22
+ _PYTHON_TO_MYSQL_CHARSET: dict[str, str] = {
23
+ "utf-8": "utf8mb4",
24
+ "utf8": "utf8mb4",
25
+ "latin-1": "latin1",
26
+ "iso-8859-1": "latin1",
27
+ "iso8859-1": "latin1",
28
+ "ascii": "ascii",
29
+ "cp1252": "cp1252",
30
+ "windows-1252": "cp1252",
31
+ }
32
+
21
33
 
22
34
  class MySQLDatabase(Database):
23
35
  """MySQL and MariaDB adapter using the pymysql package."""
@@ -190,7 +202,8 @@ class MySQLDatabase(Database):
190
202
  ):
191
203
  import_sql = f"load data local infile '{csv_file_obj.csvfname}' into table {sq_name}"
192
204
  if csv_file_obj.encoding:
193
- import_sql = f"{import_sql} character set {csv_file_obj.encoding}"
205
+ charset = _PYTHON_TO_MYSQL_CHARSET.get(csv_file_obj.encoding.lower(), csv_file_obj.encoding)
206
+ import_sql = f"{import_sql} character set {charset}"
194
207
  if csv_file_obj.delimiter or csv_file_obj.quotechar:
195
208
  import_sql = import_sql + " columns"
196
209
  if csv_file_obj.delimiter:
@@ -28,7 +28,7 @@ def import_data_table(
28
28
  hdrs: list[str],
29
29
  data: list[Any],
30
30
  ) -> None:
31
- from execsql.utils.errors import exception_info
31
+ from execsql.utils.errors import exception_desc
32
32
 
33
33
  conf = _state.conf
34
34
  if any(x is None or len(x.strip()) == 0 for x in hdrs):
@@ -91,7 +91,7 @@ def import_data_table(
91
91
  raise ErrInfo(
92
92
  type="db",
93
93
  command_text=sql,
94
- exception_msg=exception_info(),
94
+ exception_msg=exception_desc(),
95
95
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
96
96
  ) from e
97
97
  table_cols = db.table_columns(tablename, schemaname)
@@ -111,4 +111,4 @@ def import_data_table(
111
111
  except ErrInfo:
112
112
  raise
113
113
  except Exception as e:
114
- raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_info()) from e
114
+ raise ErrInfo("db", "Call to populate_table when importing data", exception_msg=exception_desc()) from e
@@ -32,7 +32,7 @@ def importtable(
32
32
  encoding: str | None = None,
33
33
  junk_header_lines: int = 0,
34
34
  ) -> None:
35
- from execsql.utils.errors import exception_info
35
+ from execsql.utils.errors import exception_desc
36
36
 
37
37
  conf = _state.conf
38
38
  if not Path(filename).is_file():
@@ -66,7 +66,7 @@ def importtable(
66
66
  raise ErrInfo(
67
67
  type="db",
68
68
  command_text=sql,
69
- exception_msg=exception_info(),
69
+ exception_msg=exception_desc(),
70
70
  other_msg=f"Could not create new table ({tablename}) for IMPORT metacommand",
71
71
  ) from e
72
72
  else:
@@ -91,7 +91,7 @@ def importtable(
91
91
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
92
92
  raise ErrInfo(
93
93
  "exception",
94
- exception_msg=exception_info(),
94
+ exception_msg=exception_desc(),
95
95
  other_msg=f"Can't import tabular file ({filename}) to table ({fq_tablename})",
96
96
  ) from e
97
97
  inf.close()
@@ -104,7 +104,7 @@ def importfile(
104
104
  columname: str,
105
105
  filename: str,
106
106
  ) -> None:
107
- from execsql.utils.errors import exception_info
107
+ from execsql.utils.errors import exception_desc
108
108
 
109
109
  if schemaname is not None:
110
110
  if not db.table_exists(tablename, schemaname):
@@ -127,6 +127,6 @@ def importfile(
127
127
  fq_tablename = db.schema_qualified_table_name(schemaname, tablename)
128
128
  raise ErrInfo(
129
129
  "exception",
130
- exception_msg=exception_info(),
130
+ exception_msg=exception_desc(),
131
131
  other_msg=f"Can't import file ({filename}) to table ({fq_tablename})",
132
132
  ) from e
@@ -24,14 +24,14 @@ def import_feather(
24
24
  filename: str,
25
25
  is_new: Any,
26
26
  ) -> None:
27
- from execsql.utils.errors import exception_info
27
+ from execsql.utils.errors import exception_desc
28
28
 
29
29
  try:
30
30
  import polars as pl
31
31
  except Exception as e:
32
32
  raise ErrInfo(
33
33
  "exception",
34
- exception_msg=exception_info(),
34
+ exception_msg=exception_desc(),
35
35
  other_msg="The polars Python library must be installed to import data from the Feather format.",
36
36
  ) from e
37
37
  df = pl.read_ipc(filename)
@@ -47,14 +47,14 @@ def import_parquet(
47
47
  filename: str,
48
48
  is_new: Any,
49
49
  ) -> None:
50
- from execsql.utils.errors import exception_info
50
+ from execsql.utils.errors import exception_desc
51
51
 
52
52
  try:
53
53
  import polars as pl
54
54
  except Exception as e:
55
55
  raise ErrInfo(
56
56
  "exception",
57
- exception_msg=exception_info(),
57
+ exception_msg=exception_desc(),
58
58
  other_msg="The polars Python library must be installed to import data from the Parquet format.",
59
59
  ) from e
60
60
  df = pl.read_parquet(filename)
@@ -0,0 +1,36 @@
1
+ """Shared helpers for integration tests.
2
+
3
+ Each backend-specific test module provides its own ``_write_conf`` and
4
+ verification helpers. The helpers here are backend-agnostic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import subprocess
10
+ import sys
11
+ import textwrap
12
+
13
+
14
+ def write_script(tmp_path, sql_text, name="test_script.sql"):
15
+ """Write a .sql script file into *tmp_path*."""
16
+ script = tmp_path / name
17
+ script.write_text(textwrap.dedent(sql_text))
18
+ return script
19
+
20
+
21
+ def run_execsql(tmp_path, script_path, extra_args=None, timeout=30):
22
+ """Run execsql on the given script via subprocess.
23
+
24
+ Returns the completed process. The working directory is set to *tmp_path*
25
+ so that execsql.conf is picked up automatically.
26
+ """
27
+ cmd = [sys.executable, "-m", "execsql", str(script_path)]
28
+ if extra_args:
29
+ cmd.extend(extra_args)
30
+ return subprocess.run(
31
+ cmd,
32
+ cwd=str(tmp_path),
33
+ capture_output=True,
34
+ text=True,
35
+ timeout=timeout,
36
+ )
@@ -0,0 +1,169 @@
1
+ """Tests for --dsn / --connection-string CLI flag interaction with execsql.conf.
2
+
3
+ Verifies that --dsn overrides conf-file connection settings, and that
4
+ the two can coexist (DSN provides connection, conf provides encoding/options).
5
+ Uses SQLite since it requires no external services.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import sqlite3
11
+ import subprocess
12
+ import sys
13
+ import textwrap
14
+
15
+ from tests.integration.conftest import write_script
16
+
17
+
18
+ def _run_execsql(tmp_path, script_path, dsn=None, extra_args=None, timeout=30):
19
+ """Run execsql with optional --dsn flag."""
20
+ cmd = [sys.executable, "-m", "execsql"]
21
+ if dsn:
22
+ cmd.extend(["--dsn", dsn])
23
+ cmd.append(str(script_path))
24
+ if extra_args:
25
+ cmd.extend(extra_args)
26
+ return subprocess.run(
27
+ cmd,
28
+ cwd=str(tmp_path),
29
+ capture_output=True,
30
+ text=True,
31
+ timeout=timeout,
32
+ )
33
+
34
+
35
+ def _write_conf(tmp_path, content):
36
+ """Write an execsql.conf with arbitrary content."""
37
+ conf = tmp_path / "execsql.conf"
38
+ conf.write_text(textwrap.dedent(content))
39
+ return conf
40
+
41
+
42
+ class TestDsnOverridesConf:
43
+ """--dsn flag should override connection settings from execsql.conf."""
44
+
45
+ def test_dsn_overrides_conf_db_file(self, tmp_path):
46
+ """DSN database path takes precedence over conf db_file."""
47
+ dsn_db = tmp_path / "dsn.db"
48
+ conf_db = tmp_path / "conf.db"
49
+
50
+ # Conf points to conf.db
51
+ _write_conf(
52
+ tmp_path,
53
+ f"""\
54
+ [connect]
55
+ db_type = l
56
+ db_file = {conf_db}
57
+ new_db = yes
58
+ password_prompt = no
59
+ """,
60
+ )
61
+
62
+ script = write_script(
63
+ tmp_path,
64
+ """\
65
+ CREATE TABLE t (id INTEGER);
66
+ INSERT INTO t VALUES (1);
67
+ """,
68
+ )
69
+
70
+ # DSN points to dsn.db — should win
71
+ result = _run_execsql(tmp_path, script, dsn=f"sqlite:///{dsn_db}")
72
+ assert result.returncode == 0, f"stderr: {result.stderr}"
73
+
74
+ # Data should be in dsn.db, not conf.db
75
+ assert dsn_db.exists(), "DSN database was not created"
76
+ conn = sqlite3.connect(str(dsn_db))
77
+ rows = conn.execute("SELECT id FROM t").fetchall()
78
+ conn.close()
79
+ assert rows == [(1,)]
80
+
81
+ def test_dsn_without_conf_file(self, tmp_path):
82
+ """DSN works even when no execsql.conf exists."""
83
+ # Use a relative path — cwd is tmp_path, so test.db lands there.
84
+ # Conf with new_db=yes is needed so SQLite creates the file.
85
+ _write_conf(
86
+ tmp_path,
87
+ """\
88
+ [connect]
89
+ new_db = yes
90
+ """,
91
+ )
92
+
93
+ script = write_script(
94
+ tmp_path,
95
+ """\
96
+ CREATE TABLE t (val TEXT);
97
+ INSERT INTO t VALUES ('hello');
98
+ """,
99
+ )
100
+
101
+ result = _run_execsql(tmp_path, script, dsn="sqlite:///test.db")
102
+ assert result.returncode == 0, f"stderr: {result.stderr}"
103
+
104
+ db_path = tmp_path / "test.db"
105
+ conn = sqlite3.connect(str(db_path))
106
+ rows = conn.execute("SELECT val FROM t").fetchall()
107
+ conn.close()
108
+ assert rows == [("hello",)]
109
+
110
+ def test_conf_encoding_used_with_dsn_connection(self, tmp_path):
111
+ """Conf file encoding settings are respected even when --dsn provides the connection."""
112
+ _write_conf(
113
+ tmp_path,
114
+ """\
115
+ [connect]
116
+ new_db = yes
117
+
118
+ [encoding]
119
+ script = utf-8
120
+ output = utf-8
121
+ """,
122
+ )
123
+
124
+ csv_path = tmp_path / "out.csv"
125
+ script = write_script(
126
+ tmp_path,
127
+ f"""\
128
+ CREATE TABLE t (id INTEGER, name TEXT);
129
+ INSERT INTO t VALUES (1, 'Alice');
130
+
131
+ -- !x! EXPORT QUERY << SELECT id, name FROM t; >> TO {csv_path} AS CSV
132
+ """,
133
+ )
134
+
135
+ result = _run_execsql(tmp_path, script, dsn="sqlite:///test.db")
136
+ assert result.returncode == 0, f"stderr: {result.stderr}"
137
+ assert csv_path.exists()
138
+
139
+ def test_dsn_db_type_overrides_conf_db_type(self, tmp_path):
140
+ """DSN scheme determines db_type even if conf specifies a different type."""
141
+ # Conf says PostgreSQL, but DSN says SQLite — DSN should win
142
+ _write_conf(
143
+ tmp_path,
144
+ """\
145
+ [connect]
146
+ db_type = p
147
+ server = nonexistent
148
+ database = fake
149
+ new_db = yes
150
+ password_prompt = no
151
+ """,
152
+ )
153
+
154
+ script = write_script(
155
+ tmp_path,
156
+ """\
157
+ CREATE TABLE t (id INTEGER);
158
+ INSERT INTO t VALUES (42);
159
+ """,
160
+ )
161
+
162
+ result = _run_execsql(tmp_path, script, dsn="sqlite:///test.db")
163
+ assert result.returncode == 0, f"stderr: {result.stderr}"
164
+
165
+ db_path = tmp_path / "test.db"
166
+ conn = sqlite3.connect(str(db_path))
167
+ rows = conn.execute("SELECT id FROM t").fetchall()
168
+ conn.close()
169
+ assert rows == [(42,)]