pyLodStorage 0.19.2__tar.gz → 0.19.4__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 (175) hide show
  1. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/PKG-INFO +3 -1
  2. pylodstorage-0.19.4/lodstorage/__init__.py +1 -0
  3. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/duckdb_query.py +9 -1
  4. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/mysql.py +17 -5
  5. pylodstorage-0.19.4/lodstorage/postgresql.py +152 -0
  6. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/query_cmd.py +5 -0
  7. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/querymain.py +7 -1
  8. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sql.py +10 -1
  9. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sql_backend.py +20 -3
  10. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/version.py +1 -1
  11. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/pyproject.toml +4 -0
  12. pylodstorage-0.19.4/tests/test_postgresql.py +120 -0
  13. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_queries.py +31 -0
  14. pylodstorage-0.19.2/lodstorage/__init__.py +0 -1
  15. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.github/workflows/build.yml +0 -0
  16. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.github/workflows/upload-to-pypi.yml +0 -0
  17. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.gitignore +0 -0
  18. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.project +0 -0
  19. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.pydevproject +0 -0
  20. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.readthedocs.yml +0 -0
  21. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/.travis.yml +0 -0
  22. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/AGENTS.md +0 -0
  23. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/CITATION.cff +0 -0
  24. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/LICENSE +0 -0
  25. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/README.md +0 -0
  26. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/index.md +0 -0
  27. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/Makefile +0 -0
  28. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/environment.pickle +0 -0
  29. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/index.doctree +0 -0
  30. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/lodstorage.doctree +0 -0
  31. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/sampledata.doctree +0 -0
  32. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/setup.doctree +0 -0
  33. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/doctrees/tests.doctree +0 -0
  34. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/.buildinfo +0 -0
  35. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/index.html +0 -0
  36. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/csv.html +0 -0
  37. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/docstring_parser.html +0 -0
  38. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/entity.html +0 -0
  39. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/jsonable.html +0 -0
  40. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/jsonpicklemixin.html +0 -0
  41. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/linkml.html +0 -0
  42. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/linkml_gen.html +0 -0
  43. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/lod.html +0 -0
  44. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/mwTable.html +0 -0
  45. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/plot.html +0 -0
  46. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/query.html +0 -0
  47. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/querymain.html +0 -0
  48. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/rdf.html +0 -0
  49. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/sample.html +0 -0
  50. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/sample2.html +0 -0
  51. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/schema.html +0 -0
  52. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/sparql.html +0 -0
  53. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/sql.html +0 -0
  54. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/storageconfig.html +0 -0
  55. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/sync.html +0 -0
  56. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/tabulateCounter.html +0 -0
  57. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/trulytabular.html +0 -0
  58. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/uml.html +0 -0
  59. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/version.html +0 -0
  60. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/xml.html +0 -0
  61. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/yamlable.html +0 -0
  62. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_modules/lodstorage/yamlablemixin.html +0 -0
  63. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_sources/index.rst.txt +0 -0
  64. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_sources/lodstorage.rst.txt +0 -0
  65. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_sources/sampledata.rst.txt +0 -0
  66. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_sources/setup.rst.txt +0 -0
  67. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_sources/tests.rst.txt +0 -0
  68. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/alabaster.css +0 -0
  69. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/basic.css +0 -0
  70. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/custom.css +0 -0
  71. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/doctools.js +0 -0
  72. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/documentation_options.js +0 -0
  73. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/file.png +0 -0
  74. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/language_data.js +0 -0
  75. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/minus.png +0 -0
  76. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/plus.png +0 -0
  77. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/pygments.css +0 -0
  78. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/searchtools.js +0 -0
  79. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/_static/sphinx_highlight.js +0 -0
  80. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/genindex.html +0 -0
  81. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/index.html +0 -0
  82. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/lodstorage.html +0 -0
  83. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/objects.inv +0 -0
  84. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/py-modindex.html +0 -0
  85. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/sampledata.html +0 -0
  86. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/search.html +0 -0
  87. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/searchindex.js +0 -0
  88. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/setup.html +0 -0
  89. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/_build/html/tests.html +0 -0
  90. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/conf.py +0 -0
  91. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/index.rst +0 -0
  92. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/lodstorage.rst +0 -0
  93. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/make.bat +0 -0
  94. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/sampledata.rst +0 -0
  95. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/setup.rst +0 -0
  96. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/docs/source/tests.rst +0 -0
  97. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/docstring_parser.py +0 -0
  98. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/exception_handler.py +0 -0
  99. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/linkml.py +0 -0
  100. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/linkml_gen.py +0 -0
  101. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/lod.py +0 -0
  102. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/lod2xml.py +0 -0
  103. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/lod_csv.py +0 -0
  104. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/multilang_querymanager.py +0 -0
  105. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/mwTable.py +0 -0
  106. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/params.py +0 -0
  107. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/plot.py +0 -0
  108. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/prefix_config.py +0 -0
  109. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/prefixes.py +0 -0
  110. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/profiler.py +0 -0
  111. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/query.py +0 -0
  112. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/rate_limiter.py +0 -0
  113. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/rdf.py +0 -0
  114. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/rdf_format.py +0 -0
  115. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sample2.py +0 -0
  116. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/schema.py +0 -0
  117. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sparql.py +0 -0
  118. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sqlite_api.py +0 -0
  119. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/storageconfig.py +0 -0
  120. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/sync.py +0 -0
  121. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/tabulateCounter.py +0 -0
  122. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/uml.py +0 -0
  123. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/lodstorage/yaml_path.py +0 -0
  124. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/mkdocs.yml +0 -0
  125. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/__init__.py +0 -0
  126. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/dblp.yaml +0 -0
  127. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/endpoints.yaml +0 -0
  128. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/endpoints_qlever.yaml +0 -0
  129. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/example.ttl +0 -0
  130. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/formats.yaml +0 -0
  131. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/got.yaml +0 -0
  132. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/prefixes.yaml +0 -0
  133. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries/queries_olympics.yaml +0 -0
  134. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries/queries_tiny_wikidatasubsets_claude4_5.yaml +0 -0
  135. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries/queries_tiny_wikidatasubsets_gemini3pro.yaml +0 -0
  136. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries/queries_tiny_wikidatasubsets_gpt5.yaml +0 -0
  137. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries/queries_tiny_wikidatasubsets_grok4.yaml +0 -0
  138. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/queries.yaml +0 -0
  139. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/royals.yaml +0 -0
  140. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/royals_linkml_schema.yaml +0 -0
  141. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/scholia.yaml +0 -0
  142. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/trulytabular.yaml +0 -0
  143. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/wf.yaml +0 -0
  144. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/sampledata/wikidata.yaml +0 -0
  145. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/blackisort +0 -0
  146. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/doc +0 -0
  147. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/install +0 -0
  148. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/installAndTest +0 -0
  149. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/jena +0 -0
  150. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/release +0 -0
  151. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/sparqlquery +0 -0
  152. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/test +0 -0
  153. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/scripts/vsenv +0 -0
  154. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/__init__.py +0 -0
  155. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/action_stats.py +0 -0
  156. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/basetest.py +0 -0
  157. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/endpoint_test.py +0 -0
  158. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_Plot.py +0 -0
  159. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_csv.py +0 -0
  160. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_docstring_parser.py +0 -0
  161. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_linkml.py +0 -0
  162. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_linkml_schema.py +0 -0
  163. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_lod.py +0 -0
  164. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_multilang_querymanager.py +0 -0
  165. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_params.py +0 -0
  166. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_prefix_config.py +0 -0
  167. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_prefixes.py +0 -0
  168. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_query_yaml.py +0 -0
  169. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_rdf.py +0 -0
  170. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_samples.py +0 -0
  171. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_sql_backend.py +0 -0
  172. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_sqlite3.py +0 -0
  173. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_sync.py +0 -0
  174. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_tabulate.py +0 -0
  175. {pylodstorage-0.19.2 → pylodstorage-0.19.4}/tests/test_yaml_path.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pyLodStorage
3
- Version: 0.19.2
3
+ Version: 0.19.4
4
4
  Summary: python List of Dict (Table) Storage library
5
5
  Project-URL: Home, https://github.com/WolfgangFahl/pyLodStorage
6
6
  Project-URL: Documentation, http://wiki.bitplan.com/index.php/PyLoDStorage
@@ -35,6 +35,8 @@ Provides-Extra: duckdb
35
35
  Requires-Dist: duckdb>=1.1.0; extra == 'duckdb'
36
36
  Provides-Extra: mysql
37
37
  Requires-Dist: pymysql>=1.1.1; extra == 'mysql'
38
+ Provides-Extra: postgresql
39
+ Requires-Dist: psycopg2-binary>=2.9.9; extra == 'postgresql'
38
40
  Provides-Extra: test
39
41
  Requires-Dist: green; extra == 'test'
40
42
  Description-Content-Type: text/markdown
@@ -0,0 +1 @@
1
+ __version__ = "0.19.4"
@@ -40,13 +40,15 @@ class DuckDBQuery:
40
40
  self.debug = debug
41
41
  self.con = duckdb.connect(self.path)
42
42
 
43
- def query(self, sql: str, params: Any = None) -> list[dict]:
43
+ def query(self, sql: str, params: Any = None, commit: bool = False) -> list[dict]:
44
44
  """
45
45
  Execute an SQL query and return all results eagerly.
46
46
 
47
47
  Args:
48
48
  sql: the SQL query string to execute
49
49
  params: optional positional parameters (list or tuple)
50
+ commit: if True, commit the connection after execution
51
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
50
52
 
51
53
  Returns:
52
54
  list of dicts, one per row
@@ -54,9 +56,15 @@ class DuckDBQuery:
54
56
  if self.debug:
55
57
  logging.debug(f"DuckDBQuery.query: {sql!r} params={params}")
56
58
  rel = self.con.execute(sql, params or [])
59
+ if rel.description is None:
60
+ if commit:
61
+ self.con.commit()
62
+ return []
57
63
  cols = [d[0] for d in rel.description]
58
64
  rows = rel.fetchall()
59
65
  result = [dict(zip(cols, row)) for row in rows]
66
+ if commit:
67
+ self.con.commit()
60
68
  return result
61
69
 
62
70
  def query_gen(self, sql: str, params: Any = None) -> Generator[dict, None, None]:
@@ -69,20 +69,28 @@ class MySqlQuery:
69
69
  decoded_record[key] = value
70
70
  return decoded_record
71
71
 
72
- def execute_sql_query(self, query: str) -> List[Dict[str, Any]]:
72
+ def execute_sql_query(
73
+ self, query: str, commit: bool = False
74
+ ) -> List[Dict[str, Any]]:
73
75
  """
74
76
  Executes an SQL query using the provided connection parameters.
75
77
 
76
78
  Args:
77
79
  query (str): The SQL query to execute.
78
- connection_params (dict): Database connection parameters.
80
+ commit (bool): if True, commit the connection before closing
81
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
79
82
 
80
83
  Returns:
81
84
  list: A list of dictionaries representing the query results.
82
85
  """
83
86
  connection, cursor = self.get_cursor(query)
84
87
  cursor.execute(query)
85
- raw_lod = cursor.fetchall()
88
+ if cursor.description is None:
89
+ raw_lod = []
90
+ else:
91
+ raw_lod = cursor.fetchall()
92
+ if commit:
93
+ connection.commit()
86
94
  connection.close()
87
95
  lod = []
88
96
  for raw_row in raw_lod:
@@ -108,18 +116,22 @@ class MySqlQuery:
108
116
  cursor.close()
109
117
  connection.close()
110
118
 
111
- def query(self, sql: str, params: Any = None) -> List[Dict[str, Any]]:
119
+ def query(
120
+ self, sql: str, params: Any = None, commit: bool = False
121
+ ) -> List[Dict[str, Any]]:
112
122
  """
113
123
  SQLBackend protocol alias for execute_sql_query.
114
124
 
115
125
  Args:
116
126
  sql: the SQL query to execute
117
127
  params: ignored (not yet supported at the pymysql layer)
128
+ commit: if True, commit the connection after execution
129
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
118
130
 
119
131
  Returns:
120
132
  list of dicts, one per row
121
133
  """
122
- return self.execute_sql_query(sql)
134
+ return self.execute_sql_query(sql, commit=commit)
123
135
 
124
136
  def query_gen(
125
137
  self, sql: str, params: Any = None
@@ -0,0 +1,152 @@
1
+ """
2
+ postgresql.py:
3
+
4
+ PostgreSQL support
5
+
6
+ """
7
+
8
+ import logging
9
+ from typing import Any, Dict, Generator, List
10
+
11
+ import psycopg2
12
+ import psycopg2.extras
13
+
14
+ from lodstorage.query import Endpoint
15
+
16
+
17
+ class PostgreSqlQuery:
18
+ """
19
+ A class to manage and execute PostgreSQL queries with optional debugging.
20
+
21
+ Attributes:
22
+ endpoint_info (Endpoint): endpoint configuration.
23
+ debug (bool): Flag to enable debugging.
24
+ """
25
+
26
+ def __init__(self, endpoint: Endpoint, debug: bool = False):
27
+ """
28
+ Initializes the PostgreSqlQuery class.
29
+
30
+ Args:
31
+ endpoint (Endpoint): endpoint configuration.
32
+ debug (bool): Flag to enable debugging.
33
+ """
34
+ self.db_params = {
35
+ "host": endpoint.host or "localhost",
36
+ "port": endpoint.port or 5432,
37
+ "user": endpoint.user or "postgres",
38
+ "password": endpoint.password,
39
+ "dbname": endpoint.database,
40
+ }
41
+
42
+ self.debug = debug
43
+
44
+ def get_cursor(self, query: str):
45
+ if self.debug:
46
+ logging.debug(f"Executing query: {query}")
47
+ logging.debug(f"With connection parameters: {self.db_params}")
48
+
49
+ connection = psycopg2.connect(**self.db_params)
50
+ cursor = connection.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
51
+ return connection, cursor
52
+
53
+ def decode_record(self, record: Dict[str, Any]) -> Dict[str, Any]:
54
+ """
55
+ Converts binary values to UTF-8 strings.
56
+
57
+ Args:
58
+ record (Dict[str, Any]): Raw database row data
59
+
60
+ Returns:
61
+ Dict[str, Any]: Data with binary values decoded to strings
62
+ """
63
+ decoded_record = {}
64
+ for key, value in record.items():
65
+ if isinstance(value, (bytes, memoryview)):
66
+ decoded_record[key] = (
67
+ bytes(value).decode("utf-8", errors="replace")
68
+ if isinstance(value, memoryview)
69
+ else value.decode("utf-8", errors="replace")
70
+ )
71
+ else:
72
+ decoded_record[key] = value
73
+ return decoded_record
74
+
75
+ def execute_sql_query(
76
+ self, query: str, commit: bool = False
77
+ ) -> List[Dict[str, Any]]:
78
+ """
79
+ Executes an SQL query using the provided connection parameters.
80
+
81
+ Args:
82
+ query (str): The SQL query to execute.
83
+ commit (bool): if True, commit the connection before closing
84
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
85
+
86
+ Returns:
87
+ list: A list of dictionaries representing the query results.
88
+ """
89
+ connection, cursor = self.get_cursor(query)
90
+ cursor.execute(query)
91
+ if cursor.description is None:
92
+ raw_lod = []
93
+ else:
94
+ raw_lod = cursor.fetchall()
95
+ if commit:
96
+ connection.commit()
97
+ connection.close()
98
+ lod = []
99
+ for raw_row in raw_lod:
100
+ row = self.decode_record(dict(raw_row))
101
+ lod.append(row)
102
+ return lod
103
+
104
+ def query_generator(self, query: str) -> Generator[Dict[str, Any], None, None]:
105
+ """
106
+ Generator for fetching records one by one from a SQL query.
107
+ """
108
+ connection, cursor = self.get_cursor(query)
109
+ try:
110
+ cursor.execute(query)
111
+ while True:
112
+ raw_record = cursor.fetchone()
113
+ if not raw_record:
114
+ break
115
+ record = self.decode_record(dict(raw_record))
116
+ yield record
117
+
118
+ finally:
119
+ cursor.close()
120
+ connection.close()
121
+
122
+ def query(
123
+ self, sql: str, params: Any = None, commit: bool = False
124
+ ) -> List[Dict[str, Any]]:
125
+ """
126
+ SQLBackend protocol alias for execute_sql_query.
127
+
128
+ Args:
129
+ sql: the SQL query to execute
130
+ params: ignored (not yet supported at the psycopg2 layer)
131
+ commit: if True, commit the connection after execution
132
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
133
+
134
+ Returns:
135
+ list of dicts, one per row
136
+ """
137
+ return self.execute_sql_query(sql, commit=commit)
138
+
139
+ def query_gen(
140
+ self, sql: str, params: Any = None
141
+ ) -> Generator[Dict[str, Any], None, None]:
142
+ """
143
+ SQLBackend protocol alias for query_generator.
144
+
145
+ Args:
146
+ sql: the SQL query to execute
147
+ params: ignored (not yet supported at the psycopg2 layer)
148
+
149
+ Yields:
150
+ one dict per row
151
+ """
152
+ return self.query_generator(sql)
@@ -136,6 +136,11 @@ class QueryCmd:
136
136
  xml = lod2xml.asXml()
137
137
  print(xml)
138
138
 
139
+ elif args.format is Format.raw:
140
+ for row in qlod:
141
+ values = list(row.values())
142
+ line = "\t".join(str(v) for v in values)
143
+ print(line)
139
144
  else:
140
145
  raise Exception(f"format {args.format} not supported yet")
141
146
 
@@ -96,7 +96,7 @@ class QueryMain(QueryCmd):
96
96
  qlod = sparql.queryAsListOfDicts(self.queryCode)
97
97
  elif args.language == "sql":
98
98
  backend = get_sql_backend(endpointConf, debug=args.debug)
99
- qlod = backend.query(self.queryCode)
99
+ qlod = backend.query(self.queryCode, commit=args.commit)
100
100
  else:
101
101
  raise Exception(f"language {args.language} not known/supported")
102
102
  self.format_output(qlod)
@@ -267,6 +267,12 @@ USAGE
267
267
  action="store_true",
268
268
  help="return the raw query result from the endpoint. (MIME type defined over -f or -m)",
269
269
  )
270
+ parser.add_argument(
271
+ "--commit",
272
+ action="store_true",
273
+ help="commit the SQL transaction after execution "
274
+ "(required for DDL/DML statements such as CREATE/INSERT/UPDATE)",
275
+ )
270
276
  parser.add_argument(
271
277
  "-V", "--version", action="version", version=program_version_message
272
278
  )
@@ -256,6 +256,11 @@ class SQLDB(object):
256
256
  query = cur.execute(sqlQuery, params)
257
257
  else:
258
258
  query = cur.execute(sqlQuery)
259
+ # description is None for statements that do not return rows
260
+ # (e.g. CREATE TABLE, INSERT, UPDATE, DELETE)
261
+ if query.description is None:
262
+ cur.close()
263
+ return
259
264
  colname = [d[0] for d in query.description]
260
265
  try:
261
266
  # loop over all rows
@@ -281,7 +286,7 @@ class SQLDB(object):
281
286
  """
282
287
  return self.queryGen(sql, params)
283
288
 
284
- def query(self, sql, params=None):
289
+ def query(self, sql, params=None, commit: bool = False):
285
290
  """
286
291
  run the given sql query and return a list of Dicts
287
292
 
@@ -289,6 +294,8 @@ class SQLDB(object):
289
294
 
290
295
  sql(string): the SQL query to be executed
291
296
  params(tuple): the query params, if any
297
+ commit(bool): if True, commit the connection after execution
298
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
292
299
 
293
300
  Returns:
294
301
  list: a list of Dicts
@@ -296,6 +303,8 @@ class SQLDB(object):
296
303
  resultList = []
297
304
  for record in self.queryGen(sql, params):
298
305
  resultList.append(record)
306
+ if commit:
307
+ self.c.commit()
299
308
  return resultList
300
309
 
301
310
  def queryAll(self, entityInfo, fixDates=True):
@@ -5,7 +5,7 @@ Unified SQL backend abstraction for pyLoDStorage.
5
5
 
6
6
  Defines the SQLBackend Protocol (duck-typed interface) and the
7
7
  get_sql_backend() factory that resolves the correct backend from
8
- an endpoint descriptor, hiding the jdbc:mysql and jdbc:duckdb
8
+ an endpoint descriptor, hiding the jdbc:mysql, jdbc:duckdb, and jdbc:postgresql
9
9
  routing details from all call sites.
10
10
 
11
11
  Created on 2026-02-21
@@ -27,6 +27,11 @@ try:
27
27
  except ImportError:
28
28
  DuckDBQuery = None # type: ignore
29
29
 
30
+ try:
31
+ from lodstorage.postgresql import PostgreSqlQuery
32
+ except ImportError:
33
+ PostgreSqlQuery = None # type: ignore
34
+
30
35
 
31
36
  @runtime_checkable
32
37
  class SQLBackend(Protocol):
@@ -40,15 +45,18 @@ class SQLBackend(Protocol):
40
45
  - lodstorage.sql.SQLDB (SQLite, always available)
41
46
  - lodstorage.mysql.MySqlQuery (MySQL/MariaDB, optional: pip install pyLodStorage[mysql])
42
47
  - lodstorage.duckdb_query.DuckDBQuery (DuckDB, optional: pip install pyLodStorage[duckdb])
48
+ - lodstorage.postgresql.PostgreSqlQuery (PostgreSQL, optional: pip install pyLodStorage[postgresql])
43
49
  """
44
50
 
45
- def query(self, sql: str, params: Any = None) -> list[dict]:
51
+ def query(self, sql: str, params: Any = None, commit: bool = False) -> list[dict]:
46
52
  """
47
53
  Execute an SQL query and return all results eagerly.
48
54
 
49
55
  Args:
50
56
  sql: the SQL query string to execute
51
57
  params: optional query parameters (support depends on backend)
58
+ commit: if True, commit the underlying connection after execution
59
+ (required for DDL/DML statements such as CREATE/INSERT/UPDATE)
52
60
 
53
61
  Returns:
54
62
  list of dicts, one per row
@@ -82,7 +90,7 @@ def get_sql_backend(endpoint, debug: bool = False) -> SQLBackend:
82
90
  debug: enable debug output on the returned backend
83
91
 
84
92
  Returns:
85
- SQLBackend — SQLDB, MySqlQuery, or DuckDBQuery depending on endpoint
93
+ SQLBackend — SQLDB, MySqlQuery, DuckDBQuery, or PostgreSqlQuery depending on endpoint
86
94
 
87
95
  Raises:
88
96
  Exception: if the requested backend's optional dependency is not installed
@@ -112,5 +120,14 @@ def get_sql_backend(endpoint, debug: bool = False) -> SQLBackend:
112
120
  backend = DuckDBQuery(endpoint=endpoint, debug=debug)
113
121
  return backend
114
122
 
123
+ if url.startswith("jdbc:postgresql"):
124
+ if PostgreSqlQuery is None:
125
+ raise Exception(
126
+ "PostgreSQL backend requested but psycopg2 is not installed. "
127
+ "Install it with: pip install pyLodStorage[postgresql]"
128
+ )
129
+ backend = PostgreSqlQuery(endpoint=endpoint, debug=debug)
130
+ return backend
131
+
115
132
  backend = SQLDB(dbname=url, debug=debug)
116
133
  return backend
@@ -15,5 +15,5 @@ class Version(object):
15
15
  name = "pylodstorage"
16
16
  version = lodstorage.__version__
17
17
  date = "2020-09-10"
18
- updated = "2026-02-28"
18
+ updated = "2026-05-17"
19
19
  description = "python List of Dict (Table) Storage library"
@@ -72,6 +72,10 @@ duckdb = [
72
72
  # https://pypi.org/project/duckdb/
73
73
  "duckdb>=1.1.0",
74
74
  ]
75
+ postgresql = [
76
+ # https://pypi.org/project/psycopg2-binary/
77
+ "psycopg2-binary>=2.9.9",
78
+ ]
75
79
  test = [
76
80
  "green",
77
81
  ]
@@ -0,0 +1,120 @@
1
+ """
2
+ test_postgresql.py
3
+
4
+ Tests for lodstorage.postgresql — PostgreSqlQuery with mocked psycopg2.
5
+
6
+ Created on 2026-03-22
7
+
8
+ @author: wf
9
+ """
10
+
11
+ import unittest
12
+ from unittest.mock import MagicMock, patch
13
+
14
+ from lodstorage.query import Endpoint
15
+ from lodstorage.sql_backend import PostgreSqlQuery, SQLBackend, get_sql_backend
16
+ from tests.basetest import Basetest
17
+
18
+ postgresql_available = PostgreSqlQuery is not None
19
+
20
+
21
+ class TestPostgreSqlQuery(Basetest):
22
+ """
23
+ Verify PostgreSqlQuery with mocked psycopg2.
24
+ """
25
+
26
+ def setUp(self, debug=False, profile=True):
27
+ Basetest.setUp(self, debug=debug, profile=profile)
28
+
29
+ def postgresql_endpoint(self) -> Endpoint:
30
+ """Return an Endpoint configured for PostgreSQL."""
31
+ ep = Endpoint()
32
+ ep.endpoint = "jdbc:postgresql"
33
+ ep.host = "127.0.0.1"
34
+ ep.port = 5432
35
+ ep.user = "postgres"
36
+ ep.password = "testpass"
37
+ ep.database = "template1"
38
+ return ep
39
+
40
+ @unittest.skipUnless(postgresql_available, "psycopg2 not installed")
41
+ def testProtocolConformance(self):
42
+ """PostgreSqlQuery must satisfy SQLBackend at runtime."""
43
+ with patch("lodstorage.postgresql.psycopg2"):
44
+ pq = PostgreSqlQuery(endpoint=self.postgresql_endpoint())
45
+ self.assertIsInstance(pq, SQLBackend)
46
+
47
+ @unittest.skipUnless(postgresql_available, "psycopg2 not installed")
48
+ def testFactoryRouting(self):
49
+ """Factory must route jdbc:postgresql Endpoint -> PostgreSqlQuery."""
50
+ backend = get_sql_backend(self.postgresql_endpoint())
51
+ self.assertIsInstance(backend, PostgreSqlQuery)
52
+
53
+ @unittest.skipUnless(postgresql_available, "psycopg2 not installed")
54
+ @patch("lodstorage.postgresql.psycopg2")
55
+ def testQuery(self, mock_psycopg2):
56
+ """query() returns a list of dicts from mocked cursor."""
57
+ mock_cursor = MagicMock()
58
+ mock_cursor.fetchall.return_value = [
59
+ {"datname": "template1"},
60
+ {"datname": "n8n"},
61
+ ]
62
+ mock_connection = MagicMock()
63
+ mock_psycopg2.connect.return_value = mock_connection
64
+ mock_connection.cursor.return_value = mock_cursor
65
+
66
+ pq = PostgreSqlQuery(endpoint=self.postgresql_endpoint())
67
+ rows = pq.query("SELECT datname FROM pg_database")
68
+
69
+ self.assertEqual(2, len(rows))
70
+ self.assertEqual("template1", rows[0]["datname"])
71
+ self.assertEqual("n8n", rows[1]["datname"])
72
+ mock_cursor.execute.assert_called_once_with("SELECT datname FROM pg_database")
73
+ mock_connection.close.assert_called_once()
74
+
75
+ @unittest.skipUnless(postgresql_available, "psycopg2 not installed")
76
+ @patch("lodstorage.postgresql.psycopg2")
77
+ def testQueryGen(self, mock_psycopg2):
78
+ """query_gen() yields rows one by one from mocked cursor."""
79
+ mock_cursor = MagicMock()
80
+ mock_cursor.fetchone.side_effect = [
81
+ {"datname": "template1"},
82
+ {"datname": "n8n"},
83
+ None,
84
+ ]
85
+ mock_connection = MagicMock()
86
+ mock_psycopg2.connect.return_value = mock_connection
87
+ mock_connection.cursor.return_value = mock_cursor
88
+
89
+ pq = PostgreSqlQuery(endpoint=self.postgresql_endpoint())
90
+ names = [r["datname"] for r in pq.query_gen("SELECT datname FROM pg_database")]
91
+
92
+ self.assertEqual(["template1", "n8n"], names)
93
+ mock_cursor.close.assert_called_once()
94
+ mock_connection.close.assert_called_once()
95
+
96
+ @unittest.skipUnless(postgresql_available, "psycopg2 not installed")
97
+ @patch("lodstorage.postgresql.psycopg2")
98
+ def testDecodeBytes(self, mock_psycopg2):
99
+ """Binary values should be decoded to UTF-8 strings."""
100
+ mock_cursor = MagicMock()
101
+ mock_cursor.fetchall.return_value = [
102
+ {"name": b"caf\xc3\xa9"},
103
+ ]
104
+ mock_connection = MagicMock()
105
+ mock_psycopg2.connect.return_value = mock_connection
106
+ mock_connection.cursor.return_value = mock_cursor
107
+
108
+ pq = PostgreSqlQuery(endpoint=self.postgresql_endpoint())
109
+ rows = pq.query("SELECT name FROM test")
110
+
111
+ self.assertEqual(1, len(rows))
112
+ self.assertEqual("café", rows[0]["name"])
113
+
114
+ def testFactoryRaisesWithoutPsycopg2(self):
115
+ """Factory must raise if psycopg2 is not installed."""
116
+ ep = self.postgresql_endpoint()
117
+ with patch("lodstorage.sql_backend.PostgreSqlQuery", None):
118
+ with self.assertRaises(Exception) as ctx:
119
+ get_sql_backend(ep)
120
+ self.assertIn("psycopg2", str(ctx.exception))
@@ -24,6 +24,7 @@ from lodstorage.query import (
24
24
  ValueFormatter,
25
25
  ValueFormatters,
26
26
  )
27
+ from lodstorage.query_cmd import QueryCmd
27
28
  from lodstorage.querymain import QueryMain
28
29
  from lodstorage.querymain import main as queryMain
29
30
  from lodstorage.sparql import SPARQL
@@ -705,6 +706,36 @@ determines the number of instances available in the OpenStreetMap for the placeT
705
706
  except Exception as ex:
706
707
  print(f"{query.title} at {endpointUrl} failed: {ex}")
707
708
 
709
+ def test_issue162_raw_format(self):
710
+ """
711
+ test raw format output
712
+
713
+ https://github.com/WolfgangFahl/pyLoDStorage/issues/162
714
+ """
715
+ debug = self.debug
716
+ qlod = [
717
+ {"grant": "GRANT USAGE ON *.* TO `test`@`%`"},
718
+ {"grant": "GRANT ALL PRIVILEGES ON `testdb`.* TO `test`@`%`"},
719
+ ]
720
+ args = Namespace(
721
+ debug=debug,
722
+ format=Format.raw,
723
+ )
724
+ query_cmd = QueryCmd.__new__(QueryCmd)
725
+ query_cmd.args = args
726
+ query_cmd.query = Query(name="test", query="SHOW GRANTS", lang="sql")
727
+ stdout = io.StringIO()
728
+ with redirect_stdout(stdout):
729
+ query_cmd.format_output(qlod)
730
+ result = stdout.getvalue()
731
+ if debug:
732
+ print(result)
733
+ self.assertIn("GRANT USAGE ON *.* TO `test`@`%`", result)
734
+ self.assertIn("GRANT ALL PRIVILEGES ON `testdb`.* TO `test`@`%`", result)
735
+ # raw format should not contain any table markup or quoting
736
+ self.assertNotIn("{|", result)
737
+ self.assertNotIn('"GRANT', result)
738
+
708
739
 
709
740
  class TestEndpoints(EndpointTest):
710
741
  """
@@ -1 +0,0 @@
1
- __version__ = "0.19.2"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes