diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..bd63c42b --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +** +!requirements*.txt diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 186d79c6..f88502b6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -54,11 +54,11 @@ jobs: uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} - + - name: Install psql, sqlite3 and spatialite run: | sudo apt-get install --yes --no-install-recommends postgresql-client sqlite3 libsqlite3-mod-spatialite - + - name: Load test postgis database shell: bash run: | diff --git a/CHANGES.rst b/CHANGES.rst index 549de70e..2869b588 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -5,7 +5,8 @@ Changelog of threedi-modelchecker 0.26.2 (unreleased) ------------------- -- Nothing changed yet. +- Added ModelSchema().upgrade_spatialite_version (and the same argument to .upgrade) to + upgrade the spatialite version from 3 to 4/5. 0.26.1 (2022-04-11) diff --git a/Docker/Dockerfile b/Docker/Dockerfile index 6340508f..7e7ae39a 100644 --- a/Docker/Dockerfile +++ b/Docker/Dockerfile @@ -1,4 +1,5 @@ -FROM python:3.7 +FROM python:3.9-slim-bullseye +# The bullseye image contains libspatialite 5 RUN apt-get update \ && apt-get install -y \ diff --git a/docker-compose.yml b/docker-compose.yml index ccef676a..de7a66a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,8 +10,6 @@ services: volumes: - .:/code working_dir: /code - depends_on: - - postgis command: bash postgis: diff --git a/requirements-dev.txt b/requirements-dev.txt index 7ade05a7..372aa0d4 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,9 @@ -coverage factory_boy -flake8 pytest -pytest-cov -pytest-flake8 mock -mypy -sqlalchemy-stubs \ No newline at end of file +pytest-cov +threedi-api-client +aiofiles +aiohttp +pytest-asyncio +numpy diff --git a/requirements.txt b/requirements.txt index 88e35e22..7299f4bf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ -GeoAlchemy2==0.6.1 -SQLAlchemy==1.3.1 +Click +GeoAlchemy2>=0.9,!=0.11.* +SQLAlchemy>=1.2 Click==7.0 -psycopg2 -alembic \ No newline at end of file +alembic>=0.9 diff --git a/tests/conftest.py b/tests/conftest.py index e0f3347f..87d25f35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -89,6 +89,22 @@ def in_memory_sqlite(): return ThreediDatabase({"db_path": ""}, db_type="spatialite", echo=False) +@pytest.fixture +def empty_sqlite_v3(tmp_path): + """An empty spatialite v3 in the latest migration state""" + tmp_sqlite = tmp_path / "empty_v3.sqlite" + shutil.copyfile(os.path.join(data_dir, "empty_v3.sqlite"), tmp_sqlite) + return ThreediDatabase({"db_path": tmp_sqlite}, db_type="spatialite", echo=False) + + +@pytest.fixture +def empty_sqlite_v3_clone(tmp_path): + """An empty spatialite v3 in the latest migration state""" + tmp_sqlite = tmp_path / "empty_v3_clone.sqlite" + shutil.copyfile(os.path.join(data_dir, "empty_v3.sqlite"), tmp_sqlite) + return ThreediDatabase({"db_path": tmp_sqlite}, db_type="spatialite", echo=False) + + @pytest.fixture def south_latest_sqlite(tmp_path): """An empty SQLite that is in its latest South migration state""" diff --git a/tests/data/empty_v3.sqlite b/tests/data/empty_v3.sqlite new file mode 100644 index 00000000..1cba853d Binary files /dev/null and b/tests/data/empty_v3.sqlite differ diff --git a/tests/test_schema.py b/tests/test_schema.py index 42e763bf..75cf94ae 100644 --- a/tests/test_schema.py +++ b/tests/test_schema.py @@ -8,6 +8,7 @@ from threedi_modelchecker.schema import get_schema_version from threedi_modelchecker.schema import ModelSchema from threedi_modelchecker.threedi_model.views import ALL_VIEWS +from threedi_modelchecker.spatialite_versions import get_spatialite_version from unittest import mock import pytest @@ -86,9 +87,7 @@ def test_get_version_alembic(in_memory_sqlite, alembic_version_table): def test_validate_schema(threedi_db): """Validate a correct schema version""" schema = ModelSchema(threedi_db) - with mock.patch.object( - schema, "get_version", return_value=get_schema_version() - ): + with mock.patch.object(schema, "get_version", return_value=get_schema_version()): assert schema.validate_schema() @@ -113,7 +112,7 @@ def test_validate_schema_too_high_migration(threedi_db, version): def test_full_upgrade_empty(in_memory_sqlite): """Upgrade an empty database to the latest version""" schema = ModelSchema(in_memory_sqlite) - schema.upgrade(backup=False, set_views=False) + schema.upgrade(backup=False, set_views=False, upgrade_spatialite_version=False) assert schema.get_version() == get_schema_version() assert in_memory_sqlite.get_engine().has_table("v2_connection_nodes") @@ -121,7 +120,7 @@ def test_full_upgrade_empty(in_memory_sqlite): def test_full_upgrade_with_preexisting_version(south_latest_sqlite): """Upgrade an empty database to the latest version""" schema = ModelSchema(south_latest_sqlite) - schema.upgrade(backup=False, set_views=False) + schema.upgrade(backup=False, set_views=False, upgrade_spatialite_version=False) assert schema.get_version() == get_schema_version() assert south_latest_sqlite.get_engine().has_table("v2_connection_nodes") @@ -129,7 +128,7 @@ def test_full_upgrade_with_preexisting_version(south_latest_sqlite): def test_full_upgrade_oldest(oldest_sqlite): """Upgrade a legacy database to the latest version""" schema = ModelSchema(oldest_sqlite) - schema.upgrade(backup=False, set_views=False) + schema.upgrade(backup=False, set_views=False, upgrade_spatialite_version=False) assert schema.get_version() == get_schema_version() assert oldest_sqlite.get_engine().has_table("v2_connection_nodes") @@ -141,7 +140,9 @@ def test_upgrade_south_not_latest_errors(in_memory_sqlite): schema, "get_version", return_value=constants.LATEST_SOUTH_MIGRATION_ID - 1 ): with pytest.raises(errors.MigrationMissingError): - schema.upgrade(backup=False, set_views=False) + schema.upgrade( + backup=False, set_views=False, upgrade_spatialite_version=False + ) def test_upgrade_with_backup(threedi_db): @@ -153,7 +154,9 @@ def test_upgrade_with_backup(threedi_db): "threedi_modelchecker.schema._upgrade_database", side_effect=RuntimeError ) as upgrade, mock.patch.object(schema, "get_version", return_value=199): with pytest.raises(RuntimeError): - schema.upgrade(backup=True, set_views=False) + schema.upgrade( + backup=True, set_views=False, upgrade_spatialite_version=False + ) (db,), kwargs = upgrade.call_args assert db is not threedi_db @@ -166,20 +169,33 @@ def test_upgrade_without_backup(threedi_db): "threedi_modelchecker.schema._upgrade_database", side_effect=RuntimeError ) as upgrade, mock.patch.object(schema, "get_version", return_value=199): with pytest.raises(RuntimeError): - schema.upgrade(backup=False, set_views=False) + schema.upgrade( + backup=False, set_views=False, upgrade_spatialite_version=False + ) (db,), kwargs = upgrade.call_args assert db is threedi_db def test_set_views(oldest_sqlite): - """Make sure that the views are regenerated - """ + """Make sure that the views are regenerated""" schema = ModelSchema(oldest_sqlite) - schema.upgrade(backup=False, set_views=True) + schema.upgrade(backup=False, set_views=True, upgrade_spatialite_version=False) assert schema.get_version() == get_schema_version() # Test all views with oldest_sqlite.session_scope() as session: for view_name in ALL_VIEWS: session.execute(f"SELECT * FROM {view_name} LIMIT 1").fetchall() + + +def test_upgrade_spatialite_3(oldest_sqlite): + lib_version, file_version_before = get_spatialite_version(oldest_sqlite) + if lib_version == file_version_before: + pytest.skip("Nothing to test: spatialite library version equals file version") + + schema = ModelSchema(oldest_sqlite) + schema.upgrade(backup=False, upgrade_spatialite_version=True) + + _, file_version_after = get_spatialite_version(oldest_sqlite) + assert file_version_after == 4 diff --git a/tests/test_spatalite_versions.py b/tests/test_spatalite_versions.py new file mode 100644 index 00000000..6d4da3d3 --- /dev/null +++ b/tests/test_spatalite_versions.py @@ -0,0 +1,34 @@ +from geoalchemy2 import func as geo_func +from threedi_modelchecker.spatialite_versions import get_spatialite_version, copy_model +from threedi_modelchecker.threedi_model import models + + +def test_get_spatialite_version(empty_sqlite_v3): + lib_version, file_version = get_spatialite_version(empty_sqlite_v3) + assert lib_version in (3, 4, 5) + assert file_version == 3 + + +def test_copy_model(empty_sqlite_v3, empty_sqlite_v3_clone): + obj = models.ConnectionNode( + id=3, code="test", the_geom="SRID=4326;POINT(-71.064544 42.287870)" + ) + with empty_sqlite_v3.session_scope() as session: + session.add(obj) + session.commit() + + copy_model(empty_sqlite_v3, empty_sqlite_v3_clone, models.ConnectionNode) + + assert models.ConnectionNode.__table__.columns + + with empty_sqlite_v3_clone.session_scope() as session: + records = list( + session.query( + models.ConnectionNode.id, + models.ConnectionNode.code, + geo_func.ST_AsText(models.ConnectionNode.the_geom), + models.ConnectionNode.the_geom_linestring, + ) + ) + + assert records == [(3, "test", "POINT(-71.064544 42.28787)", None)] diff --git a/threedi_modelchecker/migrations/env.py b/threedi_modelchecker/migrations/env.py index b2565d7e..9e53d342 100644 --- a/threedi_modelchecker/migrations/env.py +++ b/threedi_modelchecker/migrations/env.py @@ -1,5 +1,6 @@ from alembic import context from sqlalchemy import create_engine +from threedi_modelchecker.threedi_model import constants from threedi_modelchecker.threedi_model.models import Base import os @@ -30,7 +31,11 @@ def run_migrations_online(): connectable = create_engine(get_url()) with connectable.connect() as connection: - context.configure(connection=connection, target_metadata=target_metadata) + context.configure( + connection=connection, + target_metadata=target_metadata, + version_table=constants.VERSION_TABLE_NAME, + ) with context.begin_transaction(): context.run_migrations() diff --git a/threedi_modelchecker/schema.py b/threedi_modelchecker/schema.py index 51113765..da13b248 100644 --- a/threedi_modelchecker/schema.py +++ b/threedi_modelchecker/schema.py @@ -3,6 +3,7 @@ from .threedi_model import models from .threedi_model import views from alembic.config import Config +from alembic import command as alembic_command from alembic.environment import EnvironmentContext from alembic.migration import MigrationContext from alembic.script import ScriptDirectory @@ -10,7 +11,8 @@ from sqlalchemy import Integer from sqlalchemy import MetaData from sqlalchemy import Table - +from sqlalchemy import inspect +from .spatialite_versions import get_spatialite_version, copy_models import warnings @@ -34,23 +36,43 @@ def get_schema_version(): return int(env.get_head_revision()) +def _is_empty_database(db): + """Check if there are tables in the database""" + engine = db.get_engine() + inspector = inspect(engine) + return len(inspector.get_table_names()) == 0 + + +def _create_database(db): + """Create the 3Di model schema in an empty ThreediDatabase instance""" + engine = db.get_engine() + + if engine.dialect.name == "sqlite": + with db.session_scope() as session: + # Speed up by journalling in memory; in case of a crash the database + # will likely go corrupt, however we have an empty database here so + # that is no problem. + session.execute("PRAGMA journal_mode = MEMORY") + session.execute("SELECT InitSpatialMetadata()") + + models.Base.metadata.create_all(engine) + + with engine.begin() as connection: + config = get_alembic_config(connection) + alembic_command.stamp(config, "head") + + def _upgrade_database(db, revision="head"): """Upgrade ThreediDatabase instance""" - with db.get_engine().begin() as connection: - config = get_alembic_config(connection) - script = ScriptDirectory.from_config(config) + engine = db.get_engine() - def upgrade(rev, context): - return script._upgrade_revs(revision, rev) + # Fast track the upgrade to the latest version if database is empty + if revision in {"head", get_schema_version()} and _is_empty_database(db): + return _create_database(db) - with EnvironmentContext( - config, - script, - fn=upgrade, - destination_rev=revision, - version_table=constants.VERSION_TABLE_NAME, - ): - script.run_env() + with engine.begin() as connection: + config = get_alembic_config(connection) + alembic_command.upgrade(config, revision) class ModelSchema: @@ -89,7 +111,13 @@ def get_version(self): else: return self._get_version_old() - def upgrade(self, revision="head", backup=True, set_views=True): + def upgrade( + self, + revision="head", + backup=True, + set_views=True, + upgrade_spatialite_version=False, + ): """Upgrade the database to the latest version. This requires either a completely empty database or a database with its @@ -105,7 +133,14 @@ def upgrade(self, revision="head", backup=True, set_views=True): Specify 'set_views=True' to also (re)create views after the upgrade. This is not compatible when upgrading to a different version than the latest version. + + Specify 'upgrade_spatialite_version=True' to also upgrade the + spatialite file version after the upgrade. """ + if upgrade_spatialite_version and not set_views: + raise ValueError( + "Cannot upgrade the spatialite version without setting the views." + ) v = self.get_version() if v is not None and v < constants.LATEST_SOUTH_MIGRATION_ID: raise MigrationMissingError( @@ -121,7 +156,9 @@ def upgrade(self, revision="head", backup=True, set_views=True): else: _upgrade_database(self.db, revision=revision) - if set_views: + if upgrade_spatialite_version: + self.upgrade_spatialite_version() + elif set_views: self.set_views() def validate_schema(self): @@ -160,6 +197,8 @@ def set_views(self): """(Re)create views in the spatialite according to the latest definitions.""" version = self.get_version() schema_version = get_schema_version() + _, file_version = get_spatialite_version(self.db) + if version != get_schema_version(): raise MigrationMissingError( f"Setting views requires schema version " @@ -173,11 +212,28 @@ def set_views(self): f"DELETE FROM views_geometry_columns WHERE view_name = '{name}'" ) connection.execute(f"CREATE VIEW {name} AS {view['definition']}") - connection.execute( - f"INSERT INTO views_geometry_columns (view_name, view_geometry,view_rowid,f_table_name,f_geometry_column) VALUES('{name}', '{view['view_geometry']}', '{view['view_rowid']}', '{view['f_table_name']}', '{view['f_geometry_column']}')" - ) + if file_version == 3: + connection.execute( + f"INSERT INTO views_geometry_columns (view_name, view_geometry,view_rowid,f_table_name,f_geometry_column) VALUES('{name}', '{view['view_geometry']}', '{view['view_rowid']}', '{view['f_table_name']}', '{view['f_geometry_column']}')" + ) + else: + connection.execute( + f"INSERT INTO views_geometry_columns (view_name, view_geometry,view_rowid,f_table_name,f_geometry_column,read_only) VALUES('{name}', '{view['view_geometry']}', '{view['view_rowid']}', '{view['f_table_name']}', '{view['f_geometry_column']}', 0)" + ) for name in views.VIEWS_TO_DELETE: connection.execute(f"DROP VIEW IF EXISTS {name}") connection.execute( f"DELETE FROM views_geometry_columns WHERE view_name = '{name}'" ) + + def upgrade_spatialite_version(self): + lib_version, file_version = get_spatialite_version(self.db) + if file_version == 3 and lib_version in (4, 5): + self.validate_schema() + + with self.db.file_transaction(start_empty=True) as work_db: + work_schema = ModelSchema(work_db) + work_schema.upgrade( + backup=False, set_views=True, upgrade_spatialite_version=False + ) + copy_models(self.db, work_db, self.declared_models) diff --git a/threedi_modelchecker/spatialite_versions.py b/threedi_modelchecker/spatialite_versions.py new file mode 100644 index 00000000..784226fa --- /dev/null +++ b/threedi_modelchecker/spatialite_versions.py @@ -0,0 +1,54 @@ +from sqlalchemy import inspect +from geoalchemy2 import func as geo_func +from geoalchemy2.types import Geometry +from .threedi_model import models + + +def get_spatialite_version(db): + with db.session_scope() as session: + ((lib_version,),) = session.execute("SELECT spatialite_version()").fetchall() + + # Identify Spatialite version + inspector = inspect(db.get_engine()) + spatial_ref_sys_cols = [x["name"] for x in inspector.get_columns("spatial_ref_sys")] + if len(spatial_ref_sys_cols) == 0: + raise ValueError("Not a spatialite file") + + if "srs_wkt" in spatial_ref_sys_cols: + file_version = 3 + else: + file_version = 4 + + return int(lib_version.split(".")[0]), file_version + + +def cast_if_geometry(column): + """Cast Geometry columns to EWKT (so that we can save it right back later)""" + if isinstance(column.type, Geometry): + return geo_func.AsEWKT(column).label(column.name) + else: + return column + + +def model_query(session, model): + """Query fields explicitly, so that we end up with an iterator of row tuples""" + return session.query(*[cast_if_geometry(c) for c in model.__table__.columns]) + + +def model_from_tuple(model, tpl): + return model(**{c.key: v for (c, v) in zip(model.__table__.columns, tpl)}) + + +def copy_model(from_db, to_db, model): + with from_db.session_scope() as input_session: + objs = [model_from_tuple(model, x) for x in model_query(input_session, model)] + if len(objs) == 0: + return + with to_db.session_scope() as work_session: + work_session.bulk_save_objects(objs) + work_session.commit() + + +def copy_models(from_db, to_db, models=models.DECLARED_MODELS): + for model in models: + copy_model(from_db, to_db, model) diff --git a/threedi_modelchecker/threedi_database.py b/threedi_modelchecker/threedi_database.py index b4ee1552..34553e01 100644 --- a/threedi_modelchecker/threedi_database.py +++ b/threedi_modelchecker/threedi_database.py @@ -141,7 +141,7 @@ def session_scope(self, **kwargs): session.close() @contextmanager - def file_transaction(self, **kwargs): + def file_transaction(self, start_empty=False): """Copy the complete database into a tmpdir and work on that one. On contextmanager exit, the database is copied back and the real @@ -153,7 +153,8 @@ def file_transaction(self, **kwargs): ) work_file = tempfile.NamedTemporaryFile(suffix=".sqlite") # copy the database to the temporary directory - shutil.copy(self.settings["db_path"], work_file.name) + if not start_empty: + shutil.copy(self.settings["db_path"], work_file.name) # yield a new ThreediDatabase refering to the backup try: yield self.__class__({"db_path": work_file.name}, "spatialite") diff --git a/threedi_modelchecker/threedi_model/views.py b/threedi_modelchecker/threedi_model/views.py index 34913de5..c7fc109d 100644 --- a/threedi_modelchecker/threedi_model/views.py +++ b/threedi_modelchecker/threedi_model/views.py @@ -1,79 +1,79 @@ VIEWS_TO_DELETE = ["v2_crosssection_view", "v2_pipe_map_view", "v2_imp_surface_view"] ALL_VIEWS = { "v2_1d_lateral_view": { - "definition": "SELECT a.ROWID AS ROWID, a.id AS id, a.connection_node_id AS connection_node_id, a.timeseries AS timeseries, b.the_geom FROM v2_1d_lateral a JOIN v2_connection_nodes b ON a.connection_node_id = b.id", + "definition": "SELECT a.rowid AS rowid, a.id AS id, a.connection_node_id AS connection_node_id, a.timeseries AS timeseries, b.the_geom FROM v2_1d_lateral a JOIN v2_connection_nodes b ON a.connection_node_id = b.id", "view_geometry": "the_geom", "view_rowid": "connection_node_id", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom", }, "v2_1d_boundary_conditions_view": { - "definition": "SELECT a.ROWID AS ROWID, a.id AS id, a.connection_node_id AS connection_node_id, a.boundary_type AS boundary_type, a.timeseries AS timeseries, b.the_geom FROM v2_1d_boundary_conditions a JOIN v2_connection_nodes b ON a.connection_node_id = b.id", + "definition": "SELECT a.rowid AS rowid, a.id AS id, a.connection_node_id AS connection_node_id, a.boundary_type AS boundary_type, a.timeseries AS timeseries, b.the_geom FROM v2_1d_boundary_conditions a JOIN v2_connection_nodes b ON a.connection_node_id = b.id", "view_geometry": "the_geom", "view_rowid": "connection_node_id", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom", }, "v2_cross_section_location_view": { - "definition": "SELECT loc.ROWID as ROWID, loc.id as loc_id, loc.code as loc_code, loc.reference_level as loc_reference_level, loc.bank_level as loc_bank_level, loc.friction_type as loc_friction_type, loc.friction_value as loc_friction_value, loc.definition_id as loc_definition_id, loc.channel_id as loc_channel_id, loc.the_geom as the_geom, def.id as def_id, def.shape as def_shape, def.width as def_width, def.code as def_code, def.height as def_height FROM v2_cross_section_location loc, v2_cross_section_definition def WHERE loc.definition_id = def.id", + "definition": "SELECT loc.rowid as rowid, loc.id as loc_id, loc.code as loc_code, loc.reference_level as loc_reference_level, loc.bank_level as loc_bank_level, loc.friction_type as loc_friction_type, loc.friction_value as loc_friction_value, loc.definition_id as loc_definition_id, loc.channel_id as loc_channel_id, loc.the_geom as the_geom, def.id as def_id, def.shape as def_shape, def.width as def_width, def.code as def_code, def.height as def_height FROM v2_cross_section_location loc, v2_cross_section_definition def WHERE loc.definition_id = def.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_cross_section_location", "f_geometry_column": "the_geom", }, "v2_cross_section_view": { - "definition": "SELECT def.ROWID AS ROWID, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, l.id AS l_id, l.channel_id AS l_channel_id, l.definition_id AS l_definition_id, l.reference_level AS l_reference_level, l.friction_type AS l_friction_type, l.friction_value AS l_friction_value, l.bank_level AS l_bank_level, l.code AS l_code, l.the_geom AS the_geom, ch.id AS ch_id, ch.display_name AS ch_display_name, ch.code AS ch_code, ch.calculation_type AS ch_calculation_type, ch.dist_calc_points AS ch_dist_calc_points, ch.zoom_category AS ch_zoom_category, ch.connection_node_start_id AS ch_connection_node_start_id, ch.connection_node_end_id AS ch_connection_node_end_id FROM v2_cross_section_definition AS def , v2_cross_section_location AS l , v2_channel AS ch WHERE l.definition_id = def.id AND l.channel_id = ch.id", + "definition": "SELECT def.rowid AS rowid, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, l.id AS l_id, l.channel_id AS l_channel_id, l.definition_id AS l_definition_id, l.reference_level AS l_reference_level, l.friction_type AS l_friction_type, l.friction_value AS l_friction_value, l.bank_level AS l_bank_level, l.code AS l_code, l.the_geom AS the_geom, ch.id AS ch_id, ch.display_name AS ch_display_name, ch.code AS ch_code, ch.calculation_type AS ch_calculation_type, ch.dist_calc_points AS ch_dist_calc_points, ch.zoom_category AS ch_zoom_category, ch.connection_node_start_id AS ch_connection_node_start_id, ch.connection_node_end_id AS ch_connection_node_end_id FROM v2_cross_section_definition AS def , v2_cross_section_location AS l , v2_channel AS ch WHERE l.definition_id = def.id AND l.channel_id = ch.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_cross_section_location", "f_geometry_column": "the_geom", }, "v2_culvert_view": { - "definition": "SELECT cul.ROWID AS ROWID, cul.id AS cul_id, cul.display_name AS cul_display_name, cul.code AS cul_code, cul.calculation_type AS cul_calculation_type, cul.friction_value AS cul_friction_value, cul.friction_type AS cul_friction_type, cul.dist_calc_points AS cul_dist_calc_points, cul.zoom_category AS cul_zoom_category, cul.cross_section_definition_id AS cul_cross_section_definition_id, cul.discharge_coefficient_positive AS cul_discharge_coefficient_positive, cul.discharge_coefficient_negative AS cul_discharge_coefficient_negative, cul.invert_level_start_point AS cul_invert_level_start_point, cul.invert_level_end_point AS cul_invert_level_end_point, cul.the_geom AS the_geom, cul.connection_node_start_id AS cul_connection_node_start_id, cul.connection_node_end_id AS cul_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code FROM v2_culvert AS cul , v2_cross_section_definition AS def WHERE cul.cross_section_definition_id = def.id", + "definition": "SELECT cul.rowid AS rowid, cul.id AS cul_id, cul.display_name AS cul_display_name, cul.code AS cul_code, cul.calculation_type AS cul_calculation_type, cul.friction_value AS cul_friction_value, cul.friction_type AS cul_friction_type, cul.dist_calc_points AS cul_dist_calc_points, cul.zoom_category AS cul_zoom_category, cul.cross_section_definition_id AS cul_cross_section_definition_id, cul.discharge_coefficient_positive AS cul_discharge_coefficient_positive, cul.discharge_coefficient_negative AS cul_discharge_coefficient_negative, cul.invert_level_start_point AS cul_invert_level_start_point, cul.invert_level_end_point AS cul_invert_level_end_point, cul.the_geom AS the_geom, cul.connection_node_start_id AS cul_connection_node_start_id, cul.connection_node_end_id AS cul_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code FROM v2_culvert AS cul , v2_cross_section_definition AS def WHERE cul.cross_section_definition_id = def.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_culvert", "f_geometry_column": "the_geom", }, "v2_manhole_view": { - "definition": "SELECT manh.ROWID AS ROWID, manh.id AS manh_id, manh.display_name AS manh_display_name, manh.code AS manh_code, manh.connection_node_id AS manh_connection_node_id, manh.shape AS manh_shape, manh.width AS manh_width, manh.length AS manh_length, manh.manhole_indicator AS manh_manhole_indicator, manh.calculation_type AS manh_calculation_type, manh.bottom_level AS manh_bottom_level, manh.surface_level AS manh_surface_level, manh.drain_level AS manh_drain_level, manh.sediment_level AS manh_sediment_level, manh.zoom_category AS manh_zoom_category, node.id AS node_id, node.storage_area AS node_storage_area, node.initial_waterlevel AS node_initial_waterlevel, node.code AS node_code, node.the_geom AS the_geom, node.the_geom_linestring AS node_the_geom_linestring FROM v2_manhole AS manh , v2_connection_nodes AS node WHERE manh.connection_node_id = node.id", + "definition": "SELECT manh.rowid AS rowid, manh.id AS manh_id, manh.display_name AS manh_display_name, manh.code AS manh_code, manh.connection_node_id AS manh_connection_node_id, manh.shape AS manh_shape, manh.width AS manh_width, manh.length AS manh_length, manh.manhole_indicator AS manh_manhole_indicator, manh.calculation_type AS manh_calculation_type, manh.bottom_level AS manh_bottom_level, manh.surface_level AS manh_surface_level, manh.drain_level AS manh_drain_level, manh.sediment_level AS manh_sediment_level, manh.zoom_category AS manh_zoom_category, node.id AS node_id, node.storage_area AS node_storage_area, node.initial_waterlevel AS node_initial_waterlevel, node.code AS node_code, node.the_geom AS the_geom, node.the_geom_linestring AS node_the_geom_linestring FROM v2_manhole AS manh , v2_connection_nodes AS node WHERE manh.connection_node_id = node.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom", }, "v2_orifice_view": { - "definition": "SELECT orf.ROWID AS ROWID, orf.id AS orf_id, orf.display_name AS orf_display_name, orf.code AS orf_code, orf.crest_level AS orf_crest_level, orf.sewerage AS orf_sewerage, orf.cross_section_definition_id AS orf_cross_section_definition_id, orf.friction_value AS orf_friction_value, orf.friction_type AS orf_friction_type, orf.discharge_coefficient_positive AS orf_discharge_coefficient_positive, orf.discharge_coefficient_negative AS orf_discharge_coefficient_negative, orf.zoom_category AS orf_zoom_category, orf.crest_type AS orf_crest_type, orf.connection_node_start_id AS orf_connection_node_start_id, orf.connection_node_end_id AS orf_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_orifice AS orf, v2_cross_section_definition AS def, v2_connection_nodes AS start_node, v2_connection_nodes AS end_node where orf.connection_node_start_id = start_node.id AND orf.connection_node_end_id = end_node.id AND orf.cross_section_definition_id = def.id", + "definition": "SELECT orf.rowid AS rowid, orf.id AS orf_id, orf.display_name AS orf_display_name, orf.code AS orf_code, orf.crest_level AS orf_crest_level, orf.sewerage AS orf_sewerage, orf.cross_section_definition_id AS orf_cross_section_definition_id, orf.friction_value AS orf_friction_value, orf.friction_type AS orf_friction_type, orf.discharge_coefficient_positive AS orf_discharge_coefficient_positive, orf.discharge_coefficient_negative AS orf_discharge_coefficient_negative, orf.zoom_category AS orf_zoom_category, orf.crest_type AS orf_crest_type, orf.connection_node_start_id AS orf_connection_node_start_id, orf.connection_node_end_id AS orf_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_orifice AS orf, v2_cross_section_definition AS def, v2_connection_nodes AS start_node, v2_connection_nodes AS end_node where orf.connection_node_start_id = start_node.id AND orf.connection_node_end_id = end_node.id AND orf.cross_section_definition_id = def.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom_linestring", }, "v2_pipe_view": { - "definition": "SELECT pipe.ROWID AS ROWID, pipe.id AS pipe_id, pipe.display_name AS pipe_display_name, pipe.code AS pipe_code, pipe.profile_num AS pipe_profile_num, pipe.sewerage_type AS pipe_sewerage_type, pipe.calculation_type AS pipe_calculation_type, pipe.invert_level_start_point AS pipe_invert_level_start_point, pipe.invert_level_end_point AS pipe_invert_level_end_point, pipe.cross_section_definition_id AS pipe_cross_section_definition_id, pipe.friction_value AS pipe_friction_value, pipe.friction_type AS pipe_friction_type, pipe.dist_calc_points AS pipe_dist_calc_points, pipe.material AS pipe_material, pipe.original_length AS pipe_original_length, pipe.zoom_category AS pipe_zoom_category, pipe.connection_node_start_id AS pipe_connection_node_start_id, pipe.connection_node_end_id AS pipe_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_pipe AS pipe , v2_cross_section_definition AS def , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE pipe.connection_node_start_id = start_node.id AND pipe.connection_node_end_id = end_node.id AND pipe.cross_section_definition_id = def.id", + "definition": "SELECT pipe.rowid AS rowid, pipe.id AS pipe_id, pipe.display_name AS pipe_display_name, pipe.code AS pipe_code, pipe.profile_num AS pipe_profile_num, pipe.sewerage_type AS pipe_sewerage_type, pipe.calculation_type AS pipe_calculation_type, pipe.invert_level_start_point AS pipe_invert_level_start_point, pipe.invert_level_end_point AS pipe_invert_level_end_point, pipe.cross_section_definition_id AS pipe_cross_section_definition_id, pipe.friction_value AS pipe_friction_value, pipe.friction_type AS pipe_friction_type, pipe.dist_calc_points AS pipe_dist_calc_points, pipe.material AS pipe_material, pipe.original_length AS pipe_original_length, pipe.zoom_category AS pipe_zoom_category, pipe.connection_node_start_id AS pipe_connection_node_start_id, pipe.connection_node_end_id AS pipe_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_pipe AS pipe , v2_cross_section_definition AS def , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE pipe.connection_node_start_id = start_node.id AND pipe.connection_node_end_id = end_node.id AND pipe.cross_section_definition_id = def.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom_linestring", }, "v2_pumpstation_point_view": { - "definition": "SELECT a.ROWID AS ROWID, a.id AS pump_id, a.display_name, a.code, a.classification, a.sewerage, a.start_level, a.lower_stop_level, a.upper_stop_level, a.capacity, a.zoom_category, a.connection_node_start_id, a.connection_node_end_id, a.type, b.id AS connection_node_id, b.storage_area, b.the_geom FROM v2_pumpstation a JOIN v2_connection_nodes b ON a.connection_node_start_id = b.id", + "definition": "SELECT a.rowid AS rowid, a.id AS pump_id, a.display_name, a.code, a.classification, a.sewerage, a.start_level, a.lower_stop_level, a.upper_stop_level, a.capacity, a.zoom_category, a.connection_node_start_id, a.connection_node_end_id, a.type, b.id AS connection_node_id, b.storage_area, b.the_geom FROM v2_pumpstation a JOIN v2_connection_nodes b ON a.connection_node_start_id = b.id", "view_geometry": "the_geom", "view_rowid": "connection_node_start_id", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom", }, "v2_pumpstation_view": { - "definition": "SELECT pump.ROWID AS ROWID, pump.id AS pump_id, pump.display_name AS pump_display_name, pump.code AS pump_code, pump.classification AS pump_classification, pump.type AS pump_type, pump.sewerage AS pump_sewerage, pump.start_level AS pump_start_level, pump.lower_stop_level AS pump_lower_stop_level, pump.upper_stop_level AS pump_upper_stop_level, pump.capacity AS pump_capacity, pump.zoom_category AS pump_zoom_category, pump.connection_node_start_id AS pump_connection_node_start_id, pump.connection_node_end_id AS pump_connection_node_end_id, MakeLine( start_node.the_geom, end_node.the_geom ) AS the_geom FROM v2_pumpstation AS pump , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE pump.connection_node_start_id = start_node.id AND pump.connection_node_end_id = end_node.id", + "definition": "SELECT pump.rowid AS rowid, pump.id AS pump_id, pump.display_name AS pump_display_name, pump.code AS pump_code, pump.classification AS pump_classification, pump.type AS pump_type, pump.sewerage AS pump_sewerage, pump.start_level AS pump_start_level, pump.lower_stop_level AS pump_lower_stop_level, pump.upper_stop_level AS pump_upper_stop_level, pump.capacity AS pump_capacity, pump.zoom_category AS pump_zoom_category, pump.connection_node_start_id AS pump_connection_node_start_id, pump.connection_node_end_id AS pump_connection_node_end_id, MakeLine( start_node.the_geom, end_node.the_geom ) AS the_geom FROM v2_pumpstation AS pump , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE pump.connection_node_start_id = start_node.id AND pump.connection_node_end_id = end_node.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom_linestring", }, "v2_weir_view": { - "definition": "SELECT weir.ROWID AS ROWID, weir.id AS weir_id, weir.display_name AS weir_display_name, weir.code AS weir_code, weir.crest_level AS weir_crest_level, weir.crest_type AS weir_crest_type, weir.cross_section_definition_id AS weir_cross_section_definition_id, weir.sewerage AS weir_sewerage, weir.discharge_coefficient_positive AS weir_discharge_coefficient_positive, weir.discharge_coefficient_negative AS weir_discharge_coefficient_negative, weir.external AS weir_external, weir.zoom_category AS weir_zoom_category, weir.friction_value AS weir_friction_value, weir.friction_type AS weir_friction_type, weir.connection_node_start_id AS weir_connection_node_start_id, weir.connection_node_end_id AS weir_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_weir AS weir , v2_cross_section_definition AS def , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE weir.connection_node_start_id = start_node.id AND weir.connection_node_end_id = end_node.id AND weir.cross_section_definition_id = def.id", + "definition": "SELECT weir.rowid AS rowid, weir.id AS weir_id, weir.display_name AS weir_display_name, weir.code AS weir_code, weir.crest_level AS weir_crest_level, weir.crest_type AS weir_crest_type, weir.cross_section_definition_id AS weir_cross_section_definition_id, weir.sewerage AS weir_sewerage, weir.discharge_coefficient_positive AS weir_discharge_coefficient_positive, weir.discharge_coefficient_negative AS weir_discharge_coefficient_negative, weir.external AS weir_external, weir.zoom_category AS weir_zoom_category, weir.friction_value AS weir_friction_value, weir.friction_type AS weir_friction_type, weir.connection_node_start_id AS weir_connection_node_start_id, weir.connection_node_end_id AS weir_connection_node_end_id, def.id AS def_id, def.shape AS def_shape, def.width AS def_width, def.height AS def_height, def.code AS def_code, MakeLine( start_node.the_geom, end_node.the_geom) AS the_geom FROM v2_weir AS weir , v2_cross_section_definition AS def , v2_connection_nodes AS start_node , v2_connection_nodes AS end_node WHERE weir.connection_node_start_id = start_node.id AND weir.connection_node_end_id = end_node.id AND weir.cross_section_definition_id = def.id", "view_geometry": "the_geom", - "view_rowid": "ROWID", + "view_rowid": "rowid", "f_table_name": "v2_connection_nodes", "f_geometry_column": "the_geom_linestring", },