execsql2 2.15.2__tar.gz → 2.15.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 (297) hide show
  1. {execsql2-2.15.2 → execsql2-2.15.5}/CHANGELOG.md +24 -0
  2. {execsql2-2.15.2 → execsql2-2.15.5}/PKG-INFO +11 -2
  3. {execsql2-2.15.2 → execsql2-2.15.5}/README.md +3 -1
  4. {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/divergence.md +1 -0
  5. {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/security.md +2 -2
  6. {execsql2-2.15.2 → execsql2-2.15.5}/pyproject.toml +6 -2
  7. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/datetime.py +12 -0
  8. execsql2-2.15.5/tests/importers/test_csv_edge_cases.py +209 -0
  9. execsql2-2.15.5/tests/test_debug_repl.py +555 -0
  10. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_format.py +119 -0
  11. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_parser.py +79 -0
  12. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_datetime.py +15 -0
  13. {execsql2-2.15.2 → execsql2-2.15.5}/uv.lock +64 -2
  14. execsql2-2.15.2/CLAUDE.md +0 -56
  15. {execsql2-2.15.2 → execsql2-2.15.5}/.github/ISSUE_TEMPLATE/bug_report.md +0 -0
  16. {execsql2-2.15.2 → execsql2-2.15.5}/.github/ISSUE_TEMPLATE/feature_request.md +0 -0
  17. {execsql2-2.15.2 → execsql2-2.15.5}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  18. {execsql2-2.15.2 → execsql2-2.15.5}/.github/workflows/ci-cd.yml +0 -0
  19. {execsql2-2.15.2 → execsql2-2.15.5}/.gitignore +0 -0
  20. {execsql2-2.15.2 → execsql2-2.15.5}/.pre-commit-config.yaml +0 -0
  21. {execsql2-2.15.2 → execsql2-2.15.5}/.pre-commit-hooks.yaml +0 -0
  22. {execsql2-2.15.2 → execsql2-2.15.5}/.python-version +0 -0
  23. {execsql2-2.15.2 → execsql2-2.15.5}/.readthedocs.yaml +0 -0
  24. {execsql2-2.15.2 → execsql2-2.15.5}/CONTRIBUTING.md +0 -0
  25. {execsql2-2.15.2 → execsql2-2.15.5}/LICENSE.txt +0 -0
  26. {execsql2-2.15.2 → execsql2-2.15.5}/NOTICE +0 -0
  27. {execsql2-2.15.2 → execsql2-2.15.5}/SECURITY.md +0 -0
  28. {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/contributors.md +0 -0
  29. {execsql2-2.15.2 → execsql2-2.15.5}/docs/about/copyright.md +0 -0
  30. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/cli.md +0 -0
  31. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/db.md +0 -0
  32. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/exporters.md +0 -0
  33. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/importers.md +0 -0
  34. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/index.md +0 -0
  35. {execsql2-2.15.2 → execsql2-2.15.5}/docs/api/metacommands.md +0 -0
  36. {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_db_adapters.md +0 -0
  37. {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_exporters.md +0 -0
  38. {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_importers.md +0 -0
  39. {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/adding_metacommands.md +0 -0
  40. {execsql2-2.15.2 → execsql2-2.15.5}/docs/dev/architecture.md +0 -0
  41. {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/installation.md +0 -0
  42. {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/requirements.md +0 -0
  43. {execsql2-2.15.2 → execsql2-2.15.5}/docs/getting-started/syntax.md +0 -0
  44. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/debugging.md +0 -0
  45. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/documentation.md +0 -0
  46. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/encoding.md +0 -0
  47. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/examples.md +0 -0
  48. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/formatter.md +0 -0
  49. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/logging.md +0 -0
  50. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/sql_syntax.md +0 -0
  51. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/usage.md +0 -0
  52. {execsql2-2.15.2 → execsql2-2.15.5}/docs/guides/using_scripts.md +0 -0
  53. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/Compare_planets.png +0 -0
  54. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/actions.png +0 -0
  55. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/actions2.png +0 -0
  56. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/checkboxes.png +0 -0
  57. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/connect.b64 +0 -0
  58. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/connect.png +0 -0
  59. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/create_conf.png +0 -0
  60. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/data_error1_screenshot.jpg +0 -0
  61. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/entry_form.png +0 -0
  62. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/execsql_console.png +0 -0
  63. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/execsql_logo_01.png +0 -0
  64. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/fatals.png +0 -0
  65. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/logo_small.png +0 -0
  66. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal.png +0 -0
  67. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal_sm.b64 +0 -0
  68. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/pause_terminal_sm.png +0 -0
  69. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/prompt_compare.png +0 -0
  70. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/set_build_commands.jpg +0 -0
  71. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unit_conversions.b64 +0 -0
  72. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unit_conversions_029.png +0 -0
  73. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/unmatched.png +0 -0
  74. {execsql2-2.15.2 → execsql2-2.15.5}/docs/images/vim_execsql_highlight.png +0 -0
  75. {execsql2-2.15.2 → execsql2-2.15.5}/docs/index.md +0 -0
  76. {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/configuration.md +0 -0
  77. {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/metacommands.md +0 -0
  78. {execsql2-2.15.2 → execsql2-2.15.5}/docs/reference/substitution_vars.md +0 -0
  79. {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/README.md +0 -0
  80. {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/package.json +0 -0
  81. {execsql2-2.15.2 → execsql2-2.15.5}/extras/vscode-execsql/syntaxes/execsql.tmLanguage.json +0 -0
  82. {execsql2-2.15.2 → execsql2-2.15.5}/justfile +0 -0
  83. {execsql2-2.15.2 → execsql2-2.15.5}/scripts/generate_vscode_grammar.py +0 -0
  84. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/__init__.py +0 -0
  85. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/__main__.py +0 -0
  86. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/__init__.py +0 -0
  87. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/dsn.py +0 -0
  88. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/help.py +0 -0
  89. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/lint.py +0 -0
  90. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/cli/run.py +0 -0
  91. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/config.py +0 -0
  92. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/__init__.py +0 -0
  93. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/access.py +0 -0
  94. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/base.py +0 -0
  95. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/dsn.py +0 -0
  96. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/duckdb.py +0 -0
  97. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/factory.py +0 -0
  98. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/firebird.py +0 -0
  99. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/mysql.py +0 -0
  100. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/oracle.py +0 -0
  101. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/postgres.py +0 -0
  102. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/sqlite.py +0 -0
  103. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/db/sqlserver.py +0 -0
  104. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/debug/__init__.py +0 -0
  105. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/debug/repl.py +0 -0
  106. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exceptions.py +0 -0
  107. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/__init__.py +0 -0
  108. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/base.py +0 -0
  109. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/delimited.py +0 -0
  110. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/duckdb.py +0 -0
  111. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/feather.py +0 -0
  112. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/html.py +0 -0
  113. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/json.py +0 -0
  114. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/latex.py +0 -0
  115. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/markdown.py +0 -0
  116. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/ods.py +0 -0
  117. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/parquet.py +0 -0
  118. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/pretty.py +0 -0
  119. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/protocol.py +0 -0
  120. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/raw.py +0 -0
  121. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/sqlite.py +0 -0
  122. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/templates.py +0 -0
  123. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/values.py +0 -0
  124. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xls.py +0 -0
  125. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xlsx.py +0 -0
  126. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/xml.py +0 -0
  127. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/yaml.py +0 -0
  128. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/exporters/zip.py +0 -0
  129. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/format.py +0 -0
  130. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/__init__.py +0 -0
  131. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/base.py +0 -0
  132. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/console.py +0 -0
  133. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/desktop.py +0 -0
  134. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/gui/tui.py +0 -0
  135. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/__init__.py +0 -0
  136. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/base.py +0 -0
  137. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/csv.py +0 -0
  138. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/feather.py +0 -0
  139. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/json.py +0 -0
  140. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/ods.py +0 -0
  141. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/importers/xls.py +0 -0
  142. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/__init__.py +0 -0
  143. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/conditions.py +0 -0
  144. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/connect.py +0 -0
  145. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/control.py +0 -0
  146. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/data.py +0 -0
  147. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/debug.py +0 -0
  148. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/dispatch.py +0 -0
  149. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io.py +0 -0
  150. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_export.py +0 -0
  151. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_fileops.py +0 -0
  152. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_import.py +0 -0
  153. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/io_write.py +0 -0
  154. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/prompt.py +0 -0
  155. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/script_ext.py +0 -0
  156. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/system.py +0 -0
  157. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/metacommands/upsert.py +0 -0
  158. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/models.py +0 -0
  159. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/parser.py +0 -0
  160. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/py.typed +0 -0
  161. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/__init__.py +0 -0
  162. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/control.py +0 -0
  163. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/engine.py +0 -0
  164. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/script/variables.py +0 -0
  165. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/state.py +0 -0
  166. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/types.py +0 -0
  167. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/__init__.py +0 -0
  168. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/auth.py +0 -0
  169. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/crypto.py +0 -0
  170. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/errors.py +0 -0
  171. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/fileio.py +0 -0
  172. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/gui.py +0 -0
  173. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/mail.py +0 -0
  174. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/numeric.py +0 -0
  175. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/regex.py +0 -0
  176. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/strings.py +0 -0
  177. {execsql2-2.15.2 → execsql2-2.15.5}/src/execsql/utils/timer.py +0 -0
  178. {execsql2-2.15.2 → execsql2-2.15.5}/templates/README.md +0 -0
  179. {execsql2-2.15.2 → execsql2-2.15.5}/templates/config_settings.sqlite +0 -0
  180. {execsql2-2.15.2 → execsql2-2.15.5}/templates/example_config_prompt.sql +0 -0
  181. {execsql2-2.15.2 → execsql2-2.15.5}/templates/execsql.conf +0 -0
  182. {execsql2-2.15.2 → execsql2-2.15.5}/templates/make_config_db.sql +0 -0
  183. {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_compare.sql +0 -0
  184. {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_glossary.sql +0 -0
  185. {execsql2-2.15.2 → execsql2-2.15.5}/templates/md_upsert.sql +0 -0
  186. {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_compare.sql +0 -0
  187. {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_glossary.sql +0 -0
  188. {execsql2-2.15.2 → execsql2-2.15.5}/templates/pg_upsert.sql +0 -0
  189. {execsql2-2.15.2 → execsql2-2.15.5}/templates/script_template.sql +0 -0
  190. {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_compare.sql +0 -0
  191. {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_glossary.sql +0 -0
  192. {execsql2-2.15.2 → execsql2-2.15.5}/templates/ss_upsert.sql +0 -0
  193. {execsql2-2.15.2 → execsql2-2.15.5}/tests/__init__.py +0 -0
  194. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/__init__.py +0 -0
  195. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli.py +0 -0
  196. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli_e2e.py +0 -0
  197. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_cli_run.py +0 -0
  198. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_lint.py +0 -0
  199. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_ping.py +0 -0
  200. {execsql2-2.15.2 → execsql2-2.15.5}/tests/cli/test_profile.py +0 -0
  201. {execsql2-2.15.2 → execsql2-2.15.5}/tests/conftest.py +0 -0
  202. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/__init__.py +0 -0
  203. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_base.py +0 -0
  204. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_duckdb.py +0 -0
  205. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_factory.py +0 -0
  206. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_postgres.py +0 -0
  207. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_sqlite.py +0 -0
  208. {execsql2-2.15.2 → execsql2-2.15.5}/tests/db/test_sqlite_extra.py +0 -0
  209. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/__init__.py +0 -0
  210. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_base.py +0 -0
  211. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_db.py +0 -0
  212. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_delimited.py +0 -0
  213. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_duckdb_exporter.py +0 -0
  214. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_exporters.py +0 -0
  215. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_feather.py +0 -0
  216. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_html_extended.py +0 -0
  217. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_html_latex.py +0 -0
  218. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_json.py +0 -0
  219. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_json_extended.py +0 -0
  220. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_latex_extended.py +0 -0
  221. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_markdown.py +0 -0
  222. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_ods.py +0 -0
  223. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_parquet.py +0 -0
  224. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_pretty_extended.py +0 -0
  225. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_raw_extended.py +0 -0
  226. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_sqlite_exporter.py +0 -0
  227. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_templates.py +0 -0
  228. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_templates_extended.py +0 -0
  229. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_values_extended.py +0 -0
  230. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xls_xlsx.py +0 -0
  231. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xlsx.py +0 -0
  232. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_xml.py +0 -0
  233. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_yaml.py +0 -0
  234. {execsql2-2.15.2 → execsql2-2.15.5}/tests/exporters/test_zip.py +0 -0
  235. {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/__init__.py +0 -0
  236. {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_backends.py +0 -0
  237. {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_compare_stats.py +0 -0
  238. {execsql2-2.15.2 → execsql2-2.15.5}/tests/gui/test_compute_row_diffs.py +0 -0
  239. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/__init__.py +0 -0
  240. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_base_extended.py +0 -0
  241. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_csv_importer.py +0 -0
  242. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_feather_importer.py +0 -0
  243. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_json_importer.py +0 -0
  244. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_ods_importer.py +0 -0
  245. {execsql2-2.15.2 → execsql2-2.15.5}/tests/importers/test_xls_importer.py +0 -0
  246. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/__init__.py +0 -0
  247. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/conftest.py +0 -0
  248. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_dsn.py +0 -0
  249. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_duckdb.py +0 -0
  250. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_mysql.py +0 -0
  251. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_postgres.py +0 -0
  252. {execsql2-2.15.2 → execsql2-2.15.5}/tests/integration/test_sqlite.py +0 -0
  253. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/__init__.py +0 -0
  254. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_assert.py +0 -0
  255. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_breakpoint.py +0 -0
  256. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_connect.py +0 -0
  257. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_io_export.py +0 -0
  258. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_io_import.py +0 -0
  259. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands.py +0 -0
  260. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_connect.py +0 -0
  261. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_data.py +0 -0
  262. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_extended.py +0 -0
  263. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_fileops_extra.py +0 -0
  264. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_io.py +0 -0
  265. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_io_write_extra.py +0 -0
  266. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_script_ext.py +0 -0
  267. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_system.py +0 -0
  268. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_metacommands_system_extra.py +0 -0
  269. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_pg_upsert.py +0 -0
  270. {execsql2-2.15.2 → execsql2-2.15.5}/tests/metacommands/test_row_count.py +0 -0
  271. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config.py +0 -0
  272. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config_data.py +0 -0
  273. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_config_extended.py +0 -0
  274. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_engine.py +0 -0
  275. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_error_messages.py +0 -0
  276. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_exceptions.py +0 -0
  277. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_mail.py +0 -0
  278. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_models.py +0 -0
  279. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_package.py +0 -0
  280. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_registry.py +0 -0
  281. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_script.py +0 -0
  282. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_state.py +0 -0
  283. {execsql2-2.15.2 → execsql2-2.15.5}/tests/test_types.py +0 -0
  284. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/__init__.py +0 -0
  285. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_auth.py +0 -0
  286. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_auth_extra.py +0 -0
  287. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_crypto.py +0 -0
  288. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_errors.py +0 -0
  289. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_errors_extra.py +0 -0
  290. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_fileio.py +0 -0
  291. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_fileio_extra.py +0 -0
  292. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_numeric.py +0 -0
  293. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_regex.py +0 -0
  294. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_strings.py +0 -0
  295. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_timer.py +0 -0
  296. {execsql2-2.15.2 → execsql2-2.15.5}/tests/utils/test_timer_extra.py +0 -0
  297. {execsql2-2.15.2 → execsql2-2.15.5}/zensical.toml +0 -0
@@ -13,6 +13,30 @@ ______________________________________________________________________
13
13
 
14
14
  ______________________________________________________________________
15
15
 
16
+ ## [2.15.5] - 2026-04-15
17
+
18
+ ### Fixed
19
+
20
+ - `DT_Timestamp` type inference no longer claims time-only values (e.g. `13:15:45`). `dateutil.parser.parse()` silently fills in today's date for bare time strings, causing `DT_Timestamp` to match before `DT_Time` and generating PostgreSQL `InvalidDatetimeFormat` errors on CSV import. Time-only strings are now rejected by `parse_datetime()`.
21
+
22
+ ______________________________________________________________________
23
+
24
+ ## [2.15.4] - 2026-04-15
25
+
26
+ ### Fixed
27
+
28
+ - Fixed typo in `test_latin1_encoding` test data (`calf\xe9` → `calf\xe9`) that caused assertion failure on Windows CI.
29
+
30
+ ______________________________________________________________________
31
+
32
+ ## [2.15.3] - 2026-04-15
33
+
34
+ ### Added
35
+
36
+ - New optional dependency extras `auth-plaintext` and `auth-encrypted` for headless Linux keyring backends. `pip install execsql2[auth-plaintext]` installs `keyring` + `keyrings.alt`; `pip install execsql2[auth-encrypted]` adds `pycryptodome` for the encrypted file backend.
37
+
38
+ ______________________________________________________________________
39
+
16
40
  ## [2.15.2] - 2026-04-14
17
41
 
18
42
  ### Changed
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: execsql2
3
- Version: 2.15.2
3
+ Version: 2.15.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
@@ -69,6 +69,13 @@ Requires-Dist: pymysql; extra == 'all-db'
69
69
  Requires-Dist: pyodbc; extra == 'all-db'
70
70
  Provides-Extra: auth
71
71
  Requires-Dist: keyring; extra == 'auth'
72
+ Provides-Extra: auth-encrypted
73
+ Requires-Dist: keyring; extra == 'auth-encrypted'
74
+ Requires-Dist: keyrings-alt; extra == 'auth-encrypted'
75
+ Requires-Dist: pycryptodome; extra == 'auth-encrypted'
76
+ Provides-Extra: auth-plaintext
77
+ Requires-Dist: keyring; extra == 'auth-plaintext'
78
+ Requires-Dist: keyrings-alt; extra == 'auth-plaintext'
72
79
  Provides-Extra: dev
73
80
  Requires-Dist: build>=1.2.2.post1; extra == 'dev'
74
81
  Requires-Dist: bump-my-version>=1.2.7; extra == 'dev'
@@ -167,7 +174,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
167
174
 
168
175
  # Feature bundles
169
176
  pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
170
- pip install execsql2[auth] # OS keyring integration
177
+ pip install execsql2[auth] # OS keyring integration
178
+ pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
179
+ pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
171
180
 
172
181
  # Convenience
173
182
  pip install execsql2[all-db] # All database drivers
@@ -52,7 +52,9 @@ pip install execsql2[odbc] # ODBC DSN (pyodbc)
52
52
 
53
53
  # Feature bundles
54
54
  pip install execsql2[formats] # ODS, Excel, Jinja2, Feather, Parquet, HDF5
55
- pip install execsql2[auth] # OS keyring integration
55
+ pip install execsql2[auth] # OS keyring integration
56
+ pip install execsql2[auth-plaintext] # Keyring + plaintext file backend (headless Linux)
57
+ pip install execsql2[auth-encrypted] # Keyring + encrypted file backend (headless Linux)
56
58
 
57
59
  # Convenience
58
60
  pip install execsql2[all-db] # All database drivers
@@ -253,6 +253,7 @@ These are behavioral changes driven by security or correctness issues in the ups
253
253
  | `WriteHooks.write_err()` crash on empty string | `strval[-1]` raised `IndexError` on empty input. Fixed to use `str.endswith()`. Inherited from upstream. |
254
254
  | `NumericParser` division by zero | `NumericAstNode.eval()` raised unhandled `ZeroDivisionError`. Now raises `NumericParserError` with a clear message. Inherited from upstream. |
255
255
  | `CondAstNode.eval()` could return `None` | Missing fallthrough for unknown node types silently returned `None`. Now raises `CondParserError`. Inherited from upstream. |
256
+ | `DT_Timestamp` claims time-only values | `dateutil.parser.parse()` silently fills in today's date for bare time strings like `"13:15:45"`, so `DT_Timestamp` matched before `DT_Time` in the inference order. `parse_datetime()` now rejects time-only strings. Inherited from upstream. |
256
257
 
257
258
  ______________________________________________________________________
258
259
 
@@ -52,7 +52,7 @@ To disable keyring integration, set `use_keyring = No` in the `[connect]` sectio
52
52
  Passwords are stored encrypted on disk. Requires a master password the first time keyring is used per session.
53
53
 
54
54
  ```bash
55
- pip install keyrings.alt pycryptodome
55
+ pip install execsql2[auth-encrypted]
56
56
  mkdir -p ~/.config/python_keyring
57
57
  cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
58
58
  [backend]
@@ -67,7 +67,7 @@ The encrypted keyring file is stored at `~/.local/share/python_keyring/crypted_p
67
67
  Passwords are stored in plain text on disk. No master password is needed — execsql will never prompt for a password after the first successful entry.
68
68
 
69
69
  ```bash
70
- pip install keyrings.alt
70
+ pip install execsql2[auth-plaintext]
71
71
  mkdir -p ~/.config/python_keyring
72
72
  cat > ~/.config/python_keyring/keyringrc.cfg << 'EOF'
73
73
  [backend]
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "execsql2"
7
- version = "2.15.2"
7
+ version = "2.15.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" }
@@ -59,6 +59,8 @@ odbc = ["pyodbc"]
59
59
  # Feature bundles
60
60
  formats = ["odfpy", "xlrd", "openpyxl", "Jinja2", "polars", "tables", "PyYAML"]
61
61
  auth = ["keyring"]
62
+ auth-plaintext = ["keyring", "keyrings.alt"]
63
+ auth-encrypted = ["keyring", "keyrings.alt", "pycryptodome"]
62
64
  upsert = ["pg-upsert>=1.21.0"]
63
65
  # Convenience groups
64
66
  all-db = [
@@ -162,7 +164,7 @@ skip-magic-trailing-comma = false
162
164
  line-ending = "auto"
163
165
 
164
166
  [tool.bumpversion]
165
- current_version = "2.15.2"
167
+ current_version = "2.15.5"
166
168
  commit = true
167
169
  commit_args = "--no-verify"
168
170
  tag = true
@@ -231,6 +233,8 @@ hel = "hel"
231
233
  fo = "fo"
232
234
  # odfpy package imports as 'odf', not 'of'
233
235
  odf = "odf"
236
+ # Latin-1 test data: b"caf\xe9" decodes to "café"
237
+ caf = "caf"
234
238
 
235
239
  [tool.tox]
236
240
  required = ["tox-uv"]
@@ -25,12 +25,22 @@ __all__ = ["parse_datetime", "parse_datetimetz"]
25
25
  # misidentified as timestamps.
26
26
  _NUMERIC_ONLY = re.compile(r"^[+-]?\d+\.?\d*$")
27
27
 
28
+ # Match time-only strings like "13:15:45", "9:30", "1:15:45.123", "09:30 AM".
29
+ # dateutil parses these by filling in today's date, which causes DT_Timestamp
30
+ # to claim the column before DT_Time gets a chance.
31
+ _TIME_ONLY = re.compile(r"^\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?\s*(?:[AaPp][Mm])?$")
32
+
28
33
 
29
34
  def _looks_numeric(s: str) -> bool:
30
35
  """Return True if *s* is a bare number that should not be parsed as a date."""
31
36
  return bool(_NUMERIC_ONLY.match(s.strip()))
32
37
 
33
38
 
39
+ def _looks_time_only(s: str) -> bool:
40
+ """Return True if *s* is a time-only string (no date component)."""
41
+ return bool(_TIME_ONLY.match(s.strip()))
42
+
43
+
34
44
  def parse_datetime(datestr: Any) -> datetime.datetime | None:
35
45
  """Parse a date/time string into a :class:`datetime.datetime`.
36
46
 
@@ -53,6 +63,8 @@ def parse_datetime(datestr: Any) -> datetime.datetime | None:
53
63
  return None
54
64
  if _looks_numeric(datestr):
55
65
  return None
66
+ if _looks_time_only(datestr):
67
+ return None
56
68
  try:
57
69
  return _dateutil_parser.parse(datestr)
58
70
  except (ValueError, OverflowError, TypeError):
@@ -0,0 +1,209 @@
1
+ """
2
+ Edge-case tests for CSV/delimited import.
3
+
4
+ Covers scenarios not exercised by test_csv_importer.py:
5
+ - Empty files
6
+ - Header-only files (no data rows)
7
+ - Files with BOM markers (UTF-8 BOM)
8
+ - Inconsistent column counts across rows
9
+ - Unicode/special characters in data
10
+ - Very long field values
11
+ - Files with only whitespace rows
12
+ - Encoding parameter override
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import pytest
18
+
19
+ from execsql.db.sqlite import SQLiteDatabase
20
+ from execsql.exceptions import ErrInfo
21
+ from execsql.importers.csv import importtable
22
+
23
+
24
+ # ---------------------------------------------------------------------------
25
+ # Extra conf attributes required by the importers
26
+ # ---------------------------------------------------------------------------
27
+
28
+
29
+ @pytest.fixture(autouse=True)
30
+ def importer_conf(minimal_conf):
31
+ minimal_conf.del_empty_cols = False
32
+ minimal_conf.create_col_hdrs = False
33
+ minimal_conf.clean_col_hdrs = False
34
+ minimal_conf.trim_col_hdrs = "none"
35
+ minimal_conf.fold_col_hdrs = "no"
36
+ minimal_conf.dedup_col_hdrs = False
37
+ minimal_conf.import_encoding = "utf-8"
38
+ minimal_conf.import_common_cols_only = False
39
+ minimal_conf.quote_all_text = False
40
+ minimal_conf.scan_lines = 50
41
+ minimal_conf.empty_rows = True
42
+ minimal_conf.import_row_buffer = 1000
43
+ minimal_conf.import_progress_interval = 0
44
+ minimal_conf.show_progress = False
45
+ yield minimal_conf
46
+
47
+
48
+ @pytest.fixture
49
+ def db(tmp_path):
50
+ path = str(tmp_path / "test.db")
51
+ d = SQLiteDatabase(path)
52
+ yield d
53
+ d.close()
54
+
55
+
56
+ # ===========================================================================
57
+ # Empty and minimal files
58
+ # ===========================================================================
59
+
60
+
61
+ class TestEmptyAndMinimalFiles:
62
+ def test_header_only_no_data_rows(self, db, tmp_path):
63
+ """A CSV with headers but zero data rows should create a table with no rows."""
64
+ csv = tmp_path / "empty_data.csv"
65
+ csv.write_text("id,name,value\n", encoding="utf-8")
66
+ importtable(db, None, "empty_tbl", str(csv), is_new=1)
67
+ _, rows = db.select_data("SELECT * FROM empty_tbl;")
68
+ assert len(rows) == 0
69
+
70
+ def test_single_column_single_row(self, db, tmp_path):
71
+ csv = tmp_path / "single.csv"
72
+ csv.write_text("x\n42\n", encoding="utf-8")
73
+ importtable(db, None, "single_tbl", str(csv), is_new=1)
74
+ _, rows = db.select_data("SELECT x FROM single_tbl;")
75
+ assert len(rows) == 1
76
+
77
+ def test_nonexistent_file_raises(self, db, tmp_path):
78
+ with pytest.raises(ErrInfo, match="Non-existent file"):
79
+ importtable(db, None, "t", str(tmp_path / "ghost.csv"), is_new=1)
80
+
81
+
82
+ # ===========================================================================
83
+ # BOM handling
84
+ # ===========================================================================
85
+
86
+
87
+ class TestBOMHandling:
88
+ def test_utf8_bom_import(self, db, tmp_path):
89
+ """UTF-8 BOM should not corrupt the first column header."""
90
+ csv = tmp_path / "bom.csv"
91
+ csv.write_bytes(b"\xef\xbb\xbfid,name\n1,Alice\n2,Bob\n")
92
+ importtable(db, None, "bom_tbl", str(csv), is_new=1, encoding="utf-8-sig")
93
+ cols = db.table_columns("bom_tbl")
94
+ # First column should be 'id', not '\ufeffid'
95
+ assert cols[0] == "id"
96
+ _, rows = db.select_data("SELECT id FROM bom_tbl ORDER BY id;")
97
+ assert len(rows) == 2
98
+
99
+
100
+ # ===========================================================================
101
+ # Unicode and special characters
102
+ # ===========================================================================
103
+
104
+
105
+ class TestUnicodeData:
106
+ def test_unicode_values(self, db, tmp_path):
107
+ csv = tmp_path / "unicode.csv"
108
+ csv.write_text('id,name\n1,"café résumé"\n2,"日本語"\n', encoding="utf-8")
109
+ importtable(db, None, "uni_tbl", str(csv), is_new=1)
110
+ _, rows = db.select_data("SELECT name FROM uni_tbl ORDER BY id;")
111
+ assert rows[0][0] == "café résumé"
112
+ assert rows[1][0] == "日本語"
113
+
114
+ def test_embedded_commas_in_quoted_field(self, db, tmp_path):
115
+ csv = tmp_path / "commas.csv"
116
+ csv.write_text('id,description\n1,"hello, world"\n', encoding="utf-8")
117
+ importtable(db, None, "comma_tbl", str(csv), is_new=1)
118
+ _, rows = db.select_data("SELECT description FROM comma_tbl;")
119
+ assert rows[0][0] == "hello, world"
120
+
121
+ def test_embedded_newlines_in_quoted_field(self, db, tmp_path):
122
+ """Embedded newlines in quoted CSV fields are not preserved by the
123
+ execsql CSV reader — the newline is treated as a row break. This test
124
+ documents the current behavior: the import succeeds but the value is
125
+ split across rows or truncated."""
126
+ csv = tmp_path / "newlines.csv"
127
+ csv.write_text('id,val\n1,"line1\nline2"\n', encoding="utf-8")
128
+ # The import should not crash — whether the newline is preserved
129
+ # depends on the parser path (csv module fast-path vs character parser).
130
+ try:
131
+ importtable(db, None, "nl_tbl", str(csv), is_new=1)
132
+ except ErrInfo:
133
+ pass # Some parser paths may raise on the malformed row; that's acceptable
134
+
135
+ def test_embedded_quotes_doubled(self, db, tmp_path):
136
+ csv = tmp_path / "quotes.csv"
137
+ csv.write_text('id,description\n1,"he said ""hello"""\n', encoding="utf-8")
138
+ importtable(db, None, "qt_tbl", str(csv), is_new=1)
139
+ _, rows = db.select_data("SELECT description FROM qt_tbl;")
140
+ assert rows[0][0] == 'he said "hello"'
141
+
142
+
143
+ # ===========================================================================
144
+ # Long fields
145
+ # ===========================================================================
146
+
147
+
148
+ class TestLongFields:
149
+ def test_long_string_value(self, db, tmp_path):
150
+ """Values over 255 chars should be imported as TEXT type."""
151
+ long_val = "x" * 500
152
+ csv = tmp_path / "long.csv"
153
+ csv.write_text(f"id,data\n1,{long_val}\n", encoding="utf-8")
154
+ importtable(db, None, "long_tbl", str(csv), is_new=1)
155
+ _, rows = db.select_data("SELECT data FROM long_tbl;")
156
+ assert len(rows[0][0]) == 500
157
+
158
+
159
+ # ===========================================================================
160
+ # Encoding parameter
161
+ # ===========================================================================
162
+
163
+
164
+ class TestEncodingOverride:
165
+ def test_latin1_encoding(self, db, tmp_path):
166
+ csv = tmp_path / "latin1.csv"
167
+ csv.write_bytes(b"id,name\n1,caf\xe9\n")
168
+ importtable(db, None, "lat_tbl", str(csv), is_new=1, encoding="latin-1")
169
+ _, rows = db.select_data("SELECT name FROM lat_tbl;")
170
+ assert rows[0][0] == "café"
171
+
172
+
173
+ # ===========================================================================
174
+ # Junk header lines
175
+ # ===========================================================================
176
+
177
+
178
+ class TestJunkHeaderLines:
179
+ def test_skip_junk_lines(self, db, tmp_path):
180
+ csv = tmp_path / "junk.csv"
181
+ csv.write_text("Report Title\nGenerated: today\nid,name\n1,Alice\n", encoding="utf-8")
182
+ importtable(db, None, "junk_tbl", str(csv), is_new=1, junk_header_lines=2)
183
+ _, rows = db.select_data("SELECT name FROM junk_tbl;")
184
+ assert rows[0][0] == "Alice"
185
+
186
+
187
+ # ===========================================================================
188
+ # Append to existing table with type compatibility
189
+ # ===========================================================================
190
+
191
+
192
+ class TestAppendEdgeCases:
193
+ def test_append_with_null_values(self, db, tmp_path):
194
+ db.execute("CREATE TABLE t (id INTEGER, name TEXT);")
195
+ db.execute("INSERT INTO t VALUES (1, 'existing');")
196
+ db.commit()
197
+ csv = tmp_path / "nulls.csv"
198
+ csv.write_text("id,name\n2,\n", encoding="utf-8")
199
+ importtable(db, None, "t", str(csv), is_new=False)
200
+ _, rows = db.select_data("SELECT name FROM t WHERE id = 2;")
201
+ assert len(rows) == 1
202
+ # With empty_strings=True (default), empty CSV fields are imported as empty strings
203
+ assert rows[0][0] == "" or rows[0][0] is None
204
+
205
+ def test_append_nonexistent_table(self, db, tmp_path):
206
+ csv = tmp_path / "data.csv"
207
+ csv.write_text("x\n1\n", encoding="utf-8")
208
+ with pytest.raises(ErrInfo, match="Non-existent table"):
209
+ importtable(db, None, "no_such", str(csv), is_new=False)