├── .gitignore ├── .travis.yml ├── Dockerfile ├── LICENSE ├── README.md ├── __init__.py ├── archiving.svg ├── branch.svg ├── checkout.svg ├── checkout_pg.svg ├── checkout_pg_local.svg ├── commit.svg ├── commit_msg.ui ├── docs ├── contributors.rst ├── functionality.rst ├── globals.rst ├── images │ ├── CC-BY-SA-4-0.png │ ├── PostGIS-logo.png │ ├── archive_schemas.png │ ├── archiving.png │ ├── branch.png │ ├── branch_group.png │ ├── checkout.png │ ├── checkout_pg.png │ ├── checkout_pg_local.png │ ├── commit.png │ ├── commit_success.png │ ├── commit_ui.png │ ├── conflict_layer.png │ ├── conflict_warning.png │ ├── creating_mybranch.png │ ├── diff_mode_symbology.png │ ├── diff_mode_view.png │ ├── eha.jpg │ ├── empty_group.png │ ├── full_mode_view.png │ ├── groups_same_name.png │ ├── help.png │ ├── historization.png │ ├── historize.png │ ├── historize_warning.png │ ├── initial_db.png │ ├── late_by.png │ ├── late_by_warning.png │ ├── layers_not_same_db.png │ ├── merge.png │ ├── no_group_selected.png │ ├── oslandia_logo.png │ ├── pg_checkout.png │ ├── pipes_table.png │ ├── pipes_view.png │ ├── revisions_table.png │ ├── selected_features_local.png │ ├── selected_features_warning.png │ ├── sl_checkout.png │ ├── spatialite-logo.png │ ├── unversioned.png │ ├── unversioned_menu.png │ ├── update.png │ ├── uptodate.png │ ├── versioned.png │ ├── versioned_branch_menu.png │ ├── versioned_menu.png │ ├── view.png │ ├── view_dialog.png │ ├── view_dialog_diff_mode.png │ ├── working_copy.png │ ├── working_copy_pg.png │ └── working_copy_sl.png ├── index.rst ├── inner-workings.rst ├── introduction.rst ├── problem-handling.rst ├── requirements.rst └── spatialfiltering.rst ├── help.svg ├── historize.svg ├── merge.svg ├── metadata.txt ├── package.py ├── plugin.py ├── revision_dialog.ui ├── test ├── __init__.py ├── abbreviation_bug_test.py ├── archiving_test.py ├── bug_in_branch_after_commit_test.py ├── composite_primary_key_db.sql ├── constraints_test.py ├── create_db_test.sh ├── epanet_test_db.sql ├── epanet_test_db_uuid.sql ├── history_creation_test.py ├── issue287_pg_dump.sql ├── issue287_test.py ├── issue287_wc.sqlite ├── issue357_test.py ├── issue358_test.py ├── issue437_test.py ├── issue437_test_db.sql ├── issue485_test.py ├── issue486_test.py ├── issue_type_test.py ├── merge_branch_test.py ├── multiple_geometry_test.py ├── partial_checkout_test.py ├── plugin_test.py ├── posgres_working_copy_bug_in_conflict_test.py ├── posgres_working_copy_bug_in_view_test.py ├── posgres_working_copy_test.py ├── postgres_distant_test.py ├── postgres_distant_uuid_test.py ├── run_tests.sh ├── tests.py └── versioning_base_test.py ├── update.svg ├── versioningDB ├── __init__.py ├── constraints.py ├── postgresqlLocal.py ├── postgresqlServer.py ├── spatialite.py ├── sql │ └── historize.sql ├── utils.py ├── versioning.py └── versioningAbc.py └── view.svg /.gitignore: -------------------------------------------------------------------------------- 1 | # Extensions 2 | *.zip 3 | *.pyc 4 | .spyproject 5 | 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | 3 | services: 4 | - docker 5 | 6 | before_install: 7 | - docker build . -t qgis-versioning-test 8 | 9 | script: 10 | - docker run qgis-versioning-test 11 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM debian:sid 2 | 3 | RUN apt-get update \ 4 | && apt-get install -y \ 5 | gdal-bin \ 6 | libsqlite3-mod-spatialite \ 7 | postgresql-11-postgis-2.5 \ 8 | python3-psycopg2 \ 9 | qgis \ 10 | xvfb 11 | 12 | # to be able to connect locally 13 | RUN echo "host all all 127.0.0.1/32 trust" > /etc/postgresql/11/main/pg_hba.conf 14 | 15 | COPY . qgis-versioning 16 | WORKDIR qgis-versioning/test 17 | 18 | CMD ./run_tests.sh -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Versioning 2 | ========== 3 | 4 | Build and install the qgis plugin 5 | --------------------------------- 6 | 7 | cd 8 | git clone https://github.com/Oslandia/qgis-versioning.git 9 | cd qgis-versioning 10 | ./package.py # compresses all files into qgis_versioning.zip 11 | cd .qgis2/python/plugins/ 12 | mkdir qgis-versioning 13 | cd qgis-versioning 14 | # unzip contents of directory *qgis_versioning* found in qgis_versioning.zip 15 | 16 | If you have admin acces to a local postgres/postis server, you can run the regression tests: 17 | 18 | export QGIS_PREFIX_PATH=/path/to/your/qgis/installation 19 | export PYTHONPATH=$QGIS_DIR/python:$PYTHONPATH 20 | python3 tests.py 127.0.0.1 postgres -v 21 | 22 | And if you want to run only one regression test: 23 | 24 | export QGIS_PREFIX_PATH=/path/to/your/qgis/installation 25 | export PYTHONPATH=$QGIS_DIR/python:..:$PYTHONPATH 26 | python3 plugin_test.py 127.0.0.1 postgres 27 | 28 | 29 | Use the plugin in qgis 30 | ---------------------- 31 | 32 | Check that the plugin 'qgis-versioning' is activated in the plugin manager or install the versioning plugin directly in QGIS (Menu : Plugins = Manage plugins : Versioning). 33 | 34 | Load posgis layers from a scheme you want to version. 35 | 36 | Group postgis layers together. Select the group and click on the 'historize' button in the plugin toolbar (make sure the toolbar is displayed). The layers will be replaced by their view in the head revision 37 | 38 | Click on the group and then on the 'checkout' button. Choose a file to save your layers locally. 39 | 40 | Modify your layers. 41 | 42 | Click on the 'commit' icon. 43 | 44 | Documentation 45 | ======= 46 | 47 | For more information on this plugin, you can go on its plugin documentation site: http://qgis-versioning.readthedocs.io/en/latest/. You can also contribute to the source code by sending pull request or open issues if you have any comments or bug to report. 48 | 49 | See also this article describing why the plugin has been built and how : [GIS Open Source versioning tool for a multi-user Distributed Environment](http://www.gogeomatics.ca/magazine/gis-open-source-versioning-tool-part-1.htm) 50 | Cet article est aussi disponible en français : http://www.gogeomatics.ca/magazine/outil-de-versionnement-a-code-source-ouvert-partie-1.htm 51 | 52 | Credits 53 | ======= 54 | 55 | This plugin has been developed by Oslandia (http://www.oslandia.com). 56 | 57 | Oslandia provides support and assistance for QGIS and associated tools, including this plugin. 58 | 59 | This work has been funded by European funds. 60 | Thanks to the GIS Office of Apavil, Valcea County (Romania) 61 | 62 | This work has been also developed by eHealth Africa (http://ehealthafrica.org) for SpatiaLite 4.x support, filter selection for SpatiaLite file, diff mode and user identification improvements. 63 | 64 | License 65 | ======= 66 | 67 | This work is free software and licenced under the GNU GPL version 2 or any later version. 68 | See LICENSE file. 69 | -------------------------------------------------------------------------------- /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | import os 4 | import sys 5 | 6 | # So we can use absolute path 7 | path_root = os.path.join(os.path.dirname(__file__)) 8 | sys.path.append(path_root) 9 | 10 | 11 | def classFactory(iface): 12 | from .plugin import Plugin 13 | return Plugin(iface) 14 | -------------------------------------------------------------------------------- /archiving.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 22 | 24 | image/svg+xml 25 | 27 | 28 | 29 | 30 | 32 | 52 | 56 | 62 | 66 | 72 | 77 | 82 | 86 | 91 | 95 | 100 | 104 | 109 | 113 | 114 | 121 | 122 | -------------------------------------------------------------------------------- /checkout_pg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | GIS icon theme 0.2 23 | 25 | 32 | 39 | 46 | 53 | 60 | 67 | 74 | 82 | 87 | 92 | 98 | 100 | 103 | 106 | 107 | 108 | 115 | 122 | 123 | 149 | 159 | 160 | 162 | 163 | 165 | image/svg+xml 166 | 168 | GIS icon theme 0.2 169 | 170 | 171 | Robert Szczepanek 172 | 173 | 174 | 175 | 176 | Robert Szczepanek 177 | 178 | 179 | 180 | 181 | GIS icons 182 | 183 | 184 | GIS icons 185 | http://robert.szczepanek.pl/ 186 | 188 | 189 | 191 | 193 | 195 | 197 | 199 | 201 | 203 | 204 | 205 | 206 | 212 | 216 | 220 | 225 | 230 | 235 | 240 | 245 | 250 | 255 | 260 | 265 | 269 | 275 | 281 | 282 | 283 | 284 | -------------------------------------------------------------------------------- /checkout_pg_local.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 21 | GIS icon theme 0.2 23 | 25 | 32 | 39 | 46 | 53 | 60 | 67 | 74 | 82 | 87 | 92 | 98 | 100 | 103 | 106 | 107 | 108 | 115 | 122 | 123 | 149 | 161 | 162 | 164 | 165 | 167 | image/svg+xml 168 | 170 | GIS icon theme 0.2 171 | 172 | 173 | Robert Szczepanek 174 | 175 | 176 | 177 | 178 | Robert Szczepanek 179 | 180 | 181 | 182 | 183 | GIS icons 184 | 185 | 186 | GIS icons 187 | http://robert.szczepanek.pl/ 188 | 190 | 191 | 193 | 195 | 197 | 199 | 201 | 203 | 205 | 206 | 207 | 208 | 214 | 218 | 222 | 227 | 232 | 237 | 242 | 247 | 252 | 257 | 262 | 267 | 271 | 277 | 283 | 284 | 285 | 286 | -------------------------------------------------------------------------------- /commit_msg.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | CommitMsgDialog 4 | 5 | 6 | 7 | 0 8 | 0 9 | 388 10 | 334 11 | 12 | 13 | 14 | Commit versioned layers 15 | 16 | 17 | 18 | 19 | 20 20 | 280 21 | 341 22 | 32 23 | 24 | 25 | 26 | Qt::Horizontal 27 | 28 | 29 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 30 | 31 | 32 | 33 | 34 | 35 | 40 36 | 60 37 | 321 38 | 211 39 | 40 | 41 | 42 | 43 | 44 | 45 | 40 46 | 37 47 | 121 48 | 18 49 | 50 | 51 | 52 | Commit message 53 | 54 | 55 | 56 | 57 | true 58 | 59 | 60 | 61 | 167 62 | 10 63 | 191 64 | 27 65 | 66 | 67 | 68 | false 69 | 70 | 71 | false 72 | 73 | 74 | 75 | 76 | 77 | false 78 | 79 | 80 | false 81 | 82 | 83 | -1 84 | 85 | 86 | 100 87 | 88 | 89 | 2147483643 90 | 91 | 92 | false 93 | 94 | 95 | 96 | 97 | true 98 | 99 | 100 | 101 | 40 102 | 14 103 | 91 104 | 20 105 | 106 | 107 | 108 | <html><head/><body><p>Select username of data editor</p></body></html> 109 | 110 | 111 | PG username 112 | 113 | 114 | 115 | 116 | 117 | 118 | buttonBox 119 | accepted() 120 | CommitMsgDialog 121 | accept() 122 | 123 | 124 | 248 125 | 254 126 | 127 | 128 | 157 129 | 274 130 | 131 | 132 | 133 | 134 | buttonBox 135 | rejected() 136 | CommitMsgDialog 137 | reject() 138 | 139 | 140 | 316 141 | 260 142 | 143 | 144 | 286 145 | 274 146 | 147 | 148 | 149 | 150 | pg_users_combobox 151 | currentIndexChanged(QString) 152 | CommitMsgDialog 153 | setFocus() 154 | 155 | 156 | 262 157 | 23 158 | 159 | 160 | 198 161 | 156 162 | 163 | 164 | 165 | 166 | 167 | -------------------------------------------------------------------------------- /docs/contributors.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | -------------------- 4 | Project contributors 5 | -------------------- 6 | 7 | The following organizations have played a key role in the development of the |plugin|. 8 | 9 | SC Apavil Ramnicu Valcea 10 | ======================== 11 | The GIS Office of `SC Apavil Ramnicu Valcea, Romania `_ initially funded the development of the plugin through a European development project. 12 | 13 | Oslandia |oslandia|_ 14 | ==================== 15 | Oslandia_ is the initial developer of the plugin and the official maintainer on `GitHub `_. 16 | 17 | eHealth Africa |eHA|_ 18 | ===================== 19 | eHealth Africa joined the development effort in Fall 2015 and has contributed several enhancements since. 20 | 21 | Other 22 | ===== 23 | 24 | List of individual code `contributors`_. 25 | -------------------------------------------------------------------------------- /docs/globals.rst: -------------------------------------------------------------------------------- 1 | .. |doctest| replace:: :mod:`doctest` 2 | .. |date| date:: %d-%m-%Y 3 | .. |time| date:: %H:%M 4 | 5 | .. |qg| replace:: :mod:`QGIS` 6 | .. _qg: http://qgis.org 7 | 8 | .. _contributors: https://github.com/Oslandia/qgis-versioning/graphs/contributors 9 | 10 | .. |plugin| replace:: :mod:`qgis-versioning plugin` 11 | .. _plugin: https://github.com/Oslandia/qgis-versioning 12 | 13 | .. _original page: http://www.oslandia.com/qgis-versioning-plugin-en.html 14 | 15 | .. |CC| image:: images/CC-BY-SA-4-0.png 16 | .. _CC: http://creativecommons.org/licenses/by-sa/4.0/ 17 | .. |eHA| image:: images/eha.jpg 18 | .. _eHA: http://www.ehealthafrica.org/ 19 | .. _oslandia: http://www.oslandia.com/ 20 | .. |oslandia| image:: images/oslandia_logo.png 21 | .. |spatialite-logo| image:: images/spatialite-logo.png 22 | .. _spatialite-logo: https://www.gaia-gis.it/fossil/libspatialite/index 23 | .. |postgis-logo| image:: images/PostGIS-logo.png 24 | .. _postgis-logo: http://postgis.net/ 25 | .. |selected_features_warning| image:: images/selected_features_warning.png 26 | .. |selected_features_local| image:: images/selected_features_local.png 27 | 28 | .. |archive_png| image:: images/archiving.png 29 | .. |archive_schemas_png| image:: images/archive_schemas.png 30 | .. |branch_png| image:: images/branch.png 31 | .. |merge_png| image:: images/merge.png 32 | .. |checkout_png| image:: images/checkout.png 33 | .. |checkout_pg_png| image:: images/checkout_pg.png 34 | .. |checkout_pg_local_png| image:: images/checkout_pg_local.png 35 | .. |commit_png| image:: images/commit.png 36 | .. |help_png| image:: images/help.png 37 | .. |historize_png| image:: images/historize.png 38 | .. |update_png| image:: images/update.png 39 | .. |view_png| image:: images/view.png 40 | .. |unversioned_menu_png| image:: images/unversioned_menu.png 41 | .. |historize_warning_png| image:: images/historize_warning.png 42 | .. |versioned_menu_png| image:: images/versioned_menu.png 43 | .. |versbranch_menu_png| image:: images/versioned_branch_menu.png 44 | .. |working_copy_sl_png| image:: images/working_copy_sl.png 45 | .. |working_copy_pg_png| image:: images/working_copy_pg.png 46 | .. |working_copy_png| image:: images/working_copy.png 47 | .. |no_group_selected_png| image:: images/no_group_selected.png 48 | .. |layers_not_same_db_png| image:: images/layers_not_same_db.png 49 | .. |groups_same_name_png| image:: images/groups_same_name.png 50 | .. |empty_group_png| image:: images/empty_group.png 51 | .. |view_dialog_png| image:: images/view_dialog.png 52 | .. |view_dialog_diff_mode_png| image:: images/view_dialog_diff_mode.png 53 | .. |diff_mode_symbology_png| image:: images/diff_mode_symbology.png 54 | .. |unversioned_png| image:: images/unversioned.png 55 | .. |versioned_png| image:: images/versioned.png 56 | .. |sl_checkout_png| image:: images/sl_checkout.png 57 | .. |pg_checkout_png| image:: images/pg_checkout.png 58 | .. |branch_group_png| image:: images/branch_group.png 59 | .. |full_mode_view_png| image:: images/full_mode_view.png 60 | .. |diff_mode_view_png| image:: images/diff_mode_view.png 61 | .. |initial_db_png| image:: images/initial_db.png 62 | .. |historization_png| image:: images/historization.png 63 | .. |creating_mybranch_png| image:: images/creating_mybranch.png 64 | .. |revisions_table_png| image:: images/revisions_table.png 65 | .. |pipes_view_png| image:: images/pipes_view.png 66 | .. |pipes_table_png| image:: images/pipes_table.png 67 | .. |late_by_png| image:: images/late_by.png 68 | .. |uptodate_png| image:: images/uptodate.png 69 | .. |late_by_warning_png| image:: images/late_by_warning.png 70 | .. |conflict_layer_png| image:: images/conflict_layer.png 71 | .. |conflict_warning_png| image:: images/conflict_warning.png 72 | .. |commit_ui_png| image:: images/commit_ui.png 73 | .. |commit_success_png| image:: images/commit_success.png 74 | -------------------------------------------------------------------------------- /docs/images/CC-BY-SA-4-0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/CC-BY-SA-4-0.png -------------------------------------------------------------------------------- /docs/images/PostGIS-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/PostGIS-logo.png -------------------------------------------------------------------------------- /docs/images/archive_schemas.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/archive_schemas.png -------------------------------------------------------------------------------- /docs/images/archiving.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/archiving.png -------------------------------------------------------------------------------- /docs/images/branch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/branch.png -------------------------------------------------------------------------------- /docs/images/branch_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/branch_group.png -------------------------------------------------------------------------------- /docs/images/checkout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/checkout.png -------------------------------------------------------------------------------- /docs/images/checkout_pg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/checkout_pg.png -------------------------------------------------------------------------------- /docs/images/checkout_pg_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/checkout_pg_local.png -------------------------------------------------------------------------------- /docs/images/commit.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/commit.png -------------------------------------------------------------------------------- /docs/images/commit_success.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/commit_success.png -------------------------------------------------------------------------------- /docs/images/commit_ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/commit_ui.png -------------------------------------------------------------------------------- /docs/images/conflict_layer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/conflict_layer.png -------------------------------------------------------------------------------- /docs/images/conflict_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/conflict_warning.png -------------------------------------------------------------------------------- /docs/images/creating_mybranch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/creating_mybranch.png -------------------------------------------------------------------------------- /docs/images/diff_mode_symbology.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/diff_mode_symbology.png -------------------------------------------------------------------------------- /docs/images/diff_mode_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/diff_mode_view.png -------------------------------------------------------------------------------- /docs/images/eha.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/eha.jpg -------------------------------------------------------------------------------- /docs/images/empty_group.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/empty_group.png -------------------------------------------------------------------------------- /docs/images/full_mode_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/full_mode_view.png -------------------------------------------------------------------------------- /docs/images/groups_same_name.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/groups_same_name.png -------------------------------------------------------------------------------- /docs/images/help.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/help.png -------------------------------------------------------------------------------- /docs/images/historization.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/historization.png -------------------------------------------------------------------------------- /docs/images/historize.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/historize.png -------------------------------------------------------------------------------- /docs/images/historize_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/historize_warning.png -------------------------------------------------------------------------------- /docs/images/initial_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/initial_db.png -------------------------------------------------------------------------------- /docs/images/late_by.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/late_by.png -------------------------------------------------------------------------------- /docs/images/late_by_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/late_by_warning.png -------------------------------------------------------------------------------- /docs/images/layers_not_same_db.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/layers_not_same_db.png -------------------------------------------------------------------------------- /docs/images/merge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/merge.png -------------------------------------------------------------------------------- /docs/images/no_group_selected.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/no_group_selected.png -------------------------------------------------------------------------------- /docs/images/oslandia_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/oslandia_logo.png -------------------------------------------------------------------------------- /docs/images/pg_checkout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/pg_checkout.png -------------------------------------------------------------------------------- /docs/images/pipes_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/pipes_table.png -------------------------------------------------------------------------------- /docs/images/pipes_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/pipes_view.png -------------------------------------------------------------------------------- /docs/images/revisions_table.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/revisions_table.png -------------------------------------------------------------------------------- /docs/images/selected_features_local.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/selected_features_local.png -------------------------------------------------------------------------------- /docs/images/selected_features_warning.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/selected_features_warning.png -------------------------------------------------------------------------------- /docs/images/sl_checkout.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/sl_checkout.png -------------------------------------------------------------------------------- /docs/images/spatialite-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/spatialite-logo.png -------------------------------------------------------------------------------- /docs/images/unversioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/unversioned.png -------------------------------------------------------------------------------- /docs/images/unversioned_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/unversioned_menu.png -------------------------------------------------------------------------------- /docs/images/update.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/update.png -------------------------------------------------------------------------------- /docs/images/uptodate.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/uptodate.png -------------------------------------------------------------------------------- /docs/images/versioned.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/versioned.png -------------------------------------------------------------------------------- /docs/images/versioned_branch_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/versioned_branch_menu.png -------------------------------------------------------------------------------- /docs/images/versioned_menu.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/versioned_menu.png -------------------------------------------------------------------------------- /docs/images/view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/view.png -------------------------------------------------------------------------------- /docs/images/view_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/view_dialog.png -------------------------------------------------------------------------------- /docs/images/view_dialog_diff_mode.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/view_dialog_diff_mode.png -------------------------------------------------------------------------------- /docs/images/working_copy.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/working_copy.png -------------------------------------------------------------------------------- /docs/images/working_copy_pg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/working_copy_pg.png -------------------------------------------------------------------------------- /docs/images/working_copy_sl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/docs/images/working_copy_sl.png -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | .. meta:: 4 | :DC.creator: eHealth Africa 5 | :DC.language: en 6 | 7 | ----------------------- 8 | QGIS Versioning plugin 9 | ----------------------- 10 | 11 | .. |postgis-logo|_ 12 | 13 | .. |spatialite-logo|_ 14 | 15 | This is the temporary location of the |plugin|_ documentation. Click this link for the `original page`_. 16 | See also this article describing why the plugin has been built and how: `GIS Open Source versioning tool for a multi-user Distributed Environment `_. Cet article est aussi disponible `en français`_. 17 | 18 | ---------- 19 | 20 | :Date: Last generated on |date| at |time| 21 | .. |CC|_ 22 | 23 | ---------- 24 | 25 | .. toctree:: 26 | :maxdepth: 2 27 | 28 | introduction 29 | requirements 30 | functionality 31 | spatialfiltering 32 | problem-handling 33 | inner-workings 34 | contributors 35 | -------------------------------------------------------------------------------- /docs/inner-workings.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | -------------------- 4 | Inner workings 5 | -------------------- 6 | 7 | This section is intended to explain more technical aspects of the versioning implemented by the |plugin|. 8 | 9 | Central database 10 | ================ 11 | 12 | Once a database is versioned, three operations can be performed on table rows: INSERT, DELETE and UPDATE. To be able to track history, every row is kept in the tables. Deleted rows are marked as such and updated rows are a combined insertion-deletion where the deleted and added rows are linked to one another as parent and child. 13 | 14 | A total of five columns are needed for versioning the first branch: 15 | 16 | **PRIMARY KEY** 17 | a unique identifier across the table 18 | **branch_rev_begin** 19 | revision when this record was inserted 20 | **branch_rev_end** 21 | last revision for which this record exists (i.e. revision when it was deleted minus one) 22 | **branch_parent** 23 | in case the row has been inserted as a result of an update, this field stores the id of the row that has been updated 24 | **branch_child** 25 | in case the row has been marked as deleted as a result of an update, this field stores the id of the row that has been inserted in its place. 26 | 27 | For each additional branch, four additional columns are needed (the ones with the prefix 'branch\_'). 28 | 29 | .. note:: 30 | A null value for *branch_rev_begin* means that a row (feature) does not belong to that branch. 31 | 32 | SQL views are used to see a snapshot of the database for a given revision number. Noting 'rev' the revision we want to see, the condition for a row to be present in the view is: 33 | 34 | (*branch_rev_end* IS NULL OR *branch_rev_end* >= rev) AND *branch_rev_begin* <= rev 35 | 36 | In the special case of the latest revision, or head revision, the condition reads: 37 | 38 | *branch_rev_end* IS NULL AND *branch_rev_begin* IS NOT NULL 39 | 40 | .. note:: 41 | Since elements are not deleted (but merely marked as such) in an historized table, care must be taken with the definition of constraints, in particular the conceptual unicity of a field values. 42 | 43 | Views for revisions must be read-only and historized tables should **never** be edited directly. This is a basic principle for version control : editions must be made to working copies an then committed to the database. Please note that by default PostGIS 9.3 creates updatable views. 44 | 45 | 46 | Working copy database 47 | ===================== 48 | 49 | For each versioned table in the working copy, a view is created with the suffix _view (e.g. mytable_view). Those views typically filter out the historization columns and show the head revision. A set of triggers is defined to allow operating on those views (DELETE, UPDATE and INSERT). 50 | 51 | The DELETE trigger simply marks the end revision of a given record. 52 | 53 | The INSERT trigger creates a new record and fills the *branch_rev_begin* field. 54 | 55 | The UPDATE trigger creates a new record and fills the *branch_rev_begin* and *branch_parent* fields. It then marks the parent record as deleted, and fills the *branch_rev_end* and *branch_child* fields accordingly. 56 | -------------------------------------------------------------------------------- /docs/introduction.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | ------------- 4 | Introduction 5 | ------------- 6 | 7 | Summary 8 | ======= 9 | 10 | The |plugin| provides versioning of geographical features stored in PostGIS databases. A typical use case met by the plugin involves one or more users checking out a local working copy to edit features in (potentially) offline mode using SpatiaLite and committing changes back to the PostGIS server. 11 | 12 | More information can be found in the plugin's `original page`_ page. 13 | 14 | .. note:: 15 | Most of the screenshots contained in this document were made on an Ubuntu workstation. A few ones were made on a Windows workstation. 16 | -------------------------------------------------------------------------------- /docs/problem-handling.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | -------------------- 4 | When problems occur 5 | -------------------- 6 | 7 | Multiple modifications to the same dataset amongst a number of users inevitably bring about conflicts. This section shows how editing conflicts are managed by the plugin. Specific errors or exceptions are also mentioned. 8 | 9 | .. _conflict-resolution: 10 | 11 | Conflict management 12 | =================== 13 | 14 | For users to be able to commit modifications to the database, their working copy must be up to date with the database. Take the example of two users checking out SL working copies from HEAD revision X. Each on their workstations, they are editing features in their working copy. One of the users will inevitably commit his(her) changes to the database first. This will create another entry in the *revisions* table, that is increment the HEAD revision to X + 1. 15 | 16 | When the second user tries to commit, a message will warn that the working copy needs to be updated : 17 | 18 | |late_by_warning_png| 19 | 20 | .. note:: 21 | It is wise to always update a working copy before trying to commit. Updating directly by clicking |update_png| will check if the working copy is up to date. If it is, a message will let you know : |uptodate_png| 22 | 23 | Else, the working copy is updated to the latest revision, for example |late_by_png| 24 | 25 | If edits made by the first committer do not conflict with the second user's working copy, updating will proceed normally. Changes made by the other user will be merged into the second user's working copy. All is well and the second user can now commit his(her) changes. 26 | 27 | In the event there are conflicting edits, the second user will be presented with this message after updating : 28 | 29 | |conflict_warning_png| 30 | 31 | A new layer is created by the plugin for every dataset that contain errors and it is displayed in the working copy layer group. The name of that layer is made up of the original layer name to which the "_conflicts" string is appended : 32 | 33 | |conflict_layer_png| 34 | 35 | The figure above shows two conflicting features in one layer. Highlighted is the edit the second user is trying to commit. The attributes table shows the id of the conflicting feature. The conflict is resolved by deleting the unwanted entry in the conflict layer, either 'mine' or 'theirs' and saving the edits. On deletion of 'mine', the working copy edition is discarded; on deletion of 'theirs' the working copy edition is appended to the feature history (i.e. the working copy feature becomes a child of the last state of the feature in the historized database). 36 | 37 | Once the conflict table is empty, committing can proceed. 38 | 39 | .. note:: 40 | On deletion of one conflict entry, both entries are removed (by a trigger) but the attribute table (and map canvas) are not refreshed. As a workaround, the user can close and re-open the attribute table to see the actual state of the conflict table. 41 | 42 | Since deleting 'mine' implies discarding one's own change, then committing will result in no change being committed. 43 | 44 | A useful tip if you deleted the wrong one (e.g. 'theirs' and you meant to delete 'mine') **and** did not save yet : CTRL-Z (undo) to the rescue. 45 | 46 | In the more general case, multiple editions can be made to the same feature. Therefore child relations must be followed to the last one in order to present the user with the latest state of a given conflicting feature. 47 | 48 | .. warning:: 49 | Known bug : Updating a working copy may indicate it is up to date when in fact it is not. This may happen if for example the checkout (working copy creation) was done on trunk and then another branch was created afterwards. The working copy gets "stuck" at the latest revision of the branch it was checked out from. The only way to get around this is to checkout a fresh working copy from the desired branch. Edits made in the other working copy are still there and need to be integrated manually in the most recent working copy. 50 | 51 | Tip : clicking on the view icon |view_png| will tell you what the latest revision number is. 52 | 53 | Errors and exceptions 54 | ===================== 55 | 56 | Although errors are generally managed within the plugin, specific circumstances may trigger errors or Pyhton exceptions. This section shows some of those errors and how they can be avoided or recovered from. 57 | -------------------------------------------------------------------------------- /docs/requirements.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | ------------ 4 | Requirements 5 | ------------ 6 | 7 | This section is a list of both hard and soft requirements (aka suggestions). 8 | 9 | Hard requirements 10 | ================= 11 | Requirements considered 'hard' are those you cannot really live without. The plugin may work but you may experience problems to make it work. The two basic hard requirements are about |qg| and PostgreSQL/PostGIS. Another pertains to naming conventions. 12 | 13 | In a nutshell, those hard requirements are : 14 | 15 | - QGIS 2.8+ 16 | - PG 9.x (ideally 9.2+) 17 | - names asked by he plugin must begin with either a "_" (underscore) or a letter followed by any lowercase non accented letter, digit or underscore without spaces and up to 63 characters long 18 | 19 | QGIS 20 | +++++ 21 | 22 | Recent versions of the plugin were tested with |qg| 2.8. It is worth mentioning that plugin versions < 0.2 were developed for older versions of |qg|, which may incidentally ship with older versions of spatialite/SQlite. As of version 0.2 (Aug 2015), `spatialite `_ version 4.x is supported by the plugin. This in turn makes any |qg| versions that come with older spatialite versions unusable with the plugin (for spatialite checkouts at least). 23 | 24 | Another key dependency of the plugin is `ogr2ogr `_. Although the plugin does not depend on the most recent features of ogr2ogr, it is wise to stick to the version bundled in |qg| 2.8+ (2.8+ because newer versions of ogr2ogr shipped in newer versions of |qg| should be backwards compatible). 25 | 26 | PostgreSQL/PostGIS 27 | ++++++++++++++++++ 28 | 29 | The plugin should work on any minor revision of the PostgreSQL 9.x series. It was tested successfully on PostgreSQL 9.2 and 9.4. 30 | 31 | There are no hard requirements on PostGIS as such on the part of the plugin. Packing the most recent version supported in your PostgreSQL installation should be sufficient. 32 | 33 | Naming conventions 34 | ++++++++++++++++++ 35 | 36 | Operation of the plugin is best ensured by sticking to the PostgreSQL naming rules. As suggested `here `_ : 37 | 38 | .. pull-quote:: 39 | PostgreSQL uses a single data type to define all object names: the name type. A value of type name is a string of 63 or fewer characters. A name must start with a letter or an underscore; the rest of the string can contain letters, digits, and underscores. 40 | 41 | .. warning:: 42 | Do NOT use empty spaces in any identifier the plugin asks you to supply. 43 | 44 | Do NOT use accented characters (e.g. German umlaut or French "accent aigu") 45 | 46 | For optimal operation, avoid using spaces in the full path name of files intended to be used by the plugin, for example spatialite files. 47 | 48 | Even though PostgreSQL object names can contain capital letters, the plugin does not currently support object names other than in lowercase letters (plus digits and underscores as mentioned above). Even though the plugin ensures some level of protection in that respect, it is best to stick to those conventions when naming a new PG checkout (see later for an explanation), a branch or any other name the plugin asks you to provide. 49 | 50 | The same applies to spatialite filenames (SL checkout). 51 | 52 | Soft requirements 53 | ================= 54 | Soft requirements are more like "best practice" suggestions. As the saying goes : Your Mileage May Vary. 55 | 56 | Separate schema 57 | +++++++++++++++ 58 | 59 | As will be explained in more detail later in this document, the |plugin| operates "historization" by adding columns to each table in a particular database together with a *revisions* table that holds all revision information. For a number of reasons, it is wise to isolate your geographic data in a schema **other** than the *public* schema. 60 | 61 | As mentioned `here `_ : 62 | 63 | .. pull-quote:: 64 | "... store no data in the 'public' schema." 65 | 66 | The specific context of the previous quote pertains to backup and restore procedures but the advice also applies for the |plugin|. 67 | -------------------------------------------------------------------------------- /docs/spatialfiltering.rst: -------------------------------------------------------------------------------- 1 | .. include:: globals.rst 2 | 3 | ---------------------------------- 4 | Selecting features before checkout 5 | ---------------------------------- 6 | 7 | Before checking out a local spatialite working copy, one can select features from the versioned tables to work on locally. Any layer in the group can have features selected. In the case of a layer with no features selected, the whole dataset will be checked out by the |plugin|. This allows users to select only those features they are interested in editing rather than the whole dataset. 8 | 9 | .. note:: 10 | Feature selection prior to checkout only applies to spatialite checkouts. It has yet to be implemented for PG checkouts. 11 | 12 | Procedure 13 | ========= 14 | 15 | - For each layer in the group, select features you want checked out. This can be done in a number of ways in |qg|. 16 | - When ready to checkout, click on the layer group and click on the spatialite checkout button (|checkout_png|). At that point any layer with selected features will pop this warning to let the user know a subset of features will be checked out : 17 | 18 | |selected_features_warning| 19 | 20 | - Complete the rest of the default spatialite checkout workflow and check that only a subset of features was retrieved for the layers you selected features for. 21 | 22 | In our example, only the selected polygons (yellow above) and all points (since no feature selection was performed on the point layer) were checked out : 23 | 24 | |selected_features_local| 25 | -------------------------------------------------------------------------------- /metadata.txt: -------------------------------------------------------------------------------- 1 | # This file contains metadata for your plugin. 2 | 3 | # Mandatory items: 4 | 5 | [general] 6 | name=versioning 7 | qgisMinimumVersion=3.4 8 | description=postgis database versioning 9 | version=1.0 10 | author=Oslandia 11 | email=infos@oslandia.com 12 | about=A tool to manage data history, branches, and to work offline with your PostGIS-stored data and QGIS. 13 | 14 | homepage=https://github.com/Oslandia/qgis-versioning 15 | tracker=https://github.com/Oslandia/qgis-versioning/issues 16 | repository=https://github.com/Oslandia/qgis-versioning.git 17 | icon=historize.svg 18 | 19 | category=Database 20 | 21 | -------------------------------------------------------------------------------- /package.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | # coding=UTF-8 3 | """ 4 | packaging script for the qgis_versioning project 5 | 6 | USAGE 7 | python -m qgispackage.py [-h, -i, -u] [directory], 8 | 9 | OPTIONS 10 | -h, --help 11 | print this help 12 | 13 | -i, --install [directory] 14 | install the package in the .qgis2 directory, if directory is ommited, 15 | install in the QGis plugin directory 16 | 17 | -u, --uninstall 18 | uninstall (remove) the package from .qgis2 directory 19 | """ 20 | 21 | import os 22 | import zipfile 23 | import re 24 | import shutil 25 | 26 | # @todo make that work on windows 27 | qgis_plugin_dir = os.path.join(os.path.expanduser('~'), ".qgis2", "python", "plugins") 28 | 29 | def uninstall(install_dir): 30 | target_dir = os.path.join(install_dir, "qgis_versioning") 31 | if os.path.isdir(target_dir): 32 | shutil.rmtree(target_dir) 33 | 34 | def install(install_dir, zip_filename): 35 | uninstall(install_dir) 36 | with zipfile.ZipFile(zip_filename, "r") as z: 37 | z.extractall(install_dir) 38 | print("installed in", install_dir) 39 | 40 | def zip_(zip_filename): 41 | """the zip file include tests""" 42 | qgis_versioning_dir = os.path.abspath(os.path.dirname(__file__)) 43 | with zipfile.ZipFile(zip_filename, 'w') as package: 44 | for root, dirs, files in os.walk(qgis_versioning_dir): 45 | if not re.match(r".*(test_data|doc|tmp).*", root): 46 | for file_ in files: 47 | if re.match(r".*\.(py|txt|ui|svg|png|insat|sat|qml|sql|sqlite)$", file_) \ 48 | and not re.match(r"(package.py)", file_): 49 | fake_root = root.replace(qgis_versioning_dir, "qgis_versioning") 50 | package.write(os.path.join(root, file_), 51 | os.path.join(fake_root, file_)) 52 | 53 | 54 | if __name__ == "__main__": 55 | import getopt 56 | import sys 57 | 58 | try: 59 | optlist, args = getopt.getopt(sys.argv[1:], 60 | "hiu", 61 | ["help", "install", "uninstall"]) 62 | except Exception as e: 63 | sys.stderr.write(str(e)+"\n") 64 | exit(1) 65 | 66 | optlist = dict(optlist) 67 | 68 | if "-h" in optlist or "--help" in optlist: 69 | help(sys.modules[__name__]) 70 | exit(0) 71 | 72 | zip_filename = os.path.join(os.path.dirname(__file__), "qgis_versioning.zip") 73 | zip_(zip_filename) 74 | install_dir = qgis_plugin_dir if len(args)==0 else args[0] 75 | 76 | if "-u" in optlist or "--uninstall" in optlist: 77 | uninstall(install_dir) 78 | 79 | if "-i" in optlist or "--install" in optlist: 80 | install(install_dir, zip_filename) 81 | 82 | -------------------------------------------------------------------------------- /revision_dialog.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | view_dlg 4 | 5 | 6 | 7 | 0 8 | 0 9 | 751 10 | 418 11 | 12 | 13 | 14 | 15 | 0 16 | 0 17 | 18 | 19 | 20 | Revisions selection 21 | 22 | 23 | false 24 | 25 | 26 | 27 | 28 | 29 | false 30 | 31 | 32 | Check to go to diff mode for any two revisions 33 | 34 | 35 | Compare selected revisions 36 | 37 | 38 | 39 | 40 | 41 | 42 | Select one [many] for single [multiple] revisions. Fetching may take time. 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | true 52 | 53 | 54 | true 55 | 56 | 57 | false 58 | 59 | 60 | false 61 | 62 | 63 | false 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | Qt::Horizontal 73 | 74 | 75 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | buttonBox 85 | accepted() 86 | view_dlg 87 | accept() 88 | 89 | 90 | 248 91 | 254 92 | 93 | 94 | 157 95 | 274 96 | 97 | 98 | 99 | 100 | buttonBox 101 | rejected() 102 | view_dlg 103 | reject() 104 | 105 | 106 | 316 107 | 260 108 | 109 | 110 | 286 111 | 274 112 | 113 | 114 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/test/__init__.py -------------------------------------------------------------------------------- /test/abbreviation_bug_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import psycopg2 7 | import os 8 | import tempfile 9 | 10 | longname = '_this_is_a_very_long_name_that should_be_trunctated_if_buggy' 11 | another_longname = ('this_is_another_edited_very_long_name_that ' 12 | 'should_be_trunctated_if_buggy') 13 | new_longname = 'newly inserted with long name' 14 | 15 | 16 | def test(host, pguser): 17 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 18 | tmp_dir = tempfile.gettempdir() 19 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 20 | 21 | sqlite_test_filename = tmp_dir+"/abbreviation_test.sqlite" 22 | if os.path.isfile(sqlite_test_filename): 23 | os.remove(sqlite_test_filename) 24 | 25 | spversioning = versioning.spatialite(sqlite_test_filename, pg_conn_info) 26 | # create the test database 27 | os.system("dropdb --if-exists -h " + host + " -U "+pguser 28 | + " epanet_test_db") 29 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 30 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f " 31 | + test_data_dir + "/epanet_test_db.sql") 32 | 33 | # delete existing data 34 | pcon = psycopg2.connect(pg_conn_info) 35 | pcur = pcon.cursor() 36 | for i in range(10): 37 | pcur.execute(""" 38 | INSERT INTO epanet.junctions 39 | (demand_pattern_id, elevation, geom) 40 | VALUES 41 | ('{demand_pattern_id}', {elev}, 42 | ST_GeometryFromText('POINT({x} {y})',2154)); 43 | """.format( 44 | demand_pattern_id=str(i+2) 45 | + longname, 46 | elev=float(i), 47 | x=float(i+1), 48 | y=float(i+1) 49 | )) 50 | pcon.commit() 51 | versioning.historize(pg_conn_info, 'epanet') 52 | 53 | spversioning.checkout(["epanet_trunk_rev_head.junctions", 54 | "epanet_trunk_rev_head.pipes"]) 55 | assert(os.path.isfile(sqlite_test_filename) 56 | and "sqlite file must exist at this point") 57 | 58 | scon = dbapi2.connect(sqlite_test_filename) 59 | scon.enable_load_extension(True) 60 | scon.execute("SELECT load_extension('mod_spatialite')") 61 | scur = scon.cursor() 62 | scur.execute("SELECT id, demand_pattern_id from junctions") 63 | 64 | for rec in scur: 65 | if rec[0] > 2: 66 | assert rec[1].find(longname) != -1 67 | 68 | scur.execute(f""" 69 | update junctions_view 70 | set demand_pattern_id='{another_longname}' where ogc_fid > 8""") 71 | 72 | scur.execute(f""" 73 | insert into junctions_view(id, demand_pattern_id, elevation, geom) 74 | select 13, '{new_longname}', elevation, geom 75 | from junctions_view where ogc_fid=4""") 76 | scon.commit() 77 | 78 | spversioning.commit('a commit msg') 79 | 80 | pcur.execute("""select versioning_id, demand_pattern_id 81 | from epanet_trunk_rev_head.junctions""") 82 | for row in pcur: 83 | print(row) 84 | if row[0] > 8: 85 | assert row[1].find(another_longname) != -1\ 86 | or row[1].find(new_longname) != -1 87 | 88 | 89 | if __name__ == "__main__": 90 | if len(sys.argv) != 3: 91 | print("Usage: python3 versioning_base_test.py host pguser") 92 | else: 93 | test(*sys.argv[1:]) 94 | -------------------------------------------------------------------------------- /test/archiving_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from versioningDB import versioning 6 | import psycopg2 7 | import os 8 | 9 | 10 | def printTab(pcur, schema, table): 11 | pk = 'versioning_id' 12 | try: 13 | pk = versioning.pg_pk(pcur, schema, table) 14 | except: 15 | pass 16 | 17 | print("\n**********************************") 18 | print(schema+"."+table) 19 | pcur.execute("""SELECT column_name FROM information_schema.columns WHERE 20 | table_schema = '{schema}' AND table_name = '{table}'""".format(schema=schema, 21 | table=table)) 22 | cols = ",".join([i[0] for i in pcur.fetchall()]) 23 | print(cols) 24 | 25 | pcur.execute("""SELECT * FROM {schema}.{table} ORDER BY {pk}""".format(schema=schema, table=table, pk=pk)) 26 | 27 | rows = pcur.fetchall() 28 | for row in rows: 29 | r = ', '.join([str(l) for l in list(row)]) 30 | print(r) 31 | print("**********************************\n") 32 | 33 | def test(host, pguser): 34 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 35 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 36 | 37 | # create the test database 38 | 39 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 40 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 41 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 42 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 43 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 44 | 45 | # chechout 46 | #tables = ['epanet_trunk_rev_head.junctions','epanet_trunk_rev_head.pipes'] 47 | tables = ['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes'] 48 | pgversioning = versioning.pgServer(pg_conn_info, 'epanet_working_copy') 49 | pgversioning.checkout(tables) 50 | 51 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 52 | 53 | 54 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',ST_GeometryFromText('LINESTRING(1 1,0 1)',2154))") 55 | pcur.commit() 56 | pgversioning.commit("rev 1") 57 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 58 | pcur.commit() 59 | pgversioning.commit("rev 2") 60 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('4','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 61 | pcur.commit() 62 | pgversioning.commit("rev 3") 63 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('5','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 64 | pcur.commit() 65 | pgversioning.commit("rev 4") 66 | pcur.execute("DELETE FROM epanet_working_copy.pipes_view S WHERE versioning_id = 5") 67 | pcur.commit() 68 | pgversioning.commit("rev 5") 69 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('6','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 70 | pcur.commit() 71 | pgversioning.commit("rev 6") 72 | pcur.execute("UPDATE epanet_working_copy.pipes_view SET length = 4 WHERE versioning_id = 3") 73 | pcur.commit() 74 | pgversioning.commit("rev 7") 75 | pcur.execute("UPDATE epanet_working_copy.pipes_view SET length = 4 WHERE versioning_id = 1") 76 | pcur.commit() 77 | pgversioning.commit("rev 8") 78 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('7','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 79 | pcur.commit() 80 | pgversioning.commit("rev 9") 81 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('8','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 82 | pcur.commit() 83 | pgversioning.commit("rev 10") 84 | pcur.execute("DELETE FROM epanet_working_copy.pipes_view S WHERE versioning_id = 7") 85 | pcur.commit() 86 | pgversioning.commit("rev 11") 87 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('9','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 88 | pcur.commit() 89 | pgversioning.commit("rev 12") 90 | 91 | pcur.execute("SELECT * FROM epanet.pipes ORDER BY versioning_id") 92 | end = pcur.fetchall() 93 | 94 | printTab(pcur, 'epanet', 'pipes') 95 | pcur.execute("SELECT count(*) FROM epanet.pipes") 96 | [ret] = pcur.fetchone() 97 | assert(ret == 11) 98 | 99 | versioning.archive(pg_conn_info, 'epanet', 7) 100 | printTab(pcur, 'epanet', 'pipes') 101 | pcur.execute("SELECT count(*) FROM epanet.pipes") 102 | [ret] = pcur.fetchone() 103 | assert(ret == 9) 104 | pcur.execute("SELECT versioning_id FROM epanet.pipes ORDER BY versioning_id") 105 | assert([i[0] for i in pcur.fetchall()] == [1, 2, 4, 6, 7, 8, 9, 10, 11]) 106 | printTab(pcur, 'epanet_archive', 'pipes') 107 | pcur.execute("SELECT count(*) FROM epanet_archive.pipes") 108 | [ret] = pcur.fetchone() 109 | assert(ret == 2) 110 | pcur.execute("SELECT versioning_id FROM epanet_archive.pipes ORDER BY versioning_id") 111 | assert([i[0] for i in pcur.fetchall()] == [3, 5]) 112 | 113 | versioning.archive(pg_conn_info, 'epanet', 11) 114 | printTab(pcur, 'epanet', 'pipes') 115 | pcur.execute("SELECT count(*) FROM epanet.pipes") 116 | [ret] = pcur.fetchone() 117 | assert(ret == 7) 118 | pcur.execute("SELECT versioning_id FROM epanet.pipes ORDER BY versioning_id") 119 | assert([i[0] for i in pcur.fetchall()] == [2, 4, 6, 8, 9, 10, 11]) 120 | printTab(pcur, 'epanet_archive', 'pipes') 121 | pcur.execute("SELECT count(*) FROM epanet_archive.pipes") 122 | [ret] = pcur.fetchone() 123 | assert(ret == 4) 124 | pcur.execute("SELECT versioning_id FROM epanet_archive.pipes ORDER BY versioning_id") 125 | assert([i[0] for i in pcur.fetchall()] == [1, 3, 5, 7]) 126 | 127 | # view 128 | printTab(pcur, 'epanet_archive', 'pipes_all') 129 | pcur.execute("SELECT count(*) FROM epanet_archive.pipes_all") 130 | [ret] = pcur.fetchone() 131 | assert(ret == 11) 132 | pcur.execute("SELECT * FROM epanet_archive.pipes_all ORDER BY versioning_id") 133 | endv = pcur.fetchall() 134 | assert(end==endv) 135 | 136 | pcur.close() 137 | if __name__ == "__main__": 138 | if len(sys.argv) != 3: 139 | print("Usage: python3 archiving_test.py host pguser") 140 | else: 141 | test(*sys.argv[1:]) 142 | -------------------------------------------------------------------------------- /test/bug_in_branch_after_commit_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import os 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 13 | tmp_dir = tempfile.gettempdir() 14 | 15 | 16 | # create the test database 17 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 20 | 21 | versioning.historize(pg_conn_info,"epanet") 22 | 23 | # try the update 24 | wc = tmp_dir+"/bug_in_branch_after_commit_wc.sqlite" 25 | if os.path.isfile(wc): os.remove(wc) 26 | 27 | spversioning = versioning.spatialite(wc, pg_conn_info) 28 | spversioning.checkout(['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes']) 29 | 30 | scur = versioning.Db( dbapi2.connect( wc ) ) 31 | 32 | scur.execute("SELECT * FROM pipes") 33 | scur.execute("UPDATE pipes_view SET length = 1 WHERE OGC_FID = 1") 34 | scur.commit() 35 | 36 | spversioning.commit('test') 37 | 38 | versioning.add_branch(pg_conn_info,"epanet","mybranch","add 'branch") 39 | 40 | 41 | if __name__ == "__main__": 42 | if len(sys.argv) != 3: 43 | print("Usage: python3 versioning_base_test.py host pguser") 44 | else: 45 | test(*sys.argv[1:]) 46 | -------------------------------------------------------------------------------- /test/composite_primary_key_db.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE SCHEMA myschema; 3 | CREATE TABLE myschema.referenced ( 4 | id1 integer, 5 | id2 integer, 6 | name varchar, 7 | geom geometry('POINT', 2154), 8 | PRIMARY KEY (id1, id2) 9 | ); 10 | 11 | CREATE TABLE myschema.referencing ( 12 | id integer PRIMARY KEY, 13 | fkid1 integer, 14 | fkid2 integer, 15 | name varchar, 16 | geom geometry('POINT', 2154), 17 | FOREIGN KEY (fkid1, fkid2) REFERENCES myschema.referenced (id1, id2) 18 | ); 19 | 20 | INSERT INTO myschema.referenced (id1, id2, name, geom) VALUES (1,18, 'toto', ST_GeometryFromText('POINT(0 0)',2154)); 21 | INSERT INTO myschema.referenced (id1, id2, name, geom) VALUES (42,4, 'titi', ST_GeometryFromText('POINT(0 0)',2154)); 22 | 23 | INSERT INTO myschema.referencing (id, fkid1, fkid2, name, geom) 24 | VALUES (16, 1,18, 'fk_toto', ST_GeometryFromText('POINT(0 0)',2154)); 25 | -------------------------------------------------------------------------------- /test/create_db_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | SCRIPT_DIR=$(dirname $(readlink -f $0)) 4 | 5 | for db in epanet_test_db composite_primary_key_db; 6 | do 7 | dropdb --if-exists -h 127.0.01 -U postgres $db 8 | createdb -h 127.0.01 -U postgres $db 9 | psql -h 127.0.01 -U postgres $db -f $SCRIPT_DIR/$db.sql 10 | done 11 | 12 | EMPTY_DB="qgis_versioning_empty_db" 13 | dropdb --if-exists -h 127.0.01 -U postgres $EMPTY_DB 14 | createdb -h 127.0.01 -U postgres $EMPTY_DB 15 | psql -h 127.0.01 -U postgres $EMPTY_DB -c 'CREATE EXTENSION postgis' 16 | 17 | -------------------------------------------------------------------------------- /test/epanet_test_db.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE SCHEMA epanet; 3 | 4 | CREATE TABLE epanet.junctions ( 5 | id serial PRIMARY KEY, 6 | elevation float, 7 | base_demand_flow float, 8 | demand_pattern_id varchar, 9 | geom geometry('POINT',2154) 10 | ); 11 | 12 | CREATE TABLE epanet.pipes ( 13 | id serial PRIMARY KEY, 14 | start_node integer references epanet.junctions(id), 15 | end_node integer references epanet.junctions(id), 16 | length float, 17 | diameter float, 18 | roughness float, 19 | minor_loss_coefficient float, 20 | status varchar, 21 | geom geometry('LINESTRING',2154) 22 | ); 23 | 24 | -- INSERT DATA (Use to identify the data insertion block in test, do not remove this line!!!) 25 | 26 | INSERT INTO epanet.junctions 27 | (elevation, geom) 28 | VALUES 29 | (0,ST_GeometryFromText('POINT(1 0)',2154)); 30 | 31 | INSERT INTO epanet.junctions 32 | (elevation, geom) 33 | VALUES 34 | (1,ST_GeometryFromText('POINT(0 1)',2154)); 35 | 36 | 37 | INSERT INTO epanet.pipes 38 | (start_node, end_node, length, diameter, geom) 39 | VALUES 40 | (1,2,1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154)); 41 | -------------------------------------------------------------------------------- /test/epanet_test_db_uuid.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp"; 3 | CREATE SCHEMA epanet; 4 | 5 | CREATE TABLE epanet.junctions ( 6 | gid uuid DEFAULT uuid_generate_v4 (), 7 | jid serial PRIMARY KEY, 8 | id varchar, 9 | elevation float, 10 | base_demand_flow float, 11 | demand_pattern_id varchar, 12 | geom geometry('POINT',2154) 13 | ); 14 | 15 | INSERT INTO epanet.junctions 16 | (id, elevation, geom) 17 | VALUES 18 | ('0',0,ST_GeometryFromText('POINT(1 0)',2154)); 19 | 20 | INSERT INTO epanet.junctions 21 | (id, elevation, geom) 22 | VALUES 23 | ('1',1,ST_GeometryFromText('POINT(0 1)',2154)); 24 | 25 | CREATE TABLE epanet.pipes ( 26 | gid uuid DEFAULT uuid_generate_v4 (), 27 | pid serial PRIMARY KEY, 28 | id varchar, 29 | start_node varchar, 30 | end_node varchar, 31 | length float, 32 | diameter float, 33 | roughness float, 34 | minor_loss_coefficient float, 35 | status varchar, 36 | geom geometry('LINESTRING',2154) 37 | ); 38 | 39 | INSERT INTO epanet.pipes 40 | (id, start_node, end_node, length, diameter, geom) 41 | VALUES 42 | ('0','0','1',1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154)); 43 | 44 | -------------------------------------------------------------------------------- /test/history_creation_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import psycopg2 6 | import os 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 13 | tmp_dir = tempfile.gettempdir() 14 | 15 | # create the test database 16 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 17 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 19 | 20 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 21 | pcur.execute("CREATE SCHEMA epanet") 22 | pcur.execute(""" 23 | CREATE TABLE epanet.junctions ( 24 | hid serial PRIMARY KEY, 25 | id varchar, 26 | elevation float, 27 | base_demand_flow float, 28 | demand_pattern_id varchar, 29 | geom geometry('POINT',2154) 30 | )""") 31 | 32 | pcur.execute(""" 33 | INSERT INTO epanet.junctions 34 | (id, elevation, geom) 35 | VALUES 36 | ('0',0,ST_GeometryFromText('POINT(1 0)',2154))""") 37 | 38 | pcur.execute(""" 39 | INSERT INTO epanet.junctions 40 | (id, elevation, geom) 41 | VALUES 42 | ('1',1,ST_GeometryFromText('POINT(0 1)',2154))""") 43 | 44 | pcur.execute(""" 45 | CREATE TABLE epanet.pipes ( 46 | hid serial PRIMARY KEY, 47 | id varchar, 48 | start_node varchar, 49 | end_node varchar, 50 | length float, 51 | diameter float, 52 | roughness float, 53 | minor_loss_coefficient float, 54 | status varchar, 55 | geom geometry('LINESTRING',2154) 56 | )""") 57 | 58 | pcur.execute(""" 59 | INSERT INTO epanet.pipes 60 | (id, start_node, end_node, length, diameter, geom) 61 | VALUES 62 | ('0','0','1',1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154))""") 63 | 64 | pcur.commit() 65 | pcur.close() 66 | 67 | versioning.historize( pg_conn_info, 'epanet' ) 68 | 69 | failed = False 70 | try: 71 | versioning.add_branch( pg_conn_info, 'epanet', 'trunk' ) 72 | except: 73 | failed = True 74 | assert( failed ) 75 | 76 | failed = False 77 | try: 78 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'message', 'toto' ) 79 | except: 80 | failed = True 81 | assert( failed ) 82 | 83 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'test msg' ) 84 | 85 | 86 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 87 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.junctions") 88 | assert( len(pcur.fetchall()) == 2 ) 89 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.pipes") 90 | assert( len(pcur.fetchall()) == 1 ) 91 | 92 | ##versioning.add_revision_view( pg_conn_info, 'epanet', 'mybranch', 2) 93 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.junctions") 94 | ##assert( len(pcur.fetchall()) == 2 ) 95 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.pipes") 96 | ##assert( len(pcur.fetchall()) == 1 ) 97 | 98 | select_str, where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'junctions','mybranch', 2) 99 | pcur.execute(select_str + " WHERE " + where_str) 100 | assert( len(pcur.fetchall()) == 2 ) 101 | select_str, where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'pipes','mybranch', 2) 102 | pcur.execute(select_str + " WHERE " + where_str) 103 | assert( len(pcur.fetchall()) == 1 ) 104 | 105 | pcur.close() 106 | 107 | if __name__ == "__main__": 108 | if len(sys.argv) != 3: 109 | print("Usage: python3 versioning_base_test.py host pguser") 110 | else: 111 | test(*sys.argv[1:]) 112 | -------------------------------------------------------------------------------- /test/issue287_pg_dump.sql: -------------------------------------------------------------------------------- 1 | -- 2 | -- PostgreSQL database dump 3 | -- 4 | 5 | SET statement_timeout = 0; 6 | SET client_encoding = 'UTF8'; 7 | SET standard_conforming_strings = on; 8 | SET check_function_bodies = false; 9 | SET client_min_messages = warning; 10 | 11 | -- 12 | -- Name: epanet; Type: SCHEMA; Schema: - 13 | -- 14 | 15 | CREATE SCHEMA epanet; 16 | 17 | 18 | -- 19 | -- Name: epanet_trunk_rev_head; Type: SCHEMA; Schema: - 20 | -- 21 | 22 | CREATE SCHEMA epanet_trunk_rev_head; 23 | 24 | 25 | 26 | -- 27 | -- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: 28 | -- 29 | 30 | CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog; 31 | 32 | 33 | -- 34 | -- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner: 35 | -- 36 | 37 | COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language'; 38 | 39 | 40 | -- 41 | -- Name: postgis; Type: EXTENSION; Schema: -; Owner: 42 | -- 43 | 44 | CREATE EXTENSION IF NOT EXISTS postgis WITH SCHEMA public; 45 | 46 | 47 | -- 48 | -- Name: EXTENSION postgis; Type: COMMENT; Schema: -; Owner: 49 | -- 50 | 51 | COMMENT ON EXTENSION postgis IS 'PostGIS geometry, geography, and raster spatial types and functions'; 52 | 53 | 54 | SET search_path = epanet, pg_catalog; 55 | 56 | SET default_tablespace = ''; 57 | 58 | SET default_with_oids = false; 59 | 60 | -- 61 | -- Name: junctions; Type: TABLE; Schema: epanet; Tablespace: 62 | -- 63 | 64 | CREATE TABLE junctions ( 65 | id character varying, 66 | elevation double precision, 67 | base_demand_flow double precision, 68 | demand_pattern_id character varying, 69 | geom public.geometry(Point,2154), 70 | hid integer NOT NULL, 71 | trunk_rev_begin integer, 72 | trunk_rev_end integer, 73 | trunk_parent integer, 74 | trunk_child integer 75 | ); 76 | 77 | 78 | -- 79 | -- Name: junctions_hid_seq; Type: SEQUENCE; Schema: epanet 80 | -- 81 | 82 | CREATE SEQUENCE junctions_hid_seq 83 | START WITH 1 84 | INCREMENT BY 1 85 | NO MINVALUE 86 | NO MAXVALUE 87 | CACHE 1; 88 | 89 | 90 | -- 91 | -- Name: junctions_hid_seq; Type: SEQUENCE OWNED BY; Schema: epanet 92 | -- 93 | 94 | ALTER SEQUENCE junctions_hid_seq OWNED BY junctions.hid; 95 | 96 | 97 | -- 98 | -- Name: pipes; Type: TABLE; Schema: epanet; Tablespace: 99 | -- 100 | 101 | CREATE TABLE pipes ( 102 | id character varying, 103 | start_node character varying, 104 | end_node character varying, 105 | length double precision, 106 | diameter double precision, 107 | roughness double precision, 108 | minor_loss_coefficient double precision, 109 | status character varying, 110 | GEOMETRY public.geometry(LineString,2154), 111 | hid integer NOT NULL, 112 | trunk_rev_begin integer, 113 | trunk_rev_end integer, 114 | trunk_parent integer, 115 | trunk_child integer 116 | ); 117 | 118 | 119 | -- 120 | -- Name: pipes_hid_seq; Type: SEQUENCE; Schema: epanet 121 | -- 122 | 123 | CREATE SEQUENCE pipes_hid_seq 124 | START WITH 1 125 | INCREMENT BY 1 126 | NO MINVALUE 127 | NO MAXVALUE 128 | CACHE 1; 129 | 130 | 131 | -- 132 | -- Name: pipes_hid_seq; Type: SEQUENCE OWNED BY; Schema: epanet 133 | -- 134 | 135 | ALTER SEQUENCE pipes_hid_seq OWNED BY pipes.hid; 136 | 137 | 138 | -- 139 | -- Name: revisions; Type: TABLE; Schema: epanet; Tablespace: 140 | -- 141 | 142 | CREATE TABLE revisions ( 143 | rev integer NOT NULL, 144 | commit_msg character varying, 145 | branch character varying DEFAULT 'trunk'::character varying, 146 | date timestamp without time zone DEFAULT now(), 147 | author character varying 148 | ); 149 | 150 | 151 | -- 152 | -- Name: revisions_rev_seq; Type: SEQUENCE; Schema: epanet 153 | -- 154 | 155 | CREATE SEQUENCE revisions_rev_seq 156 | START WITH 1 157 | INCREMENT BY 1 158 | NO MINVALUE 159 | NO MAXVALUE 160 | CACHE 1; 161 | 162 | 163 | -- 164 | -- Name: revisions_rev_seq; Type: SEQUENCE OWNED BY; Schema: epanet 165 | -- 166 | 167 | ALTER SEQUENCE revisions_rev_seq OWNED BY revisions.rev; 168 | 169 | -- 170 | -- Name: versioning_constraints; Type: TABLE; Schema: epanet; 171 | -- 172 | 173 | CREATE TABLE epanet.versioning_constraints ( 174 | table_from character varying, 175 | columns_from character varying[], 176 | defaults_from character varying[], 177 | table_to character varying, 178 | columns_to character varying[], 179 | updtype character(1), 180 | deltype character(1) 181 | ); 182 | 183 | 184 | SET search_path = epanet_trunk_rev_head, pg_catalog; 185 | 186 | -- 187 | -- Name: junctions; Type: VIEW; Schema: epanet_trunk_rev_head 188 | -- 189 | 190 | CREATE VIEW junctions AS 191 | SELECT junctions.hid, junctions.id, junctions.elevation, junctions.base_demand_flow, junctions.demand_pattern_id, junctions.geom FROM epanet.junctions WHERE ((junctions.trunk_rev_end IS NULL) AND (junctions.trunk_rev_begin IS NOT NULL)); 192 | 193 | 194 | -- 195 | -- Name: pipes; Type: VIEW; Schema: epanet_trunk_rev_head 196 | -- 197 | 198 | CREATE VIEW pipes AS 199 | SELECT pipes.hid, pipes.id, pipes.start_node, pipes.end_node, pipes.length, pipes.diameter, pipes.roughness, pipes.minor_loss_coefficient, pipes.status, pipes.GEOMETRY FROM epanet.pipes WHERE ((pipes.trunk_rev_end IS NULL) AND (pipes.trunk_rev_begin IS NOT NULL)); 200 | 201 | 202 | SET search_path = epanet, pg_catalog; 203 | 204 | -- 205 | -- Name: hid; Type: DEFAULT; Schema: epanet 206 | -- 207 | 208 | ALTER TABLE ONLY junctions ALTER COLUMN hid SET DEFAULT nextval('junctions_hid_seq'::regclass); 209 | 210 | 211 | -- 212 | -- Name: hid; Type: DEFAULT; Schema: epanet 213 | -- 214 | 215 | ALTER TABLE ONLY pipes ALTER COLUMN hid SET DEFAULT nextval('pipes_hid_seq'::regclass); 216 | 217 | 218 | -- 219 | -- Name: rev; Type: DEFAULT; Schema: epanet 220 | -- 221 | 222 | ALTER TABLE ONLY revisions ALTER COLUMN rev SET DEFAULT nextval('revisions_rev_seq'::regclass); 223 | 224 | 225 | -- 226 | -- Data for Name: junctions; Type: TABLE DATA; Schema: epanet 227 | -- 228 | 229 | COPY junctions (id, elevation, base_demand_flow, demand_pattern_id, geom, hid, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child) FROM stdin; 230 | 0 0 \N \N 01010000206A080000000000000000F03F0000000000000000 1 1 \N \N \N 231 | 1 1 \N \N 01010000206A0800000000000000000000000000000000F03F 2 1 \N \N \N 232 | \. 233 | 234 | 235 | -- 236 | -- Name: junctions_hid_seq; Type: SEQUENCE SET; Schema: epanet 237 | -- 238 | 239 | SELECT pg_catalog.setval('junctions_hid_seq', 2, true); 240 | 241 | 242 | -- 243 | -- Data for Name: pipes; Type: TABLE DATA; Schema: epanet 244 | -- 245 | 246 | COPY pipes (id, start_node, end_node, length, diameter, roughness, minor_loss_coefficient, status, GEOMETRY, hid, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child) FROM stdin; 247 | 4 2 3 \N \N \N \N \N 01020000206A08000002000000B411C210B508D9BF1B0E49744D1DE53F9C84E785AEE3E53F4EA96C73A15FDDBF 4 3 \N \N \N 248 | 0 0 1 1 2 \N \N \N 01020000206A08000002000000BC139FE342F9DC3F56F191C62EFEE3BF2276308E5E83E1BF541DDC72A203D83F 5 3 \N 1 \N 249 | 0 0 1 1 2 \N \N \N 01020000206A08000002000000F8F3853CA84AF83FA6A0C22CC87CD83FF0E70B795095E03F2AA8300B321FF63F 3 2 2 1 5 250 | 0 0 1 1 2 \N \N \N 01020000206A08000002000000000000000000F03F00000000000000000000000000000000000000000000F03F 1 1 2 \N \N 251 | 1 3 3 \N \N \N \N \N 01020000206A08000002000000C28885E799B3E0BFE946DBC885D5E83FB4CB7990B55AE63F28A5CE11B68FE2BF 2 2 3 \N \N 252 | \. 253 | 254 | 255 | -- 256 | -- Name: pipes_hid_seq; Type: SEQUENCE SET; Schema: epanet 257 | -- 258 | 259 | SELECT pg_catalog.setval('pipes_hid_seq', 5, true); 260 | 261 | 262 | -- 263 | -- Data for Name: revisions; Type: TABLE DATA; Schema: epanet 264 | -- 265 | 266 | COPY revisions (rev, commit_msg, branch, date, author) FROM stdin; 267 | 1 initial commit trunk 2014-01-22 16:00:44.707214 \N 268 | 2 test trunk 2014-01-22 16:20:28.420197 vmo 269 | 3 test trunk 2014-01-22 16:21:28.685928 vmo 270 | 4 test trunk 2014-01-22 16:22:42.132123 vmo 271 | \. 272 | 273 | 274 | -- 275 | -- Name: revisions_rev_seq; Type: SEQUENCE SET; Schema: epanet 276 | -- 277 | 278 | SELECT pg_catalog.setval('revisions_rev_seq', 1, false); 279 | 280 | -- 281 | -- Data for Name: versioning_constraints; Type: TABLE DATA; Schema: epanet; 282 | -- 283 | 284 | COPY epanet.versioning_constraints (table_from, columns_from, defaults_from, table_to, columns_to, updtype, deltype) FROM stdin; 285 | junctions {id} {nextval('epanet.junctions_id_seq'::regclass)} \N \N 286 | pipes {id} {nextval('epanet.pipes_id_seq'::regclass)} \N \N 287 | pipes {start_node} {NULL} junctions {id} a a 288 | pipes {end_node} {NULL} junctions {id} a a 289 | \. 290 | 291 | 292 | SET search_path = public, pg_catalog; 293 | 294 | -- 295 | -- Data for Name: spatial_ref_sys; Type: TABLE DATA; Schema: public 296 | -- 297 | 298 | COPY spatial_ref_sys (srid, auth_name, auth_srid, srtext, proj4text) FROM stdin; 299 | \. 300 | 301 | 302 | SET search_path = epanet, pg_catalog; 303 | 304 | -- 305 | -- Name: junctions_pkey; Type: CONSTRAINT; Schema: epanet; Tablespace: 306 | -- 307 | 308 | ALTER TABLE ONLY junctions 309 | ADD CONSTRAINT junctions_pkey PRIMARY KEY (hid); 310 | 311 | 312 | -- 313 | -- Name: pipes_pkey; Type: CONSTRAINT; Schema: epanet; Tablespace: 314 | -- 315 | 316 | ALTER TABLE ONLY pipes 317 | ADD CONSTRAINT pipes_pkey PRIMARY KEY (hid); 318 | 319 | 320 | -- 321 | -- Name: revisions_pkey; Type: CONSTRAINT; Schema: epanet; Tablespace: 322 | -- 323 | 324 | ALTER TABLE ONLY revisions 325 | ADD CONSTRAINT revisions_pkey PRIMARY KEY (rev); 326 | 327 | 328 | -- 329 | -- Name: junctions_trunk_child_fkey; Type: FK CONSTRAINT; Schema: epanet 330 | -- 331 | 332 | ALTER TABLE ONLY junctions 333 | ADD CONSTRAINT junctions_trunk_child_fkey FOREIGN KEY (trunk_child) REFERENCES junctions(hid); 334 | 335 | 336 | -- 337 | -- Name: junctions_trunk_parent_fkey; Type: FK CONSTRAINT; Schema: epanet 338 | -- 339 | 340 | ALTER TABLE ONLY junctions 341 | ADD CONSTRAINT junctions_trunk_parent_fkey FOREIGN KEY (trunk_parent) REFERENCES junctions(hid); 342 | 343 | 344 | -- 345 | -- Name: junctions_trunk_rev_begin_fkey; Type: FK CONSTRAINT; Schema: epanet 346 | -- 347 | 348 | ALTER TABLE ONLY junctions 349 | ADD CONSTRAINT junctions_trunk_rev_begin_fkey FOREIGN KEY (trunk_rev_begin) REFERENCES revisions(rev); 350 | 351 | 352 | -- 353 | -- Name: junctions_trunk_rev_end_fkey; Type: FK CONSTRAINT; Schema: epanet 354 | -- 355 | 356 | ALTER TABLE ONLY junctions 357 | ADD CONSTRAINT junctions_trunk_rev_end_fkey FOREIGN KEY (trunk_rev_end) REFERENCES revisions(rev); 358 | 359 | 360 | -- 361 | -- Name: pipes_trunk_child_fkey; Type: FK CONSTRAINT; Schema: epanet 362 | -- 363 | 364 | ALTER TABLE ONLY pipes 365 | ADD CONSTRAINT pipes_trunk_child_fkey FOREIGN KEY (trunk_child) REFERENCES pipes(hid); 366 | 367 | 368 | -- 369 | -- Name: pipes_trunk_parent_fkey; Type: FK CONSTRAINT; Schema: epanet 370 | -- 371 | 372 | ALTER TABLE ONLY pipes 373 | ADD CONSTRAINT pipes_trunk_parent_fkey FOREIGN KEY (trunk_parent) REFERENCES pipes(hid); 374 | 375 | 376 | -- 377 | -- Name: pipes_trunk_rev_begin_fkey; Type: FK CONSTRAINT; Schema: epanet 378 | -- 379 | 380 | ALTER TABLE ONLY pipes 381 | ADD CONSTRAINT pipes_trunk_rev_begin_fkey FOREIGN KEY (trunk_rev_begin) REFERENCES revisions(rev); 382 | 383 | 384 | -- 385 | -- Name: pipes_trunk_rev_end_fkey; Type: FK CONSTRAINT; Schema: epanet 386 | -- 387 | 388 | ALTER TABLE ONLY pipes 389 | ADD CONSTRAINT pipes_trunk_rev_end_fkey FOREIGN KEY (trunk_rev_end) REFERENCES revisions(rev); 390 | 391 | 392 | -- 393 | -- Name: public; Type: ACL; Schema: -; Owner: postgres 394 | -- 395 | 396 | REVOKE ALL ON SCHEMA public FROM PUBLIC; 397 | REVOKE ALL ON SCHEMA public FROM postgres; 398 | GRANT ALL ON SCHEMA public TO postgres; 399 | GRANT ALL ON SCHEMA public TO PUBLIC; 400 | 401 | 402 | -- 403 | -- PostgreSQL database dump complete 404 | -- 405 | 406 | -------------------------------------------------------------------------------- /test/issue287_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import os 6 | import shutil 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | 13 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 14 | tmp_dir = tempfile.gettempdir() 15 | 16 | # create the test database 17 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 20 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/issue287_pg_dump.sql") 21 | 22 | # try the update 23 | sqlite_test_filename = os.path.join(tmp_dir, "issue287_wc.sqlite") 24 | shutil.copyfile(os.path.join(test_data_dir, "issue287_wc.sqlite"), sqlite_test_filename) 25 | spversioning = versioning.spatialite(sqlite_test_filename, pg_conn_info) 26 | spversioning.update() 27 | spversioning.commit("test message") 28 | 29 | if __name__ == "__main__": 30 | if len(sys.argv) != 3: 31 | print("Usage: python3 versioning_base_test.py host pguser") 32 | else: 33 | test(*sys.argv[1:]) 34 | -------------------------------------------------------------------------------- /test/issue287_wc.sqlite: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Oslandia/qgis-versioning/f6d5aa61f778bfa05c9470837845fa8f5f631b5f/test/issue287_wc.sqlite -------------------------------------------------------------------------------- /test/issue357_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from sqlite3 import dbapi2 5 | from versioningDB import versioning 6 | import os 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 13 | tmp_dir = tempfile.gettempdir() 14 | 15 | # create the test database 16 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 17 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 19 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 20 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 21 | 22 | # try the update 23 | wc = [os.path.join(tmp_dir, "issue357_wc0.sqlite"), os.path.join(tmp_dir, "issue357_wc1.sqlite")] 24 | spversioning0 = versioning.spatialite(wc[0], pg_conn_info) 25 | spversioning1 = versioning.spatialite(wc[1], pg_conn_info) 26 | for i, f in enumerate(wc): 27 | if os.path.isfile(f): os.remove(f) 28 | sp = spversioning0 if i == 0 else spversioning1 29 | sp.checkout(['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes']) 30 | 31 | scur = [] 32 | for f in wc: scur.append(versioning.Db( dbapi2.connect( f ) )) 33 | 34 | scur[0].execute("INSERT INTO pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',GeomFromText('LINESTRING(1 1,0 1)',2154))") 35 | scur[0].execute("INSERT INTO pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',GeomFromText('LINESTRING(1 -1,0 1)',2154))") 36 | scur[0].commit() 37 | 38 | 39 | spversioning0.commit( 'commit 1 wc0') 40 | spversioning1.update( ) 41 | 42 | scur[0].execute("UPDATE pipes_view SET length = 1") 43 | scur[0].commit() 44 | scur[1].execute("UPDATE pipes_view SET length = 2") 45 | scur[1].execute("UPDATE pipes_view SET length = 3") 46 | scur[1].commit() 47 | 48 | spversioning0.commit( "commit 2 wc0" ) 49 | scur[0].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 50 | print('################') 51 | for r in scur[0].fetchall(): 52 | print(r) 53 | 54 | scur[0].execute("UPDATE pipes_view SET length = 2") 55 | scur[0].execute("DELETE FROM pipes_view WHERE OGC_FID = 6") 56 | scur[0].commit() 57 | spversioning0.commit( "commit 3 wc0" ) 58 | 59 | scur[0].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 60 | print('################') 61 | for r in scur[0].fetchall(): 62 | print(r) 63 | 64 | spversioning1.update( ) 65 | 66 | scur[1].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes_diff") 67 | print('################ diff') 68 | for r in scur[1].fetchall(): 69 | print(r) 70 | 71 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 72 | assert( len(scur[1].fetchall()) == 6 ) # there must be conflicts 73 | 74 | scur[1].execute("SELECT conflict_id,origin,action,OGC_FID,trunk_parent,trunk_child FROM pipes_conflicts") 75 | print('################') 76 | for r in scur[1].fetchall(): 77 | print(r) 78 | 79 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='theirs' AND conflict_id=1") 80 | scur[1].commit() 81 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 82 | assert( len(scur[1].fetchall()) == 4 ) # there must be two removed entries 83 | 84 | scur[1].execute("SELECT conflict_id,origin,action,OGC_FID,trunk_parent,trunk_child FROM pipes_conflicts") 85 | print('################') 86 | for r in scur[1].fetchall(): 87 | print(r) 88 | 89 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='mine' AND OGC_FID = 11") 90 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='theirs'") 91 | scur[1].commit() 92 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 93 | assert( len(scur[1].fetchall()) == 0 ) # there must be no conflict 94 | 95 | 96 | scur[1].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 97 | print('################') 98 | for r in scur[1].fetchall(): 99 | print(r) 100 | 101 | if __name__ == "__main__": 102 | if len(sys.argv) != 3: 103 | print("Usage: python3 versioning_base_test.py host pguser") 104 | else: 105 | test(*sys.argv[1:]) 106 | -------------------------------------------------------------------------------- /test/issue358_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import os 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 13 | tmp_dir = tempfile.gettempdir() 14 | 15 | # create the test database 16 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 17 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 19 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 20 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 21 | 22 | # try the update 23 | wc = tmp_dir+"/issue358_wc.sqlite" 24 | if os.path.isfile(wc): os.remove(wc) 25 | spversioning = versioning.spatialite(wc, pg_conn_info) 26 | spversioning.checkout(['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes']) 27 | 28 | scur = versioning.Db( dbapi2.connect( wc ) ) 29 | 30 | scur.execute("SELECT * FROM pipes") 31 | assert( len(scur.fetchall()) == 1 ) 32 | scur.execute("UPDATE pipes_view SET length = 1 WHERE OGC_FID = 1") 33 | scur.execute("SELECT * FROM pipes") 34 | assert( len(scur.fetchall()) == 2 ) 35 | scur.execute("UPDATE pipes_view SET length = 2 WHERE OGC_FID = 2") 36 | scur.execute("SELECT * FROM pipes") 37 | assert( len(scur.fetchall()) == 2 ) 38 | 39 | if __name__ == "__main__": 40 | if len(sys.argv) != 3: 41 | print("Usage: python3 versioning_base_test.py host pguser") 42 | else: 43 | test(*sys.argv[1:]) 44 | -------------------------------------------------------------------------------- /test/issue437_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import os 7 | import tempfile 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | 13 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 14 | tmp_dir = tempfile.gettempdir() 15 | 16 | # create the test database 17 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 18 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 20 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 21 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 22 | 23 | # try the update 24 | wc = [os.path.join(tmp_dir,"issue437_wc0.sqlite"), os.path.join(tmp_dir,"issue437_wc1.sqlite")] 25 | spversioning0 = versioning.spatialite(wc[0], pg_conn_info) 26 | spversioning1 = versioning.spatialite(wc[1], pg_conn_info) 27 | for i, f in enumerate(wc): 28 | if os.path.isfile(f): os.remove(f) 29 | sp = spversioning0 if i == 0 else spversioning1 30 | sp.checkout(['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes']) 31 | 32 | scur = [] 33 | for f in wc: scur.append(versioning.Db( dbapi2.connect( f ) )) 34 | 35 | scur[0].execute("INSERT INTO pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',GeomFromText('LINESTRING(1 1,0 1)',2154))") 36 | scur[0].execute("INSERT INTO pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',GeomFromText('LINESTRING(1 -1,0 1)',2154))") 37 | scur[0].commit() 38 | 39 | 40 | spversioning0.commit( 'commit 1 wc0') 41 | spversioning1.update( ) 42 | 43 | scur[0].execute("UPDATE pipes_view SET length = 1") 44 | scur[0].commit() 45 | scur[1].execute("UPDATE pipes_view SET length = 2") 46 | scur[1].execute("UPDATE pipes_view SET length = 3") 47 | scur[1].commit() 48 | 49 | spversioning0.commit( "commit 2 wc0" ) 50 | scur[0].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 51 | print('################') 52 | for r in scur[0].fetchall(): 53 | print(r) 54 | 55 | scur[0].execute("UPDATE pipes_view SET length = 2") 56 | scur[0].execute("DELETE FROM pipes_view WHERE OGC_FID = 6") 57 | scur[0].commit() 58 | spversioning0.commit( "commit 3 wc0" ) 59 | 60 | scur[0].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 61 | print('################') 62 | for r in scur[0].fetchall(): 63 | print(r) 64 | 65 | spversioning1.update( ) 66 | 67 | scur[1].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes_diff") 68 | print('################ diff') 69 | for r in scur[1].fetchall(): 70 | print(r) 71 | 72 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 73 | assert( len(scur[1].fetchall()) == 6 ) # there must be conflicts 74 | 75 | scur[1].execute("SELECT conflict_id,origin,action,OGC_FID,trunk_parent,trunk_child FROM pipes_conflicts") 76 | print('################') 77 | for r in scur[1].fetchall(): 78 | print(r) 79 | 80 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='theirs' AND conflict_id=1") 81 | scur[1].commit() 82 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 83 | assert( len(scur[1].fetchall()) == 4 ) # there must be two removed entries 84 | 85 | scur[1].execute("SELECT conflict_id,origin,action,OGC_FID,trunk_parent,trunk_child FROM pipes_conflicts") 86 | print('################') 87 | for r in scur[1].fetchall(): 88 | print(r) 89 | 90 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='mine' AND OGC_FID = 11") 91 | scur[1].execute("DELETE FROM pipes_conflicts WHERE origin='theirs'") 92 | scur[1].commit() 93 | scur[1].execute("SELECT conflict_id FROM pipes_conflicts") 94 | assert( len(scur[1].fetchall()) == 0 ) # there must be no conflict 95 | 96 | 97 | scur[1].execute("SELECT OGC_FID,length,trunk_rev_begin,trunk_rev_end,trunk_parent,trunk_child FROM pipes") 98 | print('################') 99 | for r in scur[1].fetchall(): 100 | print(r) 101 | 102 | if __name__ == "__main__": 103 | if len(sys.argv) != 3: 104 | print("Usage: python3 versioning_base_test.py host pguser") 105 | else: 106 | test(*sys.argv[1:]) 107 | -------------------------------------------------------------------------------- /test/issue437_test_db.sql: -------------------------------------------------------------------------------- 1 | CREATE EXTENSION IF NOT EXISTS postgis; 2 | CREATE SCHEMA epanet; 3 | 4 | CREATE TABLE epanet.junctions ( 5 | id varchar, 6 | elevation float, 7 | base_demand_flow float, 8 | demand_pattern_id varchar, 9 | geom geometry('POINT',2154), 10 | ult_data timestamp 11 | ); 12 | 13 | INSERT INTO epanet.junctions 14 | (id, elevation, geom) 15 | VALUES 16 | ('0',0,ST_GeometryFromText('POINT(1 0)',2154)); 17 | 18 | INSERT INTO epanet.junctions 19 | (id, elevation, geom) 20 | VALUES 21 | ('1',1,ST_GeometryFromText('POINT(0 1)',2154)); 22 | 23 | CREATE TABLE epanet.pipes ( 24 | id varchar, 25 | start_node varchar, 26 | end_node varchar, 27 | length float, 28 | diameter float, 29 | roughness float, 30 | minor_loss_coefficient float, 31 | status varchar, 32 | geom geometry('LINESTRING',2154), 33 | ult_data timestamp 34 | ); 35 | 36 | INSERT INTO epanet.pipes 37 | (id, start_node, end_node, length, diameter, geom) 38 | VALUES 39 | ('0','0','1',1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154)); 40 | 41 | CREATE TABLE epanet.revisions( 42 | rev serial PRIMARY KEY, 43 | commit_msg varchar, 44 | branch varchar DEFAULT 'trunk', 45 | date timestamp DEFAULT current_timestamp, 46 | author varchar); 47 | INSERT INTO epanet.revisions VALUES (1,'initial commit','trunk'); 48 | 49 | ALTER TABLE epanet.junctions 50 | ADD COLUMN hid serial PRIMARY KEY, 51 | ADD COLUMN trunk_rev_begin integer REFERENCES epanet.revisions(rev), 52 | ADD COLUMN trunk_rev_end integer REFERENCES epanet.revisions(rev), 53 | ADD COLUMN trunk_parent integer REFERENCES epanet.junctions(hid), 54 | ADD COLUMN trunk_child integer REFERENCES epanet.junctions(hid); 55 | 56 | ALTER TABLE epanet.pipes 57 | ADD COLUMN hid serial PRIMARY KEY, 58 | ADD COLUMN trunk_rev_begin integer REFERENCES epanet.revisions(rev), 59 | ADD COLUMN trunk_rev_end integer REFERENCES epanet.revisions(rev), 60 | ADD COLUMN trunk_parent integer REFERENCES epanet.pipes(hid), 61 | ADD COLUMN trunk_child integer REFERENCES epanet.pipes(hid); 62 | 63 | UPDATE epanet.junctions SET trunk_rev_begin = 1; 64 | 65 | UPDATE epanet.pipes SET trunk_rev_begin = 1; 66 | 67 | CREATE SCHEMA epanet_trunk_rev_head; 68 | 69 | CREATE VIEW epanet_trunk_rev_head.junctions 70 | AS SELECT hid, id, elevation, base_demand_flow, demand_pattern_id, geom 71 | FROM epanet.junctions 72 | WHERE trunk_rev_end IS NULL AND trunk_rev_begin IS NOT NULL; 73 | 74 | CREATE VIEW epanet_trunk_rev_head.pipes 75 | AS SELECT hid, id, start_node, end_node, length, diameter, roughness, minor_loss_coefficient, status, geom 76 | FROM epanet.pipes 77 | WHERE trunk_rev_end IS NULL AND trunk_rev_begin IS NOT NULL; 78 | 79 | -------------------------------------------------------------------------------- /test/issue485_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import psycopg2 7 | import os 8 | import tempfile 9 | 10 | 11 | def test(host, pguser): 12 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 13 | 14 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 15 | tmp_dir = tempfile.gettempdir() 16 | 17 | # create the test database 18 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 20 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 21 | 22 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 23 | pcur.execute("CREATE SCHEMA epanet") 24 | pcur.execute(""" 25 | CREATE TABLE epanet.junctions ( 26 | id serial PRIMARY KEY, 27 | elevation float, 28 | base_demand_flow float, 29 | demand_pattern_id varchar, 30 | geometry geometry('POINT',2154), 31 | geometry_schematic geometry('POLYGON',2154) 32 | )""") 33 | 34 | pcur.execute(""" 35 | INSERT INTO epanet.junctions 36 | (elevation, geometry, geometry_schematic) 37 | VALUES 38 | (0,ST_GeometryFromText('POINT(0 0)',2154), 39 | ST_GeometryFromText('POLYGON((-1 -1,1 -1,1 1,-1 1,-1 -1))',2154))""") 40 | 41 | pcur.execute(""" 42 | INSERT INTO epanet.junctions 43 | (elevation, geometry, geometry_schematic) 44 | VALUES 45 | (1,ST_GeometryFromText('POINT(0 1)',2154), 46 | ST_GeometryFromText('POLYGON((0 0,2 0,2 2,0 2,0 0))',2154))""") 47 | 48 | pcur.execute(""" 49 | CREATE TABLE epanet.pipes ( 50 | id serial PRIMARY KEY, 51 | start_node varchar, 52 | end_node varchar, 53 | length float, 54 | diameter float, 55 | roughness float, 56 | minor_loss_coefficient float, 57 | status varchar, 58 | geometry geometry('LINESTRING',2154) 59 | )""") 60 | 61 | pcur.execute(""" 62 | INSERT INTO epanet.pipes 63 | (start_node, end_node, length, diameter, geometry) 64 | VALUES 65 | (1,2,1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154))""") 66 | 67 | pcur.commit() 68 | pcur.close() 69 | 70 | versioning.historize( pg_conn_info, 'epanet' ) 71 | 72 | failed = False 73 | try: 74 | versioning.add_branch( pg_conn_info, 'epanet', 'trunk' ) 75 | except: 76 | failed = True 77 | assert( failed ) 78 | 79 | failed = False 80 | try: 81 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'message', 'toto' ) 82 | except: 83 | failed = True 84 | assert( failed ) 85 | 86 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'test msg' ) 87 | 88 | 89 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 90 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.junctions") 91 | assert( len(pcur.fetchall()) == 2 ) 92 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.pipes") 93 | assert( len(pcur.fetchall()) == 1 ) 94 | 95 | ##versioning.add_revision_view( pg_conn_info, 'epanet', 'mybranch', 2) 96 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.junctions") 97 | ##assert( len(pcur.fetchall()) == 2 ) 98 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.pipes") 99 | ##assert( len(pcur.fetchall()) == 1 ) 100 | 101 | select_and_where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'junctions','mybranch', 2) 102 | print(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 103 | pcur.execute(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 104 | assert( len(pcur.fetchall()) == 2 ) 105 | select_and_where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'pipes','mybranch', 2) 106 | print(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 107 | pcur.execute(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 108 | assert( len(pcur.fetchall()) == 1 ) 109 | 110 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet.junctions") 111 | res = pcur.fetchall() 112 | assert( res[0][0] == 'POINT(0 0)' ) 113 | assert( res[1][1] == 'POLYGON((0 0,2 0,2 2,0 2,0 0))' ) 114 | 115 | 116 | wc = tmp_dir+'/wc_multiple_geometry_test.sqlite' 117 | if os.path.isfile(wc): os.remove(wc) 118 | spversioning = versioning.spatialite(wc, pg_conn_info) 119 | spversioning.checkout( ['epanet_trunk_rev_head.pipes','epanet_trunk_rev_head.junctions'] ) 120 | 121 | 122 | scur = versioning.Db( dbapi2.connect(wc) ) 123 | scur.execute("UPDATE junctions_view SET GEOMETRY = GeometryFromText('POINT(3 3)',2154) WHERE OGC_FID = 1") 124 | scur.commit() 125 | scur.close() 126 | spversioning.commit( 'moved a junction' ) 127 | 128 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet_trunk_rev_head.junctions ORDER BY versioning_id DESC") 129 | res = pcur.fetchall() 130 | for r in res: print(r) 131 | print("res={}".format(res[0][0])) 132 | assert( res[0][0] == 'POINT(3 3)' ) 133 | assert( res[0][1] == 'POLYGON((-1 -1,1 -1,1 1,-1 1,-1 -1))' ) 134 | 135 | pcur.close() 136 | 137 | # now we branch from head 138 | versioning.add_branch( pg_conn_info, 'epanet', 'b1', 'add branch b1' ) 139 | 140 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 141 | pcur.execute("SELECT versioning_id, trunk_rev_begin, trunk_rev_end, b1_rev_begin, b1_rev_end FROM epanet.junctions ORDER BY versioning_id") 142 | for r in pcur.fetchall(): print(r) 143 | pcur.close() 144 | 145 | # edit a little with a new wc 146 | os.remove(wc) 147 | spversioning.checkout( ['epanet_b1_rev_head.junctions'] ) 148 | 149 | scur = versioning.Db( dbapi2.connect(wc) ) 150 | scur.execute("UPDATE junctions_view SET GEOMETRY = GeometryFromText('POINT(4 4)',2154) WHERE OGC_FID = 3") 151 | scur.commit() 152 | scur.execute("PRAGMA table_info(junctions_view)") 153 | print("-----------------") 154 | for r in scur.fetchall(): print(r) 155 | scur.close() 156 | 157 | spversioning.commit( 'moved a junction') 158 | 159 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 160 | pcur.execute("SELECT versioning_id, trunk_rev_begin, trunk_rev_end, b1_rev_begin, b1_rev_end FROM epanet.junctions ORDER BY versioning_id") 161 | print("-----------------") 162 | for r in pcur.fetchall(): print(r) 163 | pcur.close() 164 | 165 | if __name__ == "__main__": 166 | if len(sys.argv) != 3: 167 | print("Usage: python3 versioning_base_test.py host pguser") 168 | else: 169 | test(*sys.argv[1:]) 170 | -------------------------------------------------------------------------------- /test/issue486_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | import sys 3 | sys.path.insert(0, '..') 4 | 5 | from versioningDB import versioning 6 | from sqlite3 import dbapi2 7 | import psycopg2 8 | import os 9 | import shutil 10 | import tempfile 11 | 12 | def test(host, pguser): 13 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 14 | 15 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 16 | tmp_dir = tempfile.gettempdir() 17 | 18 | # create the test database 19 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 20 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 21 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 22 | 23 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 24 | pcur.execute("CREATE SCHEMA epanet") 25 | pcur.execute(""" 26 | CREATE TABLE epanet.junctions ( 27 | hid serial PRIMARY KEY, 28 | id varchar, 29 | elevation float, 30 | base_demand_flow float, 31 | demand_pattern_id varchar, 32 | printmap integer[], 33 | geometry geometry('POINT',2154), 34 | geometry_schematic geometry('POLYGON',2154) 35 | )""") 36 | 37 | pcur.execute(""" 38 | INSERT INTO epanet.junctions 39 | (id, elevation, printmap, geometry, geometry_schematic) 40 | VALUES 41 | ('0',0,'{1,2,3}',ST_GeometryFromText('POINT(0 0)',2154), 42 | ST_GeometryFromText('POLYGON((-1 -1,1 -1,1 1,-1 1,-1 -1))',2154))""") 43 | 44 | pcur.execute(""" 45 | INSERT INTO epanet.junctions 46 | (id, elevation, printmap, geometry, geometry_schematic) 47 | VALUES 48 | ('1',1,'{}',ST_GeometryFromText('POINT(0 1)',2154), 49 | ST_GeometryFromText('POLYGON((0 0,2 0,2 2,0 2,0 0))',2154))""") 50 | 51 | pcur.execute(""" 52 | CREATE TABLE epanet.pipes ( 53 | hid serial PRIMARY KEY, 54 | id varchar, 55 | start_node varchar, 56 | end_node varchar, 57 | length float, 58 | diameter float, 59 | roughness float, 60 | minor_loss_coefficient float, 61 | status varchar, 62 | geometry geometry('LINESTRING',2154) 63 | )""") 64 | 65 | pcur.execute(""" 66 | INSERT INTO epanet.pipes 67 | (id, start_node, end_node, length, diameter, geometry) 68 | VALUES 69 | ('0','0','1',1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154))""") 70 | 71 | pcur.commit() 72 | pcur.close() 73 | 74 | versioning.historize( pg_conn_info, 'epanet' ) 75 | 76 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 77 | 78 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet_trunk_rev_head.junctions") 79 | res = pcur.fetchall() 80 | assert( res[0][0] == 'POINT(0 0)' ) 81 | assert( res[1][1] == 'POLYGON((0 0,2 0,2 2,0 2,0 0))' ) 82 | 83 | 84 | wc = tmp_dir+'/wc_multiple_geometry_test.sqlite' 85 | if os.path.isfile(wc): os.remove(wc) 86 | spversioning = versioning.spatialite(wc, pg_conn_info) 87 | spversioning.checkout( ['epanet_trunk_rev_head.pipes','epanet_trunk_rev_head.junctions'] ) 88 | 89 | 90 | scur = versioning.Db( dbapi2.connect(wc) ) 91 | scur.execute("UPDATE junctions_view SET GEOMETRY = GeometryFromText('POINT(3 3)',2154) WHERE OGC_FID = 1") 92 | scur.commit() 93 | scur.execute("SELECT * from junctions_view") 94 | print("--------------") 95 | for res in scur.fetchall(): print(res) 96 | scur.close() 97 | spversioning.commit( 'moved a junction' ) 98 | 99 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic), printmap FROM epanet_trunk_rev_head.junctions ORDER BY versioning_id DESC") 100 | res = pcur.fetchall() 101 | for r in res: print(r) 102 | assert( res[0][0] == 'POINT(3 3)' ) 103 | assert( res[0][1] == 'POLYGON((-1 -1,1 -1,1 1,-1 1,-1 -1))' ) 104 | assert( res[0][2] == [1, 2, 3] ) 105 | 106 | pcur.close() 107 | 108 | if __name__ == "__main__": 109 | if len(sys.argv) != 3: 110 | print("Usage: python3 versioning_base_test.py host pguser") 111 | else: 112 | test(*sys.argv[1:]) 113 | -------------------------------------------------------------------------------- /test/issue_type_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import os 7 | import tempfile 8 | import psycopg2 9 | 10 | 11 | def test(host, pguser): 12 | 13 | dbname = "epanet_test_db" 14 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 15 | sql_file = os.path.join(test_data_dir, "epanet_test_db.sql") 16 | tmp_dir = tempfile.gettempdir() 17 | 18 | # create the test database 19 | os.system(f"dropdb --if-exists -h {host} -U {pguser} {dbname}") 20 | os.system(f"createdb -h {host} -U {pguser} {dbname}") 21 | os.system(f"psql -h {host} -U {pguser} {dbname} -f {sql_file}") 22 | 23 | pg_conn_info = f"dbname={dbname} host={host} user={pguser}" 24 | 25 | pcon = psycopg2.connect(pg_conn_info) 26 | pcur = pcon.cursor() 27 | pcur.execute("CREATE TYPE type_example AS ENUM('TEST1', 'TEST2')") 28 | pcur.execute("ALTER TABLE epanet.junctions " 29 | "ADD COLUMN type_field type_example;") 30 | pcon.commit() 31 | 32 | versioning.historize(pg_conn_info, "epanet") 33 | 34 | # try the update 35 | wc = tmp_dir+"/issue_type.sqlite" 36 | if os.path.isfile(wc): 37 | os.remove(wc) 38 | 39 | spversioning = versioning.spatialite(wc, pg_conn_info) 40 | spversioning.checkout(['epanet_trunk_rev_head.junctions']) 41 | 42 | scur = versioning.Db(dbapi2.connect(wc)) 43 | 44 | # scur.execute("SELECT * FROM pipes") 45 | # assert( len(scur.fetchall()) == 1 ) 46 | scur.execute("UPDATE junctions_view " 47 | "SET type_field = 'TEST1' WHERE OGC_FID = 1") 48 | scur.commit() 49 | 50 | spversioning.commit("test type") 51 | 52 | pcon = psycopg2.connect(pg_conn_info) 53 | pcur = pcon.cursor() 54 | pcur.execute("SELECT type_field FROM epanet.junctions " 55 | "WHERE id = 1 AND trunk_rev_end IS NULL") 56 | 57 | res = pcur.fetchall() 58 | assert(len(res) == 1) 59 | assert(res[0][0] == "TEST1") 60 | 61 | 62 | if __name__ == "__main__": 63 | if len(sys.argv) != 3: 64 | print("Usage: python3 issue_type_test.py host pguser") 65 | else: 66 | test(*sys.argv[1:]) 67 | -------------------------------------------------------------------------------- /test/merge_branch_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # -*- coding: utf-8 -*- 3 | 4 | import sys 5 | from versioningDB import versioning 6 | import psycopg2 7 | import os 8 | 9 | 10 | def test(host, pguser): 11 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 12 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 13 | 14 | # create the test database 15 | 16 | os.system("dropdb --if-exists -h " + host + 17 | " -U "+pguser+" epanet_test_db") 18 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("psql -h " + host + " -U "+pguser + 20 | " epanet_test_db -c 'CREATE EXTENSION postgis'") 21 | os.system("psql -h " + host + " -U "+pguser + 22 | " epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 23 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 24 | 25 | # branch 26 | versioning.add_branch(pg_conn_info, "epanet", "mybranch", "add 'branch") 27 | 28 | # chechout from branch : epanet_brwcs_rev_head 29 | #tables = ['epanet_trunk_rev_head.junctions','epanet_trunk_rev_head.pipes'] 30 | tables = ['epanet_mybranch_rev_head.junctions', 31 | 'epanet_mybranch_rev_head.pipes'] 32 | pgversioning = versioning.pgServer(pg_conn_info, 'epanet_brwcs_rev_head') 33 | pgversioning.checkout(tables) 34 | 35 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 36 | 37 | # insert into epanet_brwcs_rev_head 38 | pcur.execute("INSERT INTO epanet_brwcs_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',ST_GeometryFromText('LINESTRING(1 1,0 1)',2154))") 39 | pcur.execute("INSERT INTO epanet_brwcs_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 40 | pcur.execute("DELETE FROM epanet_brwcs_rev_head.pipes_view WHERE id=3") 41 | pcur.commit() 42 | 43 | pgversioning.commit("commit", "postgres") 44 | 45 | versioning.merge(pg_conn_info, "epanet", "mybranch") 46 | 47 | pcur.execute("SELECT max(rev) FROM epanet.revisions") 48 | assert(pcur.fetchone()[0] == 4) 49 | 50 | pcur.execute( 51 | "SELECT rev, commit_msg, branch FROM epanet.revisions WHERE rev=4") 52 | assert(pcur.fetchall() == [ 53 | (4, 'Merge branch mybranch into trunk', 'trunk')]) 54 | 55 | pcur.execute( 56 | "SELECT versioning_id, trunk_rev_begin, trunk_rev_end, mybranch_rev_begin,mybranch_rev_end FROM epanet.pipes") 57 | assert(pcur.fetchall() == [(1, 1, None, 2, None), (2, 3, None, 3, None)]) 58 | 59 | pcur.close() 60 | 61 | 62 | if __name__ == "__main__": 63 | if len(sys.argv) != 3: 64 | print("Usage: python3 merge_branch_test.py host pguser") 65 | else: 66 | test(*sys.argv[1:]) 67 | -------------------------------------------------------------------------------- /test/multiple_geometry_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import psycopg2 7 | import os 8 | import tempfile 9 | 10 | 11 | def test(host, pguser): 12 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 13 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 14 | tmp_dir = tempfile.gettempdir() 15 | 16 | # create the test database 17 | 18 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 19 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 20 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 21 | 22 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 23 | pcur.execute("CREATE SCHEMA epanet") 24 | pcur.execute(""" 25 | CREATE TABLE epanet.junctions ( 26 | hid serial PRIMARY KEY, 27 | id varchar, 28 | elevation float, 29 | base_demand_flow float, 30 | demand_pattern_id varchar, 31 | geometry geometry('POINT',2154), 32 | geometry_schematic geometry('POLYGON',2154) 33 | )""") 34 | 35 | pcur.execute(""" 36 | INSERT INTO epanet.junctions 37 | (id, elevation, geometry, geometry_schematic) 38 | VALUES 39 | ('0',0,ST_GeometryFromText('POINT(0 0)',2154), 40 | ST_GeometryFromText('POLYGON((-1 -1,1 -1,1 1,-1 1,-1 -1))',2154))""") 41 | 42 | pcur.execute(""" 43 | INSERT INTO epanet.junctions 44 | (id, elevation, geometry, geometry_schematic) 45 | VALUES 46 | ('1',1,ST_GeometryFromText('POINT(0 1)',2154), 47 | ST_GeometryFromText('POLYGON((0 0,2 0,2 2,0 2,0 0))',2154))""") 48 | 49 | pcur.execute(""" 50 | CREATE TABLE epanet.pipes ( 51 | hid serial PRIMARY KEY, 52 | id varchar, 53 | start_node varchar, 54 | end_node varchar, 55 | length float, 56 | diameter float, 57 | roughness float, 58 | minor_loss_coefficient float, 59 | status varchar, 60 | geometry geometry('LINESTRING',2154) 61 | )""") 62 | 63 | pcur.execute(""" 64 | INSERT INTO epanet.pipes 65 | (id, start_node, end_node, length, diameter, geometry) 66 | VALUES 67 | ('0','0','1',1,2,ST_GeometryFromText('LINESTRING(1 0,0 1)',2154))""") 68 | 69 | pcur.commit() 70 | pcur.close() 71 | 72 | versioning.historize( pg_conn_info, 'epanet' ) 73 | 74 | failed = False 75 | try: 76 | versioning.add_branch( pg_conn_info, 'epanet', 'trunk' ) 77 | except: 78 | failed = True 79 | assert( failed ) 80 | 81 | failed = False 82 | try: 83 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'message', 'toto' ) 84 | except: 85 | failed = True 86 | assert( failed ) 87 | 88 | versioning.add_branch( pg_conn_info, 'epanet', 'mybranch', 'test msg' ) 89 | 90 | 91 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 92 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.junctions") 93 | assert( len(pcur.fetchall()) == 2 ) 94 | pcur.execute("SELECT * FROM epanet_mybranch_rev_head.pipes") 95 | assert( len(pcur.fetchall()) == 1 ) 96 | 97 | ##versioning.add_revision_view( pg_conn_info, 'epanet', 'mybranch', 2) 98 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.junctions") 99 | ##assert( len(pcur.fetchall()) == 2 ) 100 | ##pcur.execute("SELECT * FROM epanet_mybranch_rev_2.pipes") 101 | ##assert( len(pcur.fetchall()) == 1 ) 102 | 103 | select_and_where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'junctions','mybranch', 2) 104 | #print(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 105 | pcur.execute(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 106 | assert( len(pcur.fetchall()) == 2 ) 107 | select_and_where_str = versioning.rev_view_str( pg_conn_info, 'epanet', 'pipes','mybranch', 2) 108 | #print(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 109 | pcur.execute(select_and_where_str[0] + " WHERE " + select_and_where_str[1]) 110 | assert( len(pcur.fetchall()) == 1 ) 111 | 112 | ##pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet_mybranch_rev_2.junctions") 113 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet.junctions") 114 | res = pcur.fetchall() 115 | assert( res[0][0] == 'POINT(0 0)' ) 116 | assert( res[1][1] == 'POLYGON((0 0,2 0,2 2,0 2,0 0))' ) 117 | 118 | 119 | wc = os.path.join(tmp_dir, 'wc_multiple_geometry_test.sqlite') 120 | spversioning = versioning.spatialite(wc, pg_conn_info) 121 | if os.path.isfile(wc): os.remove(wc) 122 | spversioning.checkout( ['epanet_trunk_rev_head.pipes','epanet_trunk_rev_head.junctions'] ) 123 | 124 | 125 | scur = versioning.Db( dbapi2.connect(wc) ) 126 | scur.execute("UPDATE junctions_view SET GEOMETRY = GeometryFromText('POINT(3 3)',2154)") 127 | scur.commit() 128 | scur.close() 129 | 130 | spversioning.commit( 'a commit msg' ) 131 | 132 | pcur.execute("SELECT ST_AsText(geometry), ST_AsText(geometry_schematic) FROM epanet_trunk_rev_head.junctions") 133 | res = pcur.fetchall() 134 | for r in res: print(r) 135 | assert( res[0][0] == 'POINT(3 3)' ) 136 | assert( res[1][1] == 'POLYGON((0 0,2 0,2 2,0 2,0 0))' ) 137 | pcur.close() 138 | 139 | if __name__ == "__main__": 140 | if len(sys.argv) != 3: 141 | print("Usage: python3 versioning_base_test.py host pguser") 142 | else: 143 | test(*sys.argv[1:]) 144 | -------------------------------------------------------------------------------- /test/partial_checkout_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import psycopg2 7 | import os 8 | import tempfile 9 | 10 | tmp_dir = tempfile.gettempdir() 11 | sqlite_test_filename = os.path.join(tmp_dir, "partial_checkout_test.sqlite") 12 | 13 | 14 | class PartialCheckoutTest: 15 | 16 | def __init__(self, host, pguser, schema): 17 | 18 | self.schema = schema 19 | self.cur = None 20 | self.con = None 21 | self.versioning = None 22 | 23 | self.pg_conn_info = f"dbname=epanet_test_db host={host} user={pguser}" 24 | self.pg_conn_info_cpy = f"dbname=epanet_test_copy_db host={host} user={pguser}" 25 | 26 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 27 | 28 | # create the test database 29 | os.system(f"dropdb --if-exists -h {host} -U {pguser} epanet_test_db") 30 | os.system(f"dropdb --if-exists -h {host} -U {pguser} epanet_test_copy_db") 31 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 32 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_copy_db") 33 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f " 34 | + test_data_dir + "/epanet_test_db.sql") 35 | 36 | self.pcon = psycopg2.connect(self.pg_conn_info) 37 | self.pcur = self.pcon.cursor() 38 | for i in range(10): 39 | self.pcur.execute(""" 40 | INSERT INTO epanet.junctions 41 | (id, elevation, geom) 42 | VALUES 43 | ('{id}', {elev}, ST_GeometryFromText('POINT({x} {y})',2154)); 44 | """.format( 45 | id=i+3, 46 | elev=float(i), 47 | x=float(i+1), 48 | y=float(i+1) 49 | )) 50 | self.pcon.commit() 51 | 52 | versioning.historize(self.pg_conn_info, 'epanet') 53 | 54 | def __del__(self): 55 | if self.con: 56 | self.con.close() 57 | 58 | if self.pcon: 59 | self.pcon.close() 60 | 61 | def checkout(self, tables, feature_list): 62 | self.versioning.checkout(tables, feature_list) 63 | 64 | def test_select(self): 65 | 66 | self.checkout(["epanet_trunk_rev_head.junctions", 67 | "epanet_trunk_rev_head.pipes"], [[1, 2, 3], []]) 68 | 69 | self.cur.execute("SELECT elevation from {}.junctions_view".format( 70 | self.schema)) 71 | assert([res[0] for res in self.cur.fetchall()] == [0., 1., 0.]) 72 | 73 | def test_referenced(self): 74 | """ checkout table, its referenced table and the referenced 75 | features must appear""" 76 | 77 | self.checkout(["epanet_trunk_rev_head.pipes"], [[1]]) 78 | self.cur.execute("SELECT id from {}.pipes_view order by id".format( 79 | self.schema)) 80 | assert([res[0] for res in self.cur.fetchall()] == [1]) 81 | 82 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 83 | self.schema)) 84 | assert([res[0] for res in self.cur.fetchall()] == [1, 2]) 85 | 86 | def test_referenced_union(self): 87 | """ checkout table, its referenced table and the referenced 88 | features must appear, plus the one already selected""" 89 | 90 | self.checkout(["epanet_trunk_rev_head.pipes", 91 | "epanet_trunk_rev_head.junctions"], [[1], [6, 7]]) 92 | self.cur.execute("SELECT id from {}.pipes_view order by id".format( 93 | self.schema)) 94 | assert([res[0] for res in self.cur.fetchall()] == [1]) 95 | 96 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 97 | self.schema)) 98 | assert([res[0] for res in self.cur.fetchall()] == [1, 2, 6, 7]) 99 | 100 | def test_referencing(self): 101 | """ checkout table, its referencing table and the referencing 102 | features must appear""" 103 | 104 | self.checkout(["epanet_trunk_rev_head.junctions"], [[1, 6, 7]]) 105 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 106 | self.schema)) 107 | assert([res[0] for res in self.cur.fetchall()] == [1, 6, 7]) 108 | 109 | self.cur.execute("SELECT id from {}.pipes_view order by id".format( 110 | self.schema)) 111 | assert([res[0] for res in self.cur.fetchall()] == [1]) 112 | 113 | def test_duplicate_pkey_on_insert(self): 114 | 115 | self.checkout(["epanet_trunk_rev_head.junctions"], [[6, 7, 8]]) 116 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 117 | self.schema)) 118 | assert([res[0] for res in self.cur.fetchall()] == [6, 7, 8]) 119 | 120 | self.cur.execute(""" 121 | INSERT INTO {}.junctions_view (id, elevation, geom) VALUES 122 | (4, 40, ST_GeometryFromText('POINT(4 4)',2154)), 123 | (5, 50, ST_GeometryFromText('POINT(5 5)',2154))""".format( 124 | self.schema)) 125 | self.con.commit() 126 | 127 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 128 | self.schema)) 129 | assert([res[0] for res in self.cur.fetchall()] == [4, 5, 6, 7, 8]) 130 | 131 | self.con.rollback() 132 | 133 | try: 134 | self.versioning.commit("commit msg") 135 | assert(False and "Commit must fail unique constraint") 136 | except RuntimeError as e: 137 | print(e) 138 | self.con.rollback() 139 | 140 | # Check we have only one current instance of feature with id 5 141 | # and one with id 4 after commit 142 | self.pcur.execute("""SELECT COUNT(*) 143 | FROM epanet.junctions WHERE id = 5 AND trunk_rev_end IS NULL """) 144 | assert(self.pcur.fetchone()[0] == 1) 145 | 146 | self.pcur.execute("""SELECT COUNT(*) 147 | FROM epanet.junctions WHERE id = 4 AND trunk_rev_end IS NULL """) 148 | assert(self.pcur.fetchone()[0] == 1) 149 | 150 | def test_duplicate_pkey_on_update(self): 151 | 152 | self.checkout(["epanet_trunk_rev_head.junctions"], [[6, 7, 8]]) 153 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 154 | self.schema)) 155 | assert([res[0] for res in self.cur.fetchall()] == [6, 7, 8]) 156 | 157 | # Modify id with existing unique id 158 | self.cur.execute(""" 159 | UPDATE {}.junctions_view SET id = 3 WHERE id = 6""".format( 160 | self.schema)) 161 | self.cur.execute(""" 162 | UPDATE {}.junctions_view SET id = 4 WHERE id = 7""".format( 163 | self.schema)) 164 | self.con.commit() 165 | 166 | # Modify anything but id 167 | self.cur.execute(""" 168 | UPDATE {}.junctions_view SET elevation = 80 WHERE id = 8""".format( 169 | self.schema)) 170 | self.con.commit() 171 | 172 | self.cur.execute("SELECT id from {}.junctions_view order by id".format( 173 | self.schema)) 174 | assert([res[0] for res in self.cur.fetchall()] == [3, 4, 8]) 175 | 176 | self.con.rollback() 177 | 178 | try: 179 | self.versioning.commit("commit msg") 180 | assert(False and "Commit must fail unique constraint") 181 | except RuntimeError as e: 182 | print(e) 183 | self.con.rollback() 184 | 185 | # Check we have only one current instance of feature with id 3 186 | # and one with id 4 after commit 187 | self.pcur.execute("""SELECT COUNT(*) 188 | FROM epanet.junctions WHERE id = 3 AND trunk_rev_end IS NULL """) 189 | assert(self.pcur.fetchone()[0] == 1) 190 | 191 | self.pcur.execute("""SELECT COUNT(*) 192 | FROM epanet.junctions WHERE id = 4 AND trunk_rev_end IS NULL """) 193 | assert(self.pcur.fetchone()[0] == 1) 194 | 195 | 196 | class SpatialitePartialCheckoutTest(PartialCheckoutTest): 197 | 198 | def __init__(self, host, pguser): 199 | super().__init__(host, pguser, "main") 200 | 201 | if os.path.isfile(sqlite_test_filename): 202 | os.remove(sqlite_test_filename) 203 | 204 | self.versioning = versioning.spatialite(sqlite_test_filename, 205 | self.pg_conn_info) 206 | 207 | def checkout(self, tables, feature_list): 208 | 209 | super().checkout(tables, feature_list) 210 | 211 | self.con = dbapi2.connect(sqlite_test_filename) 212 | self.con.enable_load_extension(True) 213 | self.con.execute("SELECT load_extension('mod_spatialite')") 214 | self.cur = self.con.cursor() 215 | 216 | 217 | class PgServerPartialCheckoutTest(PartialCheckoutTest): 218 | 219 | def __init__(self, host, pguser): 220 | 221 | wc_schema = "epanet_workingcopy" 222 | super().__init__(host, pguser, wc_schema) 223 | 224 | self.versioning = versioning.pgServer(self.pg_conn_info, 225 | wc_schema) 226 | 227 | def checkout(self, tables, feature_list): 228 | super().checkout(tables, feature_list) 229 | self.con = self.pcon 230 | self.cur = self.pcur 231 | 232 | 233 | class PgLocalPartialCheckoutTest(PartialCheckoutTest): 234 | 235 | def __init__(self, host, pguser): 236 | 237 | wc_schema = "epanet_workingcopy" 238 | super().__init__(host, pguser, wc_schema) 239 | 240 | self.versioning = versioning.pgLocal( 241 | self.pg_conn_info, wc_schema, self.pg_conn_info_cpy) 242 | 243 | def checkout(self, tables, feature_list): 244 | super().checkout(tables, feature_list) 245 | 246 | self.con = psycopg2.connect(self.pg_conn_info_cpy) 247 | self.cur = self.con.cursor() 248 | 249 | 250 | def test(host, pguser): 251 | 252 | # loop on the 3 ways of checkout (sqlite, pgserver, pglocal) 253 | for test_class in [SpatialitePartialCheckoutTest, 254 | PgLocalPartialCheckoutTest, 255 | PgServerPartialCheckoutTest]: 256 | 257 | test = test_class(host, pguser) 258 | test.test_select() 259 | del test 260 | 261 | test = test_class(host, pguser) 262 | test.test_referenced() 263 | del test 264 | 265 | test = test_class(host, pguser) 266 | test.test_referenced_union() 267 | del test 268 | 269 | test = test_class(host, pguser) 270 | test.test_referencing() 271 | del test 272 | 273 | test = test_class(host, pguser) 274 | test.test_duplicate_pkey_on_insert() 275 | del test 276 | 277 | test = test_class(host, pguser) 278 | test.test_duplicate_pkey_on_update() 279 | del test 280 | 281 | 282 | if __name__ == "__main__": 283 | if len(sys.argv) != 3: 284 | print("Usage: python3 versioning_base_test.py host pguser") 285 | else: 286 | test(*sys.argv[1:]) 287 | -------------------------------------------------------------------------------- /test/plugin_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | from PyQt5.QtWidgets import QMessageBox 4 | from qgis.core import (QgsApplication, QgsVectorLayer, QgsProject) 5 | import sys 6 | import os 7 | import plugin 8 | import psycopg2 9 | 10 | dbname = "epanet_test_db" 11 | wc_dbname = "epanet_test_wc_db" 12 | schema = "epanet" 13 | wcs = "epanet_wc" 14 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 15 | sql_file = os.path.join(test_data_dir, "epanet_test_db.sql") 16 | 17 | # Monkey path GUI stuff 18 | # This not ideal to monkey patch too much things. It will be better to put 19 | # most of the GUI things in methods and to monkey patch these methods like 20 | # it's done with selectDatabase 21 | 22 | 23 | class EmptyObject(object): 24 | def __getattr__(self, name): 25 | return EmptyObject() 26 | 27 | def __call__(self, *args): 28 | return EmptyObject() 29 | 30 | 31 | def generate_tempfile(*args): 32 | return ("/tmp/plugin_test_file.sqlite", None) 33 | 34 | 35 | def warning(*args): 36 | print(args[2]) 37 | return QMessageBox.Ok 38 | 39 | 40 | class QLineEdit: 41 | 42 | def __init__(*args): 43 | pass 44 | 45 | def text(self): 46 | return wcs 47 | 48 | 49 | def return_wc_database(): 50 | return wc_dbname 51 | 52 | 53 | iface = EmptyObject() 54 | iface.mainWindow = EmptyObject() 55 | iface.layerTreeView = EmptyObject() 56 | plugin.QDialog = EmptyObject() 57 | plugin.uic.loadUi = EmptyObject() 58 | plugin.QFileDialog.getSaveFileName = generate_tempfile 59 | plugin.QMessageBox.warning = warning 60 | plugin.QVBoxLayout = EmptyObject() 61 | plugin.QDialogButtonBox = EmptyObject() 62 | plugin.QDialogButtonBox.Cancel = 0 63 | plugin.QDialogButtonBox.Ok = 0 64 | plugin.QLineEdit = QLineEdit 65 | 66 | 67 | class PluginTest: 68 | 69 | def __init__(self, host, pguser): 70 | 71 | self.host = host 72 | self.pguser = pguser 73 | 74 | # create the test database 75 | os.system(f"psql -h {host} -U {pguser} {dbname} -f {sql_file}") 76 | 77 | pg_conn_info = f"dbname={dbname} host={host} user={pguser}" 78 | pcon = psycopg2.connect(pg_conn_info) 79 | pcur = pcon.cursor() 80 | pcur.execute(""" 81 | INSERT INTO epanet.junctions (id, elevation, geom) 82 | VALUES (33, 30, ST_GeometryFromText('POINT(3 3)',2154)); 83 | """) 84 | pcur.execute(""" 85 | INSERT INTO epanet.junctions (id, elevation, geom) 86 | VALUES (44, 40, ST_GeometryFromText('POINT(4 4)',2154)); 87 | """) 88 | pcon.commit() 89 | pcon.close() 90 | 91 | # Initialize project 92 | layer_source = f"""host='{host}' dbname='{dbname}' user='{pguser}' 93 | srid=2154 table="epanet"."junctions" (geom) sql=""" 94 | j_layer = QgsVectorLayer(layer_source, "junctions", "postgres") 95 | assert(j_layer and j_layer.isValid() and 96 | j_layer.featureCount() == 4) 97 | assert(QgsProject.instance().addMapLayer(j_layer, False)) 98 | 99 | root = QgsProject.instance().layerTreeRoot() 100 | group = root.addGroup("epanet_group") 101 | group.addLayer(j_layer) 102 | 103 | self.versioning_plugin = plugin.Plugin(iface) 104 | self.versioning_plugin.current_layers = [j_layer] 105 | self.versioning_plugin.current_group = group 106 | 107 | self.historize() 108 | 109 | def historize(self): 110 | 111 | root = QgsProject.instance().layerTreeRoot() 112 | 113 | # historize 114 | self.versioning_plugin.historize() 115 | assert(len(root.children()) == 1) 116 | group = root.children()[0] 117 | assert(group.name() == "trunk revision head") 118 | j_layer = group.children()[0].layer() 119 | assert(j_layer.name() == "junctions") 120 | 121 | self.versioning_plugin.current_layers = [j_layer] 122 | self.versioning_plugin.current_group = group 123 | 124 | def test_checkout(self): 125 | 126 | root = QgsProject.instance().layerTreeRoot() 127 | 128 | # checkout 129 | self.checkout() 130 | assert(len(root.children()) == 2) 131 | group = root.children()[1] 132 | assert(group.name() == self.get_working_name()) 133 | 134 | j_layer = group.children()[0].layer() 135 | assert(j_layer.name() == "junctions") 136 | assert(j_layer.featureCount() == 4) 137 | 138 | root.takeChild(group) 139 | 140 | def test_checkout_w_selected_features(self): 141 | 142 | root = QgsProject.instance().layerTreeRoot() 143 | 144 | # select the 2 last features 145 | group = root.children()[0] 146 | j_layer = group.children()[0].layer() 147 | assert(j_layer.name() == "junctions") 148 | 149 | for feat in j_layer.getFeatures("id > 30"): 150 | j_layer.select(feat.id()) 151 | 152 | # checkout 153 | self.checkout() 154 | assert(len(root.children()) == 2) 155 | group = root.children()[1] 156 | assert(group.name() == self.get_working_name()) 157 | 158 | j_layer = group.children()[0].layer() 159 | assert(j_layer.name() == "junctions") 160 | fids = [feature['id'] for feature in j_layer.getFeatures()] 161 | print(f"fids={fids}") 162 | assert(fids == [33, 44]) 163 | 164 | root.takeChild(group) 165 | 166 | def __del__(self): 167 | QgsProject.instance().clear() 168 | 169 | for schema in ['epanet', 'epanet_trunk_rev_head']: 170 | os.system("psql -h {} -U {} {} -c 'DROP SCHEMA {} CASCADE'".format( 171 | self.host, self.pguser, dbname, schema)) 172 | 173 | def checkout(self): 174 | raise Exception("Must be overrided") 175 | 176 | def get_working_name(self): 177 | raise Exception("Must be overrided") 178 | 179 | 180 | class SpatialitePluginTest(PluginTest): 181 | 182 | def __init__(self, host, pguser): 183 | super().__init__(host, pguser) 184 | 185 | def checkout(self): 186 | self.versioning_plugin.checkout() 187 | 188 | def get_working_name(self): 189 | return "working copy" 190 | 191 | 192 | class PgServerPluginTest(PluginTest): 193 | 194 | def __init__(self, host, pguser): 195 | super().__init__(host, pguser) 196 | 197 | def checkout(self): 198 | self.versioning_plugin.checkout_pg() 199 | 200 | def get_working_name(self): 201 | return wcs 202 | 203 | def __del__(self): 204 | super().__del__() 205 | os.system("psql -h {} -U {} {} " 206 | "-c 'DROP SCHEMA {} CASCADE'".format( 207 | self.host, self.pguser, dbname, self.get_working_name())) 208 | 209 | 210 | class PgLocalPluginTest(PluginTest): 211 | 212 | def __init__(self, host, pguser): 213 | 214 | super().__init__(host, pguser) 215 | 216 | # Monkey patch the GUI to return database name 217 | self.versioning_plugin.selectDatabase = return_wc_database 218 | 219 | def checkout(self): 220 | self.versioning_plugin.checkout_pg_distant() 221 | 222 | def get_working_name(self): 223 | return "epanet_trunk_rev_head" 224 | 225 | def __del__(self): 226 | super().__del__() 227 | os.system("psql -h {} -U {} {} " 228 | "-c 'DROP SCHEMA {} CASCADE'".format( 229 | self.host, self.pguser, wc_dbname, 230 | self.get_working_name())) 231 | 232 | 233 | def test(host, pguser): 234 | 235 | # create the test database 236 | os.system(f"dropdb --if-exists -h {host} -U {pguser} {wc_dbname}") 237 | os.system(f"createdb -h {host} -U {pguser} {wc_dbname}") 238 | os.system(f"dropdb --if-exists -h {host} -U {pguser} {dbname}") 239 | os.system(f"createdb -h {host} -U {pguser} {dbname}") 240 | 241 | qgs = QgsApplication([], False) 242 | qgs.initQgis() 243 | 244 | for test_class in [SpatialitePluginTest, 245 | PgLocalPluginTest, 246 | PgServerPluginTest]: 247 | 248 | test = test_class(host, pguser) 249 | test.test_checkout() 250 | del test 251 | 252 | test = test_class(host, pguser) 253 | test.test_checkout_w_selected_features() 254 | del test 255 | 256 | qgs.exitQgis() 257 | 258 | 259 | if __name__ == "__main__": 260 | if len(sys.argv) != 3: 261 | print("Usage: python3 versioning_base_test.py host pguser") 262 | else: 263 | test(*sys.argv[1:]) 264 | -------------------------------------------------------------------------------- /test/posgres_working_copy_bug_in_conflict_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import psycopg2 6 | import os 7 | 8 | 9 | def prtTab( cur, tab ): 10 | print("--- ",tab," ---") 11 | cur.execute("SELECT versioning_id, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child, length FROM "+tab) 12 | for r in cur.fetchall(): 13 | t = [] 14 | for i in r: t.append(str(i)) 15 | print('\t| '.join(t)) 16 | 17 | def prtHid( cur, tab ): 18 | print("--- ",tab," ---") 19 | cur.execute("SELECT versioning_id FROM "+tab) 20 | for [r] in cur.fetchall(): print(r) 21 | 22 | def test(host, pguser): 23 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 24 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 25 | 26 | # create the test database 27 | 28 | for resolution in ['theirs','mine']: 29 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 30 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 31 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 32 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 33 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 34 | 35 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 36 | 37 | tables = ['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes'] 38 | pgversioning1 = versioning.pgServer(pg_conn_info, 'wc1') 39 | pgversioning2 = versioning.pgServer(pg_conn_info, 'wc2') 40 | pgversioning1.checkout(tables) 41 | pgversioning2.checkout(tables) 42 | print("checkout done") 43 | 44 | pcur.execute("UPDATE wc1.pipes_view SET length = 4 WHERE versioning_id = 1") 45 | prtTab( pcur, "wc1.pipes_diff") 46 | pcur.commit() 47 | #pcur.close() 48 | pgversioning1.commit("msg1") 49 | 50 | #pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 51 | 52 | print("commited") 53 | pcur.execute("UPDATE wc2.pipes_view SET length = 5 WHERE versioning_id = 1") 54 | prtTab( pcur, "wc2.pipes_diff") 55 | pcur.commit() 56 | pgversioning2.update() 57 | print("updated") 58 | prtTab( pcur, "wc2.pipes_diff") 59 | prtTab( pcur, "wc2.pipes_conflicts") 60 | 61 | pcur.execute("SELECT COUNT(*) FROM wc2.pipes_conflicts WHERE origin = 'mine'") 62 | assert( 1 == pcur.fetchone()[0] ) 63 | pcur.execute("SELECT COUNT(*) FROM wc2.pipes_conflicts WHERE origin = 'theirs'") 64 | assert( 1 == pcur.fetchone()[0] ) 65 | 66 | pcur.execute("DELETE FROM wc2.pipes_conflicts WHERE origin = '"+resolution+"'") 67 | prtTab( pcur, "wc2.pipes_conflicts") 68 | 69 | pcur.execute("SELECT COUNT(*) FROM wc2.pipes_conflicts") 70 | assert( 0 == pcur.fetchone()[0] ) 71 | pcur.close() 72 | 73 | 74 | if __name__ == "__main__": 75 | if len(sys.argv) != 3: 76 | print("Usage: python3 versioning_base_test.py host pguser") 77 | else: 78 | test(*sys.argv[1:]) 79 | -------------------------------------------------------------------------------- /test/posgres_working_copy_bug_in_view_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import psycopg2 6 | import os 7 | 8 | 9 | def prtTab( cur, tab ): 10 | print("--- ",tab," ---") 11 | cur.execute("SELECT versioning_id, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child, length FROM "+tab) 12 | for r in cur.fetchall(): 13 | t = [] 14 | for i in r: t.append(str(i)) 15 | print('\t| '.join(t)) 16 | 17 | def prtHid( cur, tab ): 18 | print("--- ",tab," ---") 19 | cur.execute("SELECT versioning_id FROM "+tab) 20 | for [r] in cur.fetchall(): print(r) 21 | 22 | def test(host, pguser): 23 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 24 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 25 | 26 | # create the test database 27 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 28 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 29 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 30 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 31 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 32 | 33 | # checkout 34 | pgversioning = versioning.pgServer(pg_conn_info, 'epanet_working_copy') 35 | pgversioning.checkout(['epanet_trunk_rev_head.junctions','epanet_trunk_rev_head.pipes']) 36 | 37 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 38 | 39 | pcur.execute("UPDATE epanet_working_copy.pipes_view SET length = 4 WHERE id = 1") 40 | prtTab(pcur, 'epanet_working_copy.pipes_diff') 41 | 42 | prtHid( pcur, 'epanet_working_copy.pipes_view') 43 | pcur.execute("SElECT COUNT(id) FROM epanet_working_copy.pipes_view") 44 | assert(1 == pcur.fetchone()[0]) 45 | 46 | if __name__ == "__main__": 47 | if len(sys.argv) != 3: 48 | print("Usage: python3 versioning_base_test.py host pguser") 49 | else: 50 | test(*sys.argv[1:]) 51 | -------------------------------------------------------------------------------- /test/posgres_working_copy_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import psycopg2 6 | import os 7 | 8 | 9 | def prtTab( cur, tab ): 10 | print("--- ",tab," ---") 11 | cur.execute("SELECT versioning_id, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child, length FROM "+tab) 12 | for r in cur.fetchall(): 13 | t = [] 14 | for i in r: t.append(str(i)) 15 | print('\t| '.join(t)) 16 | 17 | def prtHid( cur, tab ): 18 | print("--- ",tab," ---") 19 | cur.execute("SELECT versioning_id FROM "+tab) 20 | for [r] in cur.fetchall(): print(r) 21 | 22 | def test(host, pguser): 23 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 24 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 25 | 26 | # create the test database 27 | 28 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 29 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 30 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 31 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 32 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 33 | 34 | # chechout 35 | #tables = ['epanet_trunk_rev_head.junctions','epanet_trunk_rev_head.pipes'] 36 | tables = ['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes'] 37 | pgversioning1 = versioning.pgServer(pg_conn_info, 'epanet_working_copy') 38 | pgversioning2 = versioning.pgServer(pg_conn_info, 'epanet_working_copy_cflt') 39 | pgversioning1.checkout(tables) 40 | 41 | pgversioning2.checkout(tables) 42 | 43 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 44 | 45 | 46 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',ST_GeometryFromText('LINESTRING(1 1,0 1)',2154))") 47 | pcur.execute("INSERT INTO epanet_working_copy.pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 48 | pcur.commit() 49 | 50 | 51 | prtHid(pcur, 'epanet_working_copy.pipes_view') 52 | 53 | pcur.execute("SELECT versioning_id FROM epanet_working_copy.pipes_view") 54 | assert( len(pcur.fetchall()) == 3 ) 55 | pcur.execute("SELECT versioning_id FROM epanet_working_copy.pipes_diff") 56 | assert( len(pcur.fetchall()) == 2 ) 57 | pcur.execute("SELECT versioning_id FROM epanet.pipes") 58 | assert( len(pcur.fetchall()) == 1 ) 59 | 60 | 61 | prtTab(pcur, 'epanet.pipes') 62 | prtTab(pcur, 'epanet_working_copy.pipes_diff') 63 | pcur.execute("UPDATE epanet_working_copy.pipes_view SET length = 4 WHERE versioning_id = 1") 64 | prtTab(pcur, 'epanet_working_copy.pipes_diff') 65 | pcur.execute("UPDATE epanet_working_copy.pipes_view SET length = 5 WHERE versioning_id = 4") 66 | prtTab(pcur, 'epanet_working_copy.pipes_diff') 67 | 68 | pcur.execute("DELETE FROM epanet_working_copy.pipes_view WHERE versioning_id = 4") 69 | prtTab(pcur, 'epanet_working_copy.pipes_diff') 70 | pcur.commit() 71 | 72 | pgversioning1.commit("test commit msg") 73 | prtTab(pcur, 'epanet.pipes') 74 | 75 | pcur.execute("SELECT trunk_rev_end FROM epanet.pipes WHERE versioning_id = 1") 76 | assert( 1 == pcur.fetchone()[0] ) 77 | pcur.execute("SELECT COUNT(*) FROM epanet.pipes WHERE trunk_rev_begin = 2") 78 | assert( 2 == pcur.fetchone()[0] ) 79 | 80 | 81 | # modify the second working copy to create conflict 82 | prtTab(pcur, 'epanet.pipes') 83 | pcur.execute("SELECT * FROM epanet_working_copy_cflt.initial_revision") 84 | print('-- epanet_working_copy_cflt.initial_revision ---') 85 | for r in pcur.fetchall(): print(r) 86 | 87 | prtHid(pcur, 'epanet_working_copy_cflt.pipes_view') 88 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 89 | pcur.execute("UPDATE epanet_working_copy_cflt.pipes_view SET length = 8 WHERE versioning_id = 1") 90 | pcur.commit() 91 | prtTab(pcur, 'epanet.pipes') 92 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 93 | pcur.execute("SELECT COUNT(*) FROM epanet_working_copy_cflt.pipes_diff") 94 | for l in pcur.con.notices: print(l) 95 | assert( 2 == pcur.fetchone()[0] ) 96 | 97 | 98 | pcur.execute("INSERT INTO epanet_working_copy_cflt.pipes_view(id, start_node, end_node, geom) VALUES (4,'1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 99 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 100 | pcur.commit() 101 | pgversioning2.update( ) 102 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 103 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_update_diff') 104 | 105 | pcur.execute("SELECT COUNT(*) FROM epanet_working_copy_cflt.pipes_conflicts") 106 | assert( 2 == pcur.fetchone()[0] ) 107 | pcur.execute("SELECT COUNT(*) FROM epanet_working_copy_cflt.pipes_conflicts WHERE origin = 'mine'") 108 | assert( 1 == pcur.fetchone()[0] ) 109 | pcur.execute("SELECT COUNT(*) FROM epanet_working_copy_cflt.pipes_conflicts WHERE origin = 'theirs'") 110 | assert( 1 == pcur.fetchone()[0] ) 111 | 112 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_conflicts') 113 | 114 | pcur.execute("DELETE FROM epanet_working_copy_cflt.pipes_conflicts WHERE origin = 'theirs'") 115 | pcur.execute("SELECT COUNT(*) FROM epanet_working_copy_cflt.pipes_conflicts") 116 | assert( 0 == pcur.fetchone()[0] ) 117 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 118 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_conflicts') 119 | pcur.commit() 120 | 121 | pgversioning2.commit("second test commit msg") 122 | 123 | 124 | pcur.execute("SELECT * FROM epanet_working_copy_cflt.initial_revision") 125 | print('-- epanet_working_copy_cflt.initial_revision ---') 126 | for r in pcur.fetchall(): print(r) 127 | 128 | prtHid(pcur, 'epanet_working_copy_cflt.pipes_view') 129 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 130 | 131 | pcur.execute("UPDATE epanet_working_copy_cflt.pipes_view SET length = 8") 132 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 133 | pcur.commit() 134 | 135 | pgversioning2.commit("third test commit msg") 136 | 137 | 138 | prtTab(pcur, 'epanet_working_copy_cflt.pipes_diff') 139 | pcur.execute("UPDATE epanet_working_copy_cflt.pipes_view SET length = 12") 140 | pcur.commit() 141 | 142 | if __name__ == "__main__": 143 | if len(sys.argv) != 3: 144 | print("Usage: python3 versioning_base_test.py host pguser") 145 | else: 146 | test(*sys.argv[1:]) 147 | -------------------------------------------------------------------------------- /test/postgres_distant_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | from sqlite3 import dbapi2 6 | import psycopg2 7 | import os 8 | import tempfile 9 | 10 | 11 | def prtTab( cur, tab ): 12 | print("--- ",tab," ---") 13 | cur.execute("SELECT ogc_fid, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child, length FROM "+tab) 14 | for r in cur.fetchall(): 15 | t = [] 16 | for i in r: t.append(str(i)) 17 | print('\t| '.join(t)) 18 | 19 | def prtHid( cur, tab ): 20 | print("--- ",tab," ---") 21 | cur.execute("SELECT ogc_fid FROM "+tab) 22 | for [r] in cur.fetchall(): print(r) 23 | 24 | def test(host, pguser): 25 | 26 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 27 | pg_conn_info_cpy = "dbname=epanet_test_copy_db host=" + host + " user=" + pguser 28 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 29 | tmp_dir = tempfile.gettempdir() 30 | 31 | # create the test database 32 | 33 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 34 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_copy_db") 35 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 36 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_copy_db") 37 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 38 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 39 | 40 | # chechout 41 | #tables = ['epanet_trunk_rev_head.junctions','epanet_trunk_rev_head.pipes'] 42 | tables = ['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes'] 43 | pgversioning = versioning.pgLocal(pg_conn_info, 'epanet_trunk_rev_head', pg_conn_info_cpy) 44 | pgversioning.checkout(tables) 45 | 46 | pcurcpy = versioning.Db(psycopg2.connect(pg_conn_info_cpy)) 47 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 48 | 49 | pcurcpy.execute("INSERT INTO epanet_trunk_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',ST_GeometryFromText('LINESTRING(1 1,0 1)',2154))") 50 | pcurcpy.execute("INSERT INTO epanet_trunk_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 51 | pcurcpy.commit() 52 | 53 | 54 | prtHid(pcurcpy, 'epanet_trunk_rev_head.pipes_view') 55 | 56 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 57 | assert( len(pcurcpy.fetchall()) == 3 ) 58 | pcur.execute("SELECT * FROM epanet.pipes") 59 | assert( len(pcur.fetchall()) == 1 ) 60 | pgversioning.commit('INSERT') 61 | pcur.execute("SELECT * FROM epanet.pipes") 62 | assert( len(pcur.fetchall()) == 3 ) 63 | 64 | pcurcpy.execute("UPDATE epanet_trunk_rev_head.pipes_view SET start_node = '2' WHERE id = '1'") 65 | pcurcpy.commit() 66 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 67 | assert( len(pcurcpy.fetchall()) == 3 ) 68 | pcur.execute("SELECT * FROM epanet.pipes") 69 | assert( len(pcur.fetchall())== 3 ) 70 | pgversioning.commit('UPDATE') 71 | pcur.execute("SELECT * FROM epanet.pipes") 72 | assert( len(pcur.fetchall()) == 4 ) 73 | 74 | pcurcpy.execute("DELETE FROM epanet_trunk_rev_head.pipes_view WHERE id = '2'") 75 | pcurcpy.commit() 76 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 77 | assert( len(pcurcpy.fetchall()) == 2 ) 78 | pcur.execute("SELECT * FROM epanet.pipes") 79 | assert( len(pcur.fetchall()) == 4 ) 80 | pgversioning.commit('DELETE') 81 | pcur.execute("SELECT * FROM epanet.pipes") 82 | assert( len(pcur.fetchall()) == 4 ) 83 | 84 | sqlite_test_filename1 = os.path.join(tmp_dir, "versioning_base_test1.sqlite") 85 | if os.path.isfile(sqlite_test_filename1): os.remove(sqlite_test_filename1) 86 | spversioning1 = versioning.spatialite(sqlite_test_filename1, pg_conn_info) 87 | spversioning1.checkout( ['epanet_trunk_rev_head.pipes','epanet_trunk_rev_head.junctions'] ) 88 | scon = dbapi2.connect(sqlite_test_filename1) 89 | scon.enable_load_extension(True) 90 | scon.execute("SELECT load_extension('mod_spatialite')") 91 | scur = scon.cursor() 92 | scur.execute("INSERT INTO pipes_view(id, start_node, end_node, geom) VALUES (4, 1, 2,ST_GeometryFromText('LINESTRING(2 0, 0 2)',2154))") 93 | scon.commit() 94 | spversioning1.commit("sp commit") 95 | 96 | pgversioning.update( ) 97 | pcur.execute("SELECT * FROM epanet.pipes") 98 | assert( len(pcur.fetchall()) == 5 ) 99 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes") 100 | assert( len(pcurcpy.fetchall()) == 5 ) 101 | 102 | pcur.execute("SELECT * FROM epanet_trunk_rev_head.pipes") 103 | assert( len(pcur.fetchall()) == 3 ) 104 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 105 | assert( len(pcurcpy.fetchall()) == 3 ) 106 | 107 | pcur.execute("SELECT versioning_id FROM epanet_trunk_rev_head.pipes ORDER BY versioning_id") 108 | ret = pcur.fetchall() 109 | 110 | assert([i[0] for i in ret] == [3, 4, 5]) 111 | pcurcpy.execute("SELECT ogc_fid FROM epanet_trunk_rev_head.pipes_view ORDER BY ogc_fid") 112 | ret = pcurcpy.fetchall() 113 | assert([i[0] for i in ret] == [3, 4, 5]) 114 | 115 | pcurcpy.execute("INSERT INTO epanet_trunk_rev_head.pipes_view(id, start_node, end_node, geom) VALUES (5,'1','2',ST_GeometryFromText('LINESTRING(3 2,0 1)',2154))") 116 | pcurcpy.commit() 117 | pgversioning.commit('INSERT AFTER UPDATE') 118 | 119 | 120 | pcurcpy.close() 121 | pcur.close() 122 | 123 | if __name__ == "__main__": 124 | if len(sys.argv) != 3: 125 | print("Usage: python3 postgres_distant_test.py host pguser") 126 | else: 127 | test(*sys.argv[1:]) 128 | -------------------------------------------------------------------------------- /test/postgres_distant_uuid_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB import versioning 5 | import psycopg2 6 | import os 7 | import tempfile 8 | 9 | 10 | def prtTab( cur, tab ): 11 | print("--- ",tab," ---") 12 | cur.execute("SELECT ogc_fid, trunk_rev_begin, trunk_rev_end, trunk_parent, trunk_child, length FROM "+tab) 13 | for r in cur.fetchall(): 14 | t = [] 15 | for i in r: t.append(str(i)) 16 | print('\t| '.join(t)) 17 | 18 | def prtHid( cur, tab ): 19 | print("--- ",tab," ---") 20 | cur.execute("SELECT ogc_fid FROM "+tab) 21 | for [r] in cur.fetchall(): print(r) 22 | 23 | def test(host, pguser): 24 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 25 | pg_conn_info_cpy = "dbname=epanet_test_copy_db host=" + host + " user=" + pguser 26 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 27 | tmp_dir = tempfile.gettempdir() 28 | 29 | # create the test database 30 | 31 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 32 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_copy_db") 33 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 34 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_copy_db") 35 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -c 'CREATE EXTENSION postgis'") 36 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_copy_db -c 'CREATE EXTENSION postgis'") 37 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 38 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 39 | 40 | # checkout 41 | tables = ['epanet_trunk_rev_head.junctions', 'epanet_trunk_rev_head.pipes'] 42 | 43 | pgversioning = versioning.pgLocal(pg_conn_info, 'epanet_trunk_rev_head', pg_conn_info_cpy) 44 | pgversioning.checkout(tables) 45 | 46 | 47 | pcurcpy = versioning.Db(psycopg2.connect(pg_conn_info_cpy)) 48 | pcur = versioning.Db(psycopg2.connect(pg_conn_info)) 49 | 50 | 51 | pcurcpy.execute("INSERT INTO epanet_trunk_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('2','1','2',ST_GeometryFromText('LINESTRING(1 1,0 1)',2154))") 52 | pcurcpy.execute("INSERT INTO epanet_trunk_rev_head.pipes_view(id, start_node, end_node, geom) VALUES ('3','1','2',ST_GeometryFromText('LINESTRING(1 -1,0 1)',2154))") 53 | pcurcpy.commit() 54 | 55 | 56 | prtHid(pcurcpy, 'epanet_trunk_rev_head.pipes_view') 57 | 58 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 59 | assert( len(pcurcpy.fetchall()) == 3 ) 60 | pcur.execute("SELECT * FROM epanet.pipes") 61 | assert( len(pcur.fetchall()) == 1 ) 62 | pgversioning.commit('INSERT') 63 | pcur.execute("SELECT * FROM epanet.pipes") 64 | assert( len(pcur.fetchall()) == 3 ) 65 | 66 | pcurcpy.execute("UPDATE epanet_trunk_rev_head.pipes_view SET start_node = 2 WHERE id = 1") 67 | pcurcpy.commit() 68 | pcurcpy.execute("SELECT * FROM epanet_trunk_rev_head.pipes_view") 69 | assert( len(pcurcpy.fetchall()) == 3 ) 70 | pcur.execute("SELECT * FROM epanet.pipes") 71 | assert( len(pcur.fetchall())== 3 ) 72 | pgversioning.commit('UPDATE') 73 | pcur.execute("SELECT * FROM epanet.pipes") 74 | assert( len(pcur.fetchall()) == 4 ) 75 | 76 | pcurcpy.close() 77 | pcur.close() 78 | 79 | if __name__ == "__main__": 80 | if len(sys.argv) != 3: 81 | print("Usage: python3 postgres_distant_uuid_test.py host pguser") 82 | else: 83 | test(*sys.argv[1:]) 84 | -------------------------------------------------------------------------------- /test/run_tests.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | export DISPLAY=:99 4 | Xvfb :99& 5 | 6 | service postgresql start 7 | 8 | python3 tests.py 127.0.0.1 postgres -v 9 | -------------------------------------------------------------------------------- /test/tests.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | # coding = utf-8 4 | 5 | import os 6 | import sys 7 | 8 | from subprocess import Popen, PIPE 9 | 10 | 11 | def test(host, pguser, verbose=True): 12 | __current_dir = os.path.abspath(os.path.dirname(__file__)) 13 | plugin_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 14 | 15 | tests = [os.path.join(__current_dir,file_) 16 | for file_ in os.listdir(__current_dir) 17 | if file_[-8:]=="_test.py"] 18 | 19 | failed = 0 20 | env = os.environ.copy() 21 | env['PYTHONPATH'] = plugin_dir + (":" + env['PYTHONPATH'] if 'PYTHONPATH' 22 | in env else "") 23 | for i, test in enumerate(tests): 24 | sys.stdout.write("% 4d/%d %s %s"%( 25 | i+1, len(tests), test, "."*max(0, (80-len(test))))) 26 | sys.stdout.flush() 27 | child = Popen([sys.executable, test, host, pguser], stdout=PIPE, 28 | stderr=PIPE, env=env) 29 | out, err = child.communicate() 30 | if child.returncode: 31 | sys.stdout.write("failed\n") 32 | if verbose: 33 | sys.stdout.write(err.decode("utf-8")) 34 | failed += 1 35 | else: 36 | sys.stdout.write("ok\n") 37 | 38 | if failed: 39 | sys.stderr.write("%d/%d test failed\n"%(failed, len(tests))) 40 | raise RuntimeError("%d/%d test failed\n"%(failed, len(tests))) 41 | else: 42 | sys.stdout.write("%d/%d test passed (%d%%)\n"%( 43 | len(tests)-failed, 44 | len(tests), 45 | int((100.*(len(tests)-failed))/len(tests)))) 46 | 47 | 48 | if __name__=="__main__": 49 | if len(sys.argv) <= 2 or len(sys.argv) > 4: 50 | print("Usage: python3 tests.py HOST PGUSER [-v]") 51 | exit(0) 52 | 53 | verbose = False 54 | if len(sys.argv) == 4 and sys.argv[3] == '-v': 55 | verbose = True 56 | 57 | test(sys.argv[1], sys.argv[2], verbose) 58 | -------------------------------------------------------------------------------- /test/versioning_base_test.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import sys 4 | from versioningDB.versioning import diff_rev_view_str 5 | from versioningDB import versioning 6 | from sqlite3 import dbapi2 7 | import psycopg2 8 | import os 9 | import tempfile 10 | 11 | 12 | def test(host, pguser): 13 | pg_conn_info = "dbname=epanet_test_db host=" + host + " user=" + pguser 14 | tmp_dir = tempfile.gettempdir() 15 | test_data_dir = os.path.dirname(os.path.realpath(__file__)) 16 | 17 | sqlite_test_filename1 = os.path.join(tmp_dir, "versioning_base_test1.sqlite") 18 | sqlite_test_filename2 = os.path.join(tmp_dir, "versioning_base_test2.sqlite") 19 | sqlite_test_filename3 = os.path.join(tmp_dir, "versioning_base_test3.sqlite") 20 | sqlite_test_filename4 = os.path.join(tmp_dir, "versioning_base_test4.sqlite") 21 | sqlite_test_filename5 = os.path.join(tmp_dir, "versioning_base_test5.sqlite") 22 | if os.path.isfile(sqlite_test_filename1): os.remove(sqlite_test_filename1) 23 | if os.path.isfile(sqlite_test_filename2): os.remove(sqlite_test_filename2) 24 | if os.path.isfile(sqlite_test_filename3): os.remove(sqlite_test_filename3) 25 | if os.path.isfile(sqlite_test_filename4): os.remove(sqlite_test_filename4) 26 | if os.path.isfile(sqlite_test_filename5): os.remove(sqlite_test_filename5) 27 | 28 | # create the test database 29 | 30 | os.system("dropdb --if-exists -h " + host + " -U "+pguser+" epanet_test_db") 31 | os.system("createdb -h " + host + " -U "+pguser+" epanet_test_db") 32 | os.system("psql -h " + host + " -U "+pguser+" epanet_test_db -f "+test_data_dir+"/epanet_test_db.sql") 33 | 34 | versioning.historize("dbname=epanet_test_db host={} user={}".format(host,pguser), "epanet") 35 | 36 | spversioning1 = versioning.spatialite(sqlite_test_filename1, pg_conn_info) 37 | spversioning2 = versioning.spatialite(sqlite_test_filename2, pg_conn_info) 38 | spversioning3 = versioning.spatialite(sqlite_test_filename3, pg_conn_info) 39 | spversioning4 = versioning.spatialite(sqlite_test_filename4, pg_conn_info) 40 | spversioning5 = versioning.spatialite(sqlite_test_filename5, pg_conn_info) 41 | # chechout two tables 42 | 43 | try: 44 | spversioning1.checkout(["epanet_trunk_rev_head.junctions","epanet.pipes"]) 45 | assert(False and "checkout from schema withouti suffix _branch_rev_head should not be successfull") 46 | except RuntimeError: 47 | pass 48 | 49 | assert( not os.path.isfile(sqlite_test_filename1) and "sqlite file must not exist at this point" ) 50 | spversioning1.checkout(["epanet_trunk_rev_head.junctions","epanet_trunk_rev_head.pipes"]) 51 | assert( os.path.isfile(sqlite_test_filename1) and "sqlite file must exist at this point" ) 52 | 53 | try: 54 | spversioning1.checkout(["epanet_trunk_rev_head.junctions","epanet_trunk_rev_head.pipes"]) 55 | assert(False and "trying to checkout on an existing file must fail") 56 | except RuntimeError: 57 | pass 58 | 59 | # edit one table and commit changes; rev = 2 60 | 61 | scon = dbapi2.connect(sqlite_test_filename1) 62 | scon.enable_load_extension(True) 63 | scon.execute("SELECT load_extension('mod_spatialite')") 64 | scur = scon.cursor() 65 | scur.execute("UPDATE junctions_view SET elevation = '8' WHERE id = '2'") 66 | scon.commit() 67 | scur.execute("SELECT COUNT(*) FROM junctions") 68 | assert( scur.fetchone()[0] == 3 ) 69 | scon.close() 70 | spversioning1.commit('first edit commit') 71 | pcon = psycopg2.connect(pg_conn_info) 72 | pcur = pcon.cursor() 73 | pcur.execute("SELECT COUNT(*) FROM epanet.junctions") 74 | assert( pcur.fetchone()[0] == 3 ) 75 | pcur.execute("SELECT COUNT(*) FROM epanet.revisions") 76 | assert( pcur.fetchone()[0] == 2 ) 77 | 78 | # add revision : edit one table and commit changes; rev = 3 79 | 80 | spversioning2.checkout(["epanet_trunk_rev_head.junctions", "epanet_trunk_rev_head.pipes"]) 81 | 82 | scon = dbapi2.connect(sqlite_test_filename2) 83 | scon.enable_load_extension(True) 84 | scon.execute("SELECT load_extension('mod_spatialite')") 85 | scur = scon.cursor() 86 | scur.execute("UPDATE junctions_view SET elevation = '22' WHERE id = '2'") 87 | scon.commit() 88 | #scur.execute("SELECT COUNT(*) FROM junctions") 89 | #assert( scur.fetchone()[0] == 3 ) 90 | scon.close() 91 | spversioning2.commit('second edit commit') 92 | 93 | # add revision : insert one junction and commit changes; rev = 4 94 | 95 | spversioning3.checkout(["epanet_trunk_rev_head.junctions"]) 96 | 97 | scon = dbapi2.connect(sqlite_test_filename3) 98 | scon.enable_load_extension(True) 99 | scon.execute("SELECT load_extension('mod_spatialite')") 100 | scur = scon.cursor() 101 | scur.execute("INSERT INTO junctions_view(id, elevation, geom) VALUES ('10','100',GeomFromText('POINT(2 0)',2154))") 102 | scon.commit() 103 | #scur.execute("SELECT COUNT(*) FROM junctions") 104 | #assert( scur.fetchone()[0] == 3 ) 105 | scon.close() 106 | spversioning3.commit('insert commit') 107 | 108 | # add revision : delete one junction and commit changes; rev = 5 109 | 110 | spversioning4.checkout(["epanet_trunk_rev_head.junctions", "epanet_trunk_rev_head.pipes"]) 111 | 112 | scon = dbapi2.connect(sqlite_test_filename4) 113 | scur = scon.cursor() 114 | 115 | # remove pipes so wen can delete referenced junctions 116 | scur.execute("DELETE FROM pipes_view") 117 | scon.commit() 118 | scur.execute("SELECT COUNT(*) FROM pipes_view") 119 | assert(scur.fetchone()[0]==0) 120 | 121 | scur.execute("DELETE FROM junctions_view WHERE id = 1") 122 | scon.commit() 123 | #scur.execute("SELECT COUNT(*) FROM junctions") 124 | #assert( scur.fetchone()[0] == 3 ) 125 | scon.close() 126 | spversioning4.commit('delete id=1 commit') 127 | 128 | select_str = diff_rev_view_str(pg_conn_info, 'epanet', 'junctions','trunk', 1,2) 129 | pcur.execute(select_str) 130 | res = pcur.fetchall() 131 | assert(res[0][0] == 'u') 132 | #print("fetchall 1 vs 2 = " + str(res)) 133 | #fetchall 1 vs 2 = [ 134 | #('u', 3, '1', 8.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 2, 2, 2, 4)] 135 | 136 | select_str = diff_rev_view_str(pg_conn_info, 'epanet', 'junctions','trunk', 1,3) 137 | pcur.execute(select_str) 138 | 139 | res = pcur.fetchall() 140 | assert(res[0][0] == 'i') 141 | assert(res[1][0] == 'u') 142 | #print("fetchall 1 vs 3 = " + str(res)) 143 | #fetchall 1 vs 3 = [ 144 | #('u', 4, '1', 22.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 3, None, 3, None), 145 | #('i', 3, '1', 8.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 2, 2, 2, 4)] 146 | 147 | select_str = diff_rev_view_str(pg_conn_info, 'epanet', 'junctions','trunk', 1,4) 148 | pcur.execute(select_str) 149 | res = pcur.fetchall() 150 | assert(res[0][0] == 'i') 151 | assert(res[1][0] == 'i') 152 | assert(res[2][0] == 'u') 153 | assert(res[3][0] == 'a') # object is in intermediate state; will be deleted in rev 5 154 | #print("fetchall 1 vs 4 = " + str(res)) 155 | #fetchall 1 vs 4 = [ 156 | #('u', 4, '1', 22.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 3, None, 3, None), 157 | #('i', 3, '1', 8.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 2, 2, 2, 4), 158 | #('a', 5, '10', 100.0, None, None, '01010000206A08000000000000000000400000000000000000', 4, None, None, None), 159 | #('i', 1, '0', 0.0, None, None, '01010000206A080000000000000000F03F0000000000000000', 1, 4, None, None)] 160 | 161 | select_str = diff_rev_view_str(pg_conn_info, 'epanet', 'junctions','trunk', 1,5) 162 | pcur.execute(select_str) 163 | res = pcur.fetchall() 164 | assert(res[0][0] == 'd') 165 | assert(res[1][0] == 'i') 166 | assert(res[2][0] == 'u') 167 | assert(res[3][0] == 'a') 168 | #print("fetchall 1 vs 5 = " + str(res)) 169 | #fetchall 1 vs 5 = [ 170 | #('u', 4, '1', 22.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 3, None, 3, None), 171 | #('i', 3, '1', 8.0, None, None, '01010000206A0800000000000000000000000000000000F03F', 2, 2, 2, 4), 172 | #('a', 5, '10', 100.0, None, None, '01010000206A08000000000000000000400000000000000000', 4, None, None, None), 173 | #('d', 1, '0', 0.0, None, None, '01010000206A080000000000000000F03F0000000000000000', 1, 4, None, None)] 174 | 175 | # add revision : edit one table then delete and commit changes; rev = 6 176 | 177 | spversioning5.checkout(["epanet_trunk_rev_head.junctions", "epanet_trunk_rev_head.pipes"]) 178 | 179 | scon = dbapi2.connect(sqlite_test_filename5) 180 | scur = scon.cursor() 181 | scon.enable_load_extension(True) 182 | scon.execute("SELECT load_extension('mod_spatialite')") 183 | scur.execute("UPDATE junctions_view SET elevation = '22' WHERE id = '1'") 184 | scur.execute("DELETE FROM junctions_view WHERE id = '1'") 185 | scon.commit() 186 | #scur.execute("SELECT COUNT(*) FROM junctions") 187 | #assert( scur.fetchone()[0] == 3 ) 188 | scon.close() 189 | spversioning5.commit('update and delete commit') 190 | 191 | 192 | if __name__ == "__main__": 193 | if len(sys.argv) != 3: 194 | print("Usage: python3 versioning_base_test.py host pguser") 195 | else: 196 | test(*sys.argv[1:]) 197 | -------------------------------------------------------------------------------- /versioningDB/__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | -------------------------------------------------------------------------------- /versioningDB/constraints.py: -------------------------------------------------------------------------------- 1 | 2 | """ 3 | /*************************************************************************** 4 | versioning 5 | A QGIS plugin 6 | postgis database versioning 7 | ------------------- 8 | begin : 2018-06-14 9 | copyright : (C) 2018 by Oslandia 10 | email : infos@oslandia.com 11 | ***************************************************************************/ 12 | 13 | /*************************************************************************** 14 | * * 15 | * This program is free software; you can redistribute it and/or modify * 16 | * it under the terms of the GNU General Public License as published by * 17 | * the Free Software Foundation; either version 2 of the License, or * 18 | * (at your option) any later version. * 19 | * * 20 | ***************************************************************************/ 21 | """ 22 | 23 | from .utils import get_pkeys 24 | 25 | 26 | class Constraint: 27 | 28 | def __init__(self, table_from, columns_from, defaults_from, table_to, 29 | columns_to, updtype, deltype): 30 | """ Construct a unique or foreign key constraint 31 | 32 | :param table_from: referencing table 33 | :param columns_from: referencing columns 34 | :param defaults_from: default values 35 | :param table_to: referenced table (None if constraint is unique key) 36 | :param columns_to: referenced columns (empty if constraint is 37 | unique key) 38 | :param updtype: update type (cascade, set default, set null, restrict) 39 | :param deltype: delete type (cascade, set default, set null, restrict) 40 | 41 | """ 42 | assert(not columns_to or len(columns_from) == len(columns_to)) 43 | 44 | self.table_from = table_from 45 | self.columns_from = columns_from 46 | self.defaults_from = defaults_from 47 | self.table_to = table_to 48 | self.columns_to = columns_to 49 | self.updtype = updtype 50 | self.deltype = deltype 51 | 52 | def get_q_table_from(self, schema): 53 | """ Build and return fully qualified table from 54 | 55 | :param schema: table schema 56 | :returns: fully qualified table name 57 | :rtype: str 58 | 59 | """ 60 | return ((schema + "." + self.table_from) if schema 61 | else self.table_from) 62 | 63 | def get_q_table_to(self, schema): 64 | """ Build and return fully qualified table to 65 | 66 | :param schema: table schema 67 | :returns: fully qualified table name 68 | :rtype: str 69 | 70 | """ 71 | 72 | return ((schema + "." + self.table_to) if schema 73 | else self.table_to) 74 | 75 | 76 | class ConstraintBuilder: 77 | 78 | def __init__(self, b_cur, wc_cur, b_schema, wc_schema): 79 | """ Constructor to build unique and foreign key constraint 80 | 81 | :param b_cur: base cursor (must be opened and valid) 82 | :param wc_cur: working copy cursor (must be opened and valid) 83 | :param b_schema: base schema 84 | :param wc_schema: working copy schema 85 | 86 | """ 87 | self.b_cur = b_cur 88 | self.wc_cur = wc_cur 89 | self.b_schema = b_schema 90 | self.wc_schema = wc_schema 91 | 92 | b_cur.execute(f""" 93 | SELECT table_from, columns_from, defaults_from, table_to, 94 | columns_to, updtype, deltype 95 | FROM {b_schema}.versioning_constraints 96 | """) 97 | 98 | # build two dict to speed access to constraint from 99 | # referencing and referenced table 100 | self.referencing_constraints = {} 101 | self.referenced_constraints = {} 102 | 103 | # Build trigger upon this contraints and setup on view 104 | for (table_from, columns_from, defaults_from, table_to, columns_to, 105 | updtype, deltype) in b_cur.fetchall(): 106 | 107 | constraint = Constraint(table_from, columns_from, defaults_from, 108 | table_to, columns_to, updtype, deltype) 109 | 110 | self.referencing_constraints.setdefault(table_from, []).append( 111 | constraint) 112 | 113 | if table_to: 114 | self.referenced_constraints.setdefault(table_to, []).append( 115 | constraint) 116 | 117 | def get_referencing_constraint(self, method, table): 118 | """ Build and return unique and foreign key referencing constraints 119 | sql for given table 120 | 121 | :param method: insert, update or delete 122 | :param table: the referencing table for which we need to build 123 | constraints 124 | 125 | """ 126 | 127 | sql_constraint = "" 128 | if (table not in self.referencing_constraints 129 | or method not in ['insert', 'update']): 130 | return sql_constraint 131 | 132 | for constraint in self.referencing_constraints.get(table, []): 133 | 134 | # unique constraint 135 | if not constraint.table_to: 136 | 137 | q_table_from = constraint.get_q_table_from(self.wc_schema) 138 | 139 | # check if unique keys already exist 140 | when_filter = "(SELECT COUNT(*) FROM {}_view WHERE {}) != 0".format( 141 | q_table_from, 142 | " AND ".join(["{0} = NEW.{0}".format(column) for column in constraint.columns_from])) 143 | 144 | # check if unique keys have been modified 145 | if method == 'update': 146 | when_filter += " AND " + " AND ".join(["NEW.{0} != OLD.{0}".format(column) 147 | for column in constraint.columns_from]) 148 | 149 | keys = ",".join(constraint.columns_from) 150 | 151 | # postgres requests 152 | if self.wc_cur.isPostgres(): 153 | 154 | sql_constraint += f"""IF {when_filter} THEN 155 | RAISE EXCEPTION 'Fail {q_table_from} {keys} unique constraint'; 156 | END IF; 157 | """ 158 | 159 | # spatialite requests 160 | else: 161 | 162 | sql_constraint += f'SELECT RAISE(FAIL, "Fail {q_table_from} {keys} unique constraint") WHERE {when_filter};' 163 | 164 | # foreign key constraint 165 | else: 166 | 167 | q_table_to = constraint.get_q_table_to(self.wc_schema) 168 | 169 | # check if referenced keys exists 170 | when_filter = "(SELECT COUNT(*) FROM {}_view WHERE {}) = 0".format( 171 | q_table_to, " AND ".join( 172 | [f"(NEW.{column_from} IS NULL " 173 | f"OR {column_to} = NEW.{column_from})" 174 | for column_to, column_from 175 | in zip(constraint.columns_to, constraint.columns_from)])) 176 | 177 | keys = ",".join(constraint.columns_from) 178 | 179 | # postgres requests 180 | if self.wc_cur.db_type == 'pg : ': 181 | 182 | sql_constraint += f"""IF {when_filter} THEN 183 | RAISE EXCEPTION 'Fail {keys} foreign key constraint'; 184 | END IF;""" 185 | 186 | # spatialite requests 187 | else: 188 | 189 | sql_constraint += f'SELECT RAISE(FAIL, "Fail {keys} foreign key constraint") WHERE {when_filter};' 190 | 191 | return sql_constraint 192 | 193 | def get_referenced_constraint(self, method, table): 194 | """ Build and return foreign key referenced constraints sql for given table 195 | 196 | :param method: insert, update or delete 197 | :param table: the referenced table for which we need to build 198 | constraints 199 | 200 | """ 201 | 202 | sql_constraint = "" 203 | if table not in self.referenced_constraints or method not in ['delete', 'update']: 204 | return sql_constraint 205 | 206 | for constraint in self.referenced_constraints.get(table, []): 207 | 208 | # check if referenced keys have been modified 209 | where = None 210 | if method == 'update': 211 | where = "({})".format( 212 | " OR ".join(["NEW.{0} != OLD.{0}".format(column) 213 | for column in constraint.columns_to])) 214 | else: 215 | where = "True" 216 | 217 | action_type = (constraint.updtype if method == 'update' 218 | else constraint.deltype) 219 | 220 | q_table_from = constraint.get_q_table_from(self.wc_schema) 221 | 222 | col_where = " AND ".join([f"{column_from} = OLD.{column_to}" 223 | for column_from, column_to in 224 | zip(constraint.columns_from, 225 | constraint.columns_to)]) 226 | 227 | # cascade 228 | if action_type == 'c': 229 | where += " AND " + col_where 230 | 231 | if method == 'update': 232 | updated_fields = ",".join( 233 | [f"{column_from} = NEW.{column_to}" 234 | for column_from, column_to 235 | in zip(constraint.columns_from, 236 | constraint.columns_to)]) 237 | 238 | sql_constraint += f""" 239 | UPDATE {q_table_from}_view 240 | SET {updated_fields} WHERE {where};""" 241 | else: 242 | sql_constraint += f""" 243 | DELETE FROM {q_table_from}_view WHERE {where};""" 244 | 245 | # set null or set default 246 | elif action_type == 'n' or action_type == 'd': 247 | where += " AND " + col_where 248 | 249 | updated_fields = ",".join( 250 | ["{} = {}".format( 251 | column_from, 252 | "NULL" if (action_type == 'n' 253 | or default_from is None) 254 | else default_from) 255 | for column_from, column_to, default_from 256 | in zip(constraint.columns_from, 257 | constraint.columns_to, 258 | constraint.defaults_from)]) 259 | 260 | sql_constraint += f"""UPDATE {q_table_from}_view 261 | SET {updated_fields} WHERE {col_where};""" 262 | 263 | # fail 264 | else: 265 | 266 | where += f""" AND (SELECT COUNT(*) FROM {q_table_from}_view 267 | WHERE {col_where}) > 0""" 268 | 269 | keys_label = ",".join(constraint.columns_to) + (" is" if len(constraint.columns_to) == 1 else " are") 270 | sql_constraint += (f"""IF {where} THEN RAISE EXCEPTION '{keys_label} still referenced by {q_table_from}'; END IF;""" 271 | if self.wc_cur.db_type == 'pg : ' 272 | else f"""SELECT RAISE(FAIL, "{keys_label} still referenced by {q_table_from}") WHERE {where};""") 273 | 274 | return sql_constraint 275 | 276 | 277 | def check_unique_constraints(b_cur, wc_cur, wc_schema): 278 | 279 | wc_cur.execute("SELECT rev, branch, table_schema, table_name " 280 | f"FROM {wc_schema}.initial_revision") 281 | versioned_layers = wc_cur.fetchall() 282 | 283 | errors = [] 284 | for [rev, branch, b_schema, table] in versioned_layers: 285 | 286 | # Spatialite 287 | if wc_cur.isSpatialite(): 288 | table_w_revs = f"{table}" 289 | vid = "ogc_fid" 290 | 291 | # PgServer 292 | elif b_cur is wc_cur: 293 | table_w_revs = f"{wc_schema}.{table}_diff" 294 | vid = "versioning_id" 295 | 296 | # PgLocal 297 | else: 298 | table_w_revs = f"{wc_schema}.{table}" 299 | vid = "ogc_fid" 300 | 301 | pkeys = get_pkeys(b_cur, b_schema, table) 302 | pkey_list = ",".join(["trev." + pkey for pkey in pkeys]) 303 | new_pkeys_filter = " OR ".join(["trev.{0} != trev2.{0}".format(pkey) 304 | for pkey in pkeys]) 305 | 306 | wc_cur.execute(f""" 307 | -- INSERTED PKEY 308 | SELECT {pkey_list} 309 | FROM {table_w_revs} trev 310 | WHERE {branch}_rev_end is NULL 311 | AND {branch}_parent is NULL 312 | AND {branch}_rev_begin > {rev} 313 | UNION 314 | -- UPDATED PKEY 315 | SELECT {pkey_list} 316 | FROM {table_w_revs} trev, {table_w_revs} trev2 317 | WHERE trev.{branch}_parent IS NOT NULL 318 | AND trev.{branch}_rev_begin > 1 319 | AND trev.{branch}_parent = trev2.{vid} 320 | AND ({new_pkeys_filter}) 321 | """) 322 | 323 | new_keys = wc_cur.fetchall() 324 | 325 | if not new_keys: 326 | continue 327 | 328 | # search in database if there are some working copy new key that 329 | # already exist. 330 | new_key_list = ",".join(["({})".format( 331 | ",".join([str(int_key)for int_key in new_key])) 332 | for new_key in new_keys]) 333 | 334 | b_cur.execute(f""" 335 | SELECT {pkey_list} 336 | FROM {b_schema}.{table} trev 337 | WHERE {branch}_rev_end is NULL 338 | AND {branch}_parent is NULL 339 | INTERSECT 340 | SELECT * 341 | FROM (VALUES {new_key_list}) AS new_keys""") 342 | 343 | def to_string(rec): 344 | return " and ".join( 345 | [f"{pkey}={value}" for pkey, value in zip(pkeys, rec)]) 346 | 347 | errors += [" {}.{} : {}".format(wc_schema, table, to_string(res)) 348 | for res in b_cur.fetchall()] 349 | 350 | if errors: 351 | raise RuntimeError("Some new or updated row violate the primary key" 352 | " constraint in base database :\n{}".format( 353 | "\n".join(errors))) 354 | -------------------------------------------------------------------------------- /versioningDB/sql/historize.sql: -------------------------------------------------------------------------------- 1 | -- Create table to store PRIMARY KEY and FOREIGN KEY constraints 2 | -- before deleting them 3 | CREATE TABLE {schema}.versioning_constraints ( 4 | table_from varchar, 5 | columns_from varchar[], 6 | defaults_from varchar[], 7 | table_to varchar, 8 | columns_to varchar[], 9 | updtype char, 10 | deltype char); 11 | 12 | -- populate constraints table 13 | INSERT INTO {schema}.versioning_constraints 14 | SELECT (SELECT relname FROM pg_class WHERE oid = conrelid::regclass) AS table_from, 15 | 16 | (SELECT array_agg(att.attname) 17 | FROM (SELECT unnest(conkey) AS key) AS keys, 18 | pg_attribute att WHERE att.attrelid = conrelid AND att.attnum = keys.key) AS columns_from, 19 | 20 | (SELECT array_agg(adef.adsrc) 21 | FROM (SELECT unnest(conkey) AS key) AS keys 22 | LEFT JOIN pg_attrdef adef ON adef.adrelid = conrelid 23 | AND adef.adnum = keys.key ) AS defaults_from, 24 | 25 | (SELECT relname FROM pg_class WHERE oid = confrelid::regclass) as table_to, 26 | 27 | (SELECT array_agg(att.attname) 28 | FROM (SELECT unnest(confkey) AS key) AS keys, 29 | pg_attribute att WHERE att.attrelid = confrelid AND att.attnum = keys.key) AS columns_to, 30 | 31 | c.confupdtype as updtype, 32 | c.confdeltype as deltype 33 | FROM pg_constraint c 34 | JOIN pg_namespace n ON n.oid = c.connamespace 35 | WHERE contype IN ('f', 'p ') 36 | AND n.nspname = '{schema}'; 37 | 38 | -- Drop foreign keys and primary keys 39 | DO 40 | $do$ 41 | DECLARE 42 | c record; 43 | BEGIN 44 | FOR c IN 45 | SELECT pgc.conname as name, pgc.conrelid::regclass as table 46 | FROM pg_constraint pgc, pg_namespace pgn 47 | WHERE pgn.oid = pgc.connamespace 48 | AND pgc.contype IN ('f', 'p ') 49 | AND pgn.nspname = '{schema}' 50 | ORDER BY pgc.contype ASC -- foreign keys first 51 | LOOP 52 | EXECUTE 'ALTER TABLE ' || c.table || ' DROP CONSTRAINT ' || c.name; 53 | END LOOP; 54 | END 55 | $do$; 56 | 57 | -- Add the versioning_id primary key 58 | DO 59 | $$ 60 | DECLARE 61 | rec record; 62 | BEGIN 63 | FOR rec IN 64 | SELECT schemaname, tablename 65 | FROM pg_catalog.pg_tables 66 | WHERE schemaname = '{schema}' 67 | AND tablename != 'versioning_constraints' 68 | LOOP 69 | EXECUTE 'ALTER TABLE ' || rec.schemaname || '.' || rec.tablename 70 | || ' ADD COLUMN versioning_id SERIAL PRIMARY KEY'; 71 | END LOOP; 72 | END 73 | $$; 74 | 75 | -- Create revisions table 76 | CREATE TABLE {schema}.revisions ( 77 | rev serial PRIMARY KEY, 78 | commit_msg varchar, 79 | branch varchar DEFAULT 'trunk', 80 | date timestamp DEFAULT current_timestamp, 81 | author varchar); 82 | 83 | -------------------------------------------------------------------------------- /versioningDB/versioningAbc.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | 3 | from __future__ import absolute_import 4 | 5 | from .postgresqlServer import pgVersioningServer 6 | from .spatialite import spVersioning 7 | from .postgresqlLocal import pgVersioningLocal 8 | 9 | TYPE = ('postgres', 'spatialite', 'pgDistant') 10 | CONNECTIONS = {'postgres': 2, 'spatialite': 2, 'pgDistant': 3} 11 | 12 | class versioningAbc(object): 13 | 14 | def __init__(self, connection, typebase): 15 | assert(isinstance(connection, list) and 16 | typebase in TYPE and 17 | len(connection) == CONNECTIONS[typebase]) 18 | 19 | self.typebase = typebase 20 | # spatialite : [sqlite_filename, pg_conn_info] 21 | # postgres : [pg_conn_info, working_copy_schema] 22 | # pgDistant : [pg_conn_info, working_copy_schema, pg_conn_info_out] 23 | self.connection = connection 24 | if self.typebase == 'spatialite': 25 | self.ver = spVersioning() 26 | elif self.typebase == 'pgDistant': 27 | self.ver = pgVersioningLocal() 28 | else: 29 | self.ver = pgVersioningServer() 30 | 31 | def revision(self): 32 | return self.ver.revision(self.connection) 33 | 34 | def late(self ): 35 | return self.ver.late(self.connection) 36 | 37 | def update(self): 38 | self.ver.update(self.connection) 39 | 40 | def checkout(self, pg_table_names, selected_feature_lists = []): 41 | self.ver.checkout(self.connection, pg_table_names, selected_feature_lists) 42 | 43 | def unresolved_conflicts(self): 44 | return self.ver.unresolved_conflicts(self.connection) 45 | 46 | def commit(self, commit_msg, commit_user = ''): 47 | return self.ver.commit(self.connection, commit_msg, commit_user) 48 | --------------------------------------------------------------------------------