From 130d953c405c21f1b34a57aebd7e04e6dac50b74 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 18 Jun 2025 14:28:23 +0200 Subject: [PATCH 1/8] initial tree commit --- app/api/tree.py | 57 ++++++++++++++++++++++++++++++++++++++++++++ app/models/tree.py | 22 +++++++++++++++++ app/schemas/tree.py | 38 +++++++++++++++++++++++++++++ app/services/tree.py | 37 ++++++++++++++++++++++++++++ 4 files changed, 154 insertions(+) create mode 100644 app/api/tree.py create mode 100644 app/models/tree.py create mode 100644 app/schemas/tree.py create mode 100644 app/services/tree.py diff --git a/app/api/tree.py b/app/api/tree.py new file mode 100644 index 0000000..4cb0614 --- /dev/null +++ b/app/api/tree.py @@ -0,0 +1,57 @@ +import json + +from typing import List, Dict, Any, Optional + +from fastapi import Depends, APIRouter, HTTPException, status +from geojson import Feature, FeatureCollection +from sqlalchemy.ext.asyncio import AsyncSession + +from ..schemas.tree import ( + TreeGeometryResponse, + TreeResponse +) +from ..dependencies import get_session +from ..services.tree import ( + get_tree_by_id +) + +route_police = APIRouter(prefix='/tree/v1') + +def create_geojson_from_rows(rows: List[Dict[str, Any]]) -> FeatureCollection: + features = [ + Feature( + id=row['id'], + geometry=json.loads(row['geojson']), + properties={'label': row['label']} + ) + for row in rows + ] + + crs = {'type': 'name', 'properties': { + 'name': 'urn:ogc:def:crs:OGC:1.3:CRS84'}} + + return FeatureCollection(features, crs=crs) + + +@route_police.get( + '/details', + response_model=TreeResponse, + tags=['Polizeidienststellen'], + description=( + 'Retrieves police station details based on the provided station id.' + ) +) +async def fetch_tree_by_id( + tree_id: int, + session: AsyncSession = Depends(get_session) +) -> List[TreeResponse]: + rows = await get_tree_by_id(session, tree_id) + identifier = f'station_id {tree_id}' + + if not rows: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'No matches found for {identifier}' + ) + + return rows diff --git a/app/models/tree.py b/app/models/tree.py new file mode 100644 index 0000000..9c2f2d6 --- /dev/null +++ b/app/models/tree.py @@ -0,0 +1,22 @@ +from tokenize import String +from datetime import date +from sqlmodel import SQLModel, Field +from typing import Optional +from geoalchemy2 import Geometry +from sqlalchemy import Column +from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION + + +class Street_tree_register(SQLModel, table=True): + __tablename__ = "flensburg.street_tree_register" + + id: int = Field(primary_key=True, nullable=False) + tree_number = Column(String, nullable=False) + street = Column(String, nullable=False) + area_type = Column(String) + species = Column(String, nullable=False) + north = Column(Numeric, nullable=False) + east = Column(Numeric, nullable=False) + register_date = Column(Date, nullable=False) + type = Column(String, nullable=False) + geom = Column(Geometry, ('POINT', srid=4326)) diff --git a/app/schemas/tree.py b/app/schemas/tree.py new file mode 100644 index 0000000..c362134 --- /dev/null +++ b/app/schemas/tree.py @@ -0,0 +1,38 @@ +from typing import List, Optional +from pydantic import BaseModel, EmailStr, HttpUrl + +class GeoPoint(BaseModel): + type: str = 'Point' + coordinates: List[float] + +class TreeResponse(BaseModel): + id: int + tree_number: str + street: str + area_type: str + species: str + north: float + east: float + register_date: str + type: str + geom = GeoPoint + +class CrsProperties(BaseModel): + name: str + + +class Crs(BaseModel): + type: str + properties: CrsProperties + + +class TreeFeature(BaseModel): + type: str = 'Feature' + id: int + geometry: GeoPoint + + +class TreeGeometryResponse(BaseModel): + type: str = 'FeatureCollection' + crs: Crs + features: List[TreeFeature] diff --git a/app/services/tree.py b/app/services/tree.py new file mode 100644 index 0000000..6f39dcd --- /dev/null +++ b/app/services/tree.py @@ -0,0 +1,37 @@ +from sqlalchemy.sql import text +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from fastapi import HTTPException + +from ..models.tree import Street_tree_register + +async def get_tree_by_id(session: AsyncSession, tree_id: int): + model = Street_tree_register + + stmt = select(model.id).where(model.id = tree_id) + + result = await session.execute() + stmt = text(''' + SELECT + ST_AsGeoJSON(s.wkb_geometry, 15)::jsonb AS geojson, + s.id, + s.name, + s.city, + s.zipcode, + s.street, + s.house_number, + s.telephone, + s.fax, + s.email, + s.website + FROM + sh_police_station AS s + WHERE + s.id = :station_id + ''') + + sql = stmt.bindparams(station_id=validated_station_id) + result = await session.execute(sql) + row = result.mappings().one_or_none() + + return row From cb3d5b956b71ef5049e914234846e162caa0ca02 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 18 Jun 2025 14:55:21 +0200 Subject: [PATCH 2/8] fixed small issues --- app/api/tree.py | 19 +++++++++---------- app/models/tree.py | 8 ++------ app/schemas/tree.py | 2 +- app/services/tree.py | 32 ++++---------------------------- 4 files changed, 16 insertions(+), 45 deletions(-) diff --git a/app/api/tree.py b/app/api/tree.py index 4cb0614..8f569e3 100644 --- a/app/api/tree.py +++ b/app/api/tree.py @@ -1,21 +1,20 @@ import json -from typing import List, Dict, Any, Optional +from typing import List, Dict, Any from fastapi import Depends, APIRouter, HTTPException, status from geojson import Feature, FeatureCollection from sqlalchemy.ext.asyncio import AsyncSession from ..schemas.tree import ( - TreeGeometryResponse, - TreeResponse + StreetTreeResponse ) from ..dependencies import get_session from ..services.tree import ( get_tree_by_id ) -route_police = APIRouter(prefix='/tree/v1') +route_street_tree = APIRouter(prefix='/street_tree/v1') def create_geojson_from_rows(rows: List[Dict[str, Any]]) -> FeatureCollection: features = [ @@ -33,20 +32,20 @@ def create_geojson_from_rows(rows: List[Dict[str, Any]]) -> FeatureCollection: return FeatureCollection(features, crs=crs) -@route_police.get( +@route_street_tree.get( '/details', - response_model=TreeResponse, - tags=['Polizeidienststellen'], + response_model=StreetTreeResponse, + tags=['Strassenbaeume'], description=( - 'Retrieves police station details based on the provided station id.' + 'Retrieves street tree details based on the provided tree id.' ) ) async def fetch_tree_by_id( tree_id: int, session: AsyncSession = Depends(get_session) -) -> List[TreeResponse]: +) -> List[StreetTreeResponse]: rows = await get_tree_by_id(session, tree_id) - identifier = f'station_id {tree_id}' + identifier = f'tree_id {tree_id}' if not rows: raise HTTPException( diff --git a/app/models/tree.py b/app/models/tree.py index 9c2f2d6..bb82857 100644 --- a/app/models/tree.py +++ b/app/models/tree.py @@ -1,10 +1,6 @@ -from tokenize import String -from datetime import date from sqlmodel import SQLModel, Field -from typing import Optional from geoalchemy2 import Geometry -from sqlalchemy import Column -from sqlalchemy.dialects.postgresql import DOUBLE_PRECISION +from sqlalchemy import Column, Numeric, String, Date class Street_tree_register(SQLModel, table=True): @@ -19,4 +15,4 @@ class Street_tree_register(SQLModel, table=True): east = Column(Numeric, nullable=False) register_date = Column(Date, nullable=False) type = Column(String, nullable=False) - geom = Column(Geometry, ('POINT', srid=4326)) + geom = Column(Geometry('POINT', srid=4326)) diff --git a/app/schemas/tree.py b/app/schemas/tree.py index c362134..995a7c2 100644 --- a/app/schemas/tree.py +++ b/app/schemas/tree.py @@ -5,7 +5,7 @@ class GeoPoint(BaseModel): type: str = 'Point' coordinates: List[float] -class TreeResponse(BaseModel): +class StreetTreeResponse(BaseModel): id: int tree_number: str street: str diff --git a/app/services/tree.py b/app/services/tree.py index 6f39dcd..6492548 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -1,37 +1,13 @@ -from sqlalchemy.sql import text from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.future import select -from fastapi import HTTPException -from ..models.tree import Street_tree_register +from ..models.tree import StreetTreeRegister async def get_tree_by_id(session: AsyncSession, tree_id: int): - model = Street_tree_register + model = StreetTreeRegister - stmt = select(model.id).where(model.id = tree_id) - - result = await session.execute() - stmt = text(''' - SELECT - ST_AsGeoJSON(s.wkb_geometry, 15)::jsonb AS geojson, - s.id, - s.name, - s.city, - s.zipcode, - s.street, - s.house_number, - s.telephone, - s.fax, - s.email, - s.website - FROM - sh_police_station AS s - WHERE - s.id = :station_id - ''') - - sql = stmt.bindparams(station_id=validated_station_id) - result = await session.execute(sql) + stmt = select(model.id).where(model.id == tree_id) + result = await session.execute(stmt) row = result.mappings().one_or_none() return row From 2b0f3d184d716b7215311cae9df44e160311908d Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 18 Jun 2025 18:42:33 +0200 Subject: [PATCH 3/8] fixed model name --- app/models/tree.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/app/models/tree.py b/app/models/tree.py index bb82857..381a29d 100644 --- a/app/models/tree.py +++ b/app/models/tree.py @@ -2,8 +2,7 @@ from geoalchemy2 import Geometry from sqlalchemy import Column, Numeric, String, Date - -class Street_tree_register(SQLModel, table=True): +class StreetTreeRegister(SQLModel): __tablename__ = "flensburg.street_tree_register" id: int = Field(primary_key=True, nullable=False) From 3e3f50a4d73c8e0ea7d1ed47dfd401009003a078 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 2 Jul 2025 19:15:29 +0200 Subject: [PATCH 4/8] add species query --- app/api/tree.py | 24 +++++++++++++++- app/services/tree.py | 66 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 88 insertions(+), 2 deletions(-) diff --git a/app/api/tree.py b/app/api/tree.py index 8f569e3..1dcd698 100644 --- a/app/api/tree.py +++ b/app/api/tree.py @@ -11,7 +11,8 @@ ) from ..dependencies import get_session from ..services.tree import ( - get_tree_by_id + get_tree_by_id, + get_tree_by_species ) route_street_tree = APIRouter(prefix='/street_tree/v1') @@ -54,3 +55,24 @@ async def fetch_tree_by_id( ) return rows + +@route_street_tree.get( + '/species', + response_model=StreetTreeResponse, + tags=['Strassenbaeume'], + description=( + 'Retrieves street tree details based on the provided tree id.' + ) +) +async def fetch_tree_by_species( + session: AsyncSession = Depends(get_session) +) -> List[StreetTreeResponse]: + rows = await get_tree_by_species(session) + + if not rows: + raise HTTPException( + status_code=status.HTTP_404_NOT_FOUND, + detail=f'No matches found' + ) + + return rows diff --git a/app/services/tree.py b/app/services/tree.py index 6bba84b..7ed862e 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -1,5 +1,8 @@ +from sqlalchemy import select, case, literal_column, func +from sqlalchemy.sql import exists, and_, not_ +from sqlalchemy.orm import aliased +from geoalchemy2 import functions as geofunc from sqlalchemy.ext.asyncio import AsyncSession -from sqlmodel import select from app.models.tree import StreetTreeRegister async def get_tree_by_id(session: AsyncSession, tree_id: int): @@ -10,3 +13,64 @@ async def get_tree_by_id(session: AsyncSession, tree_id: int): row = result.scalars().first() return row + +async def get_tree_by_species(session: AsyncSession): + # Alias for subquery + gef = aliased(StreetTreeRegister) + + stmt = ( + select( + StreetTreeRegister.id, + StreetTreeRegister.tree_number, + StreetTreeRegister.street, + StreetTreeRegister.species, + StreetTreeRegister.type, + func.round( + func.ST_X(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), + 6 + ).label("lon"), + func.round( + func.ST_Y(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), + 6 + ).label("lat"), + case( + ( + StreetTreeRegister.species.ilike("%Tilia%"), 1, + ), + ( + StreetTreeRegister.species.ilike("%Acer%"), 2, + ), + ( + StreetTreeRegister.species.ilike("%Quercus%"), 3, + ), + ( + StreetTreeRegister.species.ilike("%Fagus%"), 4, + ), + ( + StreetTreeRegister.species.ilike("%Betula%"), 5, + ), + ( + StreetTreeRegister.species.ilike("%Carpinus%"), 6, + ), + else_=0, + ).label("species_index") + ) + .where( + StreetTreeRegister.type == 'bestand', + not_( + exists() + .where( + and_( + gef.type == 'gefaellt', + gef.tree_number == StreetTreeRegister.tree_number, + gef.street == StreetTreeRegister.street, + ) + ) + ) + ) + .order_by(StreetTreeRegister.species) + ) + + result = await session.execute(stmt) + + return result.scalars().all() From fe4e50845d62b3fc7b2a55f16be2c9a78939bd04 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 2 Jul 2025 20:33:38 +0200 Subject: [PATCH 5/8] changed species query to sql --- app/services/tree.py | 90 ++++++++++++++++---------------------------- 1 file changed, 32 insertions(+), 58 deletions(-) diff --git a/app/services/tree.py b/app/services/tree.py index 7ed862e..123ee91 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -1,7 +1,6 @@ from sqlalchemy import select, case, literal_column, func -from sqlalchemy.sql import exists, and_, not_ +from sqlalchemy.sql import exists, and_, not_, text from sqlalchemy.orm import aliased -from geoalchemy2 import functions as geofunc from sqlalchemy.ext.asyncio import AsyncSession from app.models.tree import StreetTreeRegister @@ -15,62 +14,37 @@ async def get_tree_by_id(session: AsyncSession, tree_id: int): return row async def get_tree_by_species(session: AsyncSession): - # Alias for subquery - gef = aliased(StreetTreeRegister) - - stmt = ( - select( - StreetTreeRegister.id, - StreetTreeRegister.tree_number, - StreetTreeRegister.street, - StreetTreeRegister.species, - StreetTreeRegister.type, - func.round( - func.ST_X(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), - 6 - ).label("lon"), - func.round( - func.ST_Y(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), - 6 - ).label("lat"), - case( - ( - StreetTreeRegister.species.ilike("%Tilia%"), 1, - ), - ( - StreetTreeRegister.species.ilike("%Acer%"), 2, - ), - ( - StreetTreeRegister.species.ilike("%Quercus%"), 3, - ), - ( - StreetTreeRegister.species.ilike("%Fagus%"), 4, - ), - ( - StreetTreeRegister.species.ilike("%Betula%"), 5, - ), - ( - StreetTreeRegister.species.ilike("%Carpinus%"), 6, - ), - else_=0, - ).label("species_index") - ) - .where( - StreetTreeRegister.type == 'bestand', - not_( - exists() - .where( - and_( - gef.type == 'gefaellt', - gef.tree_number == StreetTreeRegister.tree_number, - gef.street == StreetTreeRegister.street, - ) - ) - ) - ) - .order_by(StreetTreeRegister.species) - ) + stmt = text(''' + SELECT + tr.id, + tr.tree_number, + tr.street, + tr.species, + tr.type, + ROUND(ST_X(ST_Transform(tr.geom, 4326))::numeric, 6) AS lon, + ROUND(ST_Y(ST_Transform(tr.geom, 4326))::numeric, 6) AS lat, + CASE + WHEN tr.species ILIKE '%Tilia%' THEN 1 + WHEN tr.species ILIKE '%Acer%' THEN 2 + WHEN tr.species ILIKE '%Quercus%' THEN 3 + WHEN tr.species ILIKE '%Fagus%' THEN 4 + WHEN tr.species ILIKE '%Betula%' THEN 5 + WHEN tr.species ILIKE '%Carpinus%' THEN 6 + ELSE 0 + END AS species_index + FROM {{schema}}.street_tree_register tr + WHERE tr.type = 'bestand' + AND NOT EXISTS ( + SELECT 1 + FROM {{schema}}.street_tree_register gef + WHERE gef.type = 'gefaellt' + AND gef.tree_number = tr.tree_number + AND gef.street = tr.street + ) + ORDER BY tr.species + ''') result = await session.execute(stmt) + rows = result.mappings().all() - return result.scalars().all() + return [dict(row) for row in rows] From 50d1946e59475d3c1e43fee73addbfa8beb24f18 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Wed, 2 Jul 2025 21:00:45 +0200 Subject: [PATCH 6/8] fixed sql query typo --- app/services/tree.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/services/tree.py b/app/services/tree.py index 123ee91..dba5e8a 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -32,11 +32,11 @@ async def get_tree_by_species(session: AsyncSession): WHEN tr.species ILIKE '%Carpinus%' THEN 6 ELSE 0 END AS species_index - FROM {{schema}}.street_tree_register tr + FROM flensburg.street_tree_register tr WHERE tr.type = 'bestand' AND NOT EXISTS ( SELECT 1 - FROM {{schema}}.street_tree_register gef + FROM flensburg.street_tree_register gef WHERE gef.type = 'gefaellt' AND gef.tree_number = tr.tree_number AND gef.street = tr.street From de33abf171c9a89e8126e5e60200efac71398216 Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Tue, 8 Jul 2025 06:46:55 +0200 Subject: [PATCH 7/8] changed species query to sqalchemy --- app/services/tree.py | 84 +++++++++++++++++++++++++++++--------------- 1 file changed, 55 insertions(+), 29 deletions(-) diff --git a/app/services/tree.py b/app/services/tree.py index dba5e8a..a019250 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -14,35 +14,61 @@ async def get_tree_by_id(session: AsyncSession, tree_id: int): return row async def get_tree_by_species(session: AsyncSession): - stmt = text(''' - SELECT - tr.id, - tr.tree_number, - tr.street, - tr.species, - tr.type, - ROUND(ST_X(ST_Transform(tr.geom, 4326))::numeric, 6) AS lon, - ROUND(ST_Y(ST_Transform(tr.geom, 4326))::numeric, 6) AS lat, - CASE - WHEN tr.species ILIKE '%Tilia%' THEN 1 - WHEN tr.species ILIKE '%Acer%' THEN 2 - WHEN tr.species ILIKE '%Quercus%' THEN 3 - WHEN tr.species ILIKE '%Fagus%' THEN 4 - WHEN tr.species ILIKE '%Betula%' THEN 5 - WHEN tr.species ILIKE '%Carpinus%' THEN 6 - ELSE 0 - END AS species_index - FROM flensburg.street_tree_register tr - WHERE tr.type = 'bestand' - AND NOT EXISTS ( - SELECT 1 - FROM flensburg.street_tree_register gef - WHERE gef.type = 'gefaellt' - AND gef.tree_number = tr.tree_number - AND gef.street = tr.street - ) - ORDER BY tr.species - ''') + # Alias for subquery + gef = aliased(StreetTreeRegister) + + stmt = ( + select( + StreetTreeRegister.id, + StreetTreeRegister.tree_number, + StreetTreeRegister.street, + StreetTreeRegister.species, + StreetTreeRegister.type, + func.round( + func.ST_X(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), + 6 + ).label("lon"), + func.round( + func.ST_Y(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), + 6 + ).label("lat"), + case( + ( + StreetTreeRegister.species.ilike("%Tilia%"), 1, + ), + ( + StreetTreeRegister.species.ilike("%Acer%"), 2, + ), + ( + StreetTreeRegister.species.ilike("%Quercus%"), 3, + ), + ( + StreetTreeRegister.species.ilike("%Fagus%"), 4, + ), + ( + StreetTreeRegister.species.ilike("%Betula%"), 5, + ), + ( + StreetTreeRegister.species.ilike("%Carpinus%"), 6, + ), + else_=0, + ).label("species_index") + ) + .where( + StreetTreeRegister.type == 'bestand', + not_( + exists() + .where( + and_( + gef.type == 'gefaellt', + gef.tree_number == StreetTreeRegister.tree_number, + gef.street == StreetTreeRegister.street, + ) + ) + ) + ) + .order_by(StreetTreeRegister.species) + ) result = await session.execute(stmt) rows = result.mappings().all() From 8314cfb09aca3e5168c9f90f6ee0ce269d7b736d Mon Sep 17 00:00:00 2001 From: sonnenteich Date: Tue, 8 Jul 2025 06:49:58 +0200 Subject: [PATCH 8/8] changed back to sql query --- app/services/tree.py | 84 +++++++++++++++----------------------------- 1 file changed, 29 insertions(+), 55 deletions(-) diff --git a/app/services/tree.py b/app/services/tree.py index a019250..dba5e8a 100644 --- a/app/services/tree.py +++ b/app/services/tree.py @@ -14,61 +14,35 @@ async def get_tree_by_id(session: AsyncSession, tree_id: int): return row async def get_tree_by_species(session: AsyncSession): - # Alias for subquery - gef = aliased(StreetTreeRegister) - - stmt = ( - select( - StreetTreeRegister.id, - StreetTreeRegister.tree_number, - StreetTreeRegister.street, - StreetTreeRegister.species, - StreetTreeRegister.type, - func.round( - func.ST_X(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), - 6 - ).label("lon"), - func.round( - func.ST_Y(func.ST_Transform(StreetTreeRegister.geom, 4326)).cast('numeric'), - 6 - ).label("lat"), - case( - ( - StreetTreeRegister.species.ilike("%Tilia%"), 1, - ), - ( - StreetTreeRegister.species.ilike("%Acer%"), 2, - ), - ( - StreetTreeRegister.species.ilike("%Quercus%"), 3, - ), - ( - StreetTreeRegister.species.ilike("%Fagus%"), 4, - ), - ( - StreetTreeRegister.species.ilike("%Betula%"), 5, - ), - ( - StreetTreeRegister.species.ilike("%Carpinus%"), 6, - ), - else_=0, - ).label("species_index") - ) - .where( - StreetTreeRegister.type == 'bestand', - not_( - exists() - .where( - and_( - gef.type == 'gefaellt', - gef.tree_number == StreetTreeRegister.tree_number, - gef.street == StreetTreeRegister.street, - ) - ) - ) - ) - .order_by(StreetTreeRegister.species) - ) + stmt = text(''' + SELECT + tr.id, + tr.tree_number, + tr.street, + tr.species, + tr.type, + ROUND(ST_X(ST_Transform(tr.geom, 4326))::numeric, 6) AS lon, + ROUND(ST_Y(ST_Transform(tr.geom, 4326))::numeric, 6) AS lat, + CASE + WHEN tr.species ILIKE '%Tilia%' THEN 1 + WHEN tr.species ILIKE '%Acer%' THEN 2 + WHEN tr.species ILIKE '%Quercus%' THEN 3 + WHEN tr.species ILIKE '%Fagus%' THEN 4 + WHEN tr.species ILIKE '%Betula%' THEN 5 + WHEN tr.species ILIKE '%Carpinus%' THEN 6 + ELSE 0 + END AS species_index + FROM flensburg.street_tree_register tr + WHERE tr.type = 'bestand' + AND NOT EXISTS ( + SELECT 1 + FROM flensburg.street_tree_register gef + WHERE gef.type = 'gefaellt' + AND gef.tree_number = tr.tree_number + AND gef.street = tr.street + ) + ORDER BY tr.species + ''') result = await session.execute(stmt) rows = result.mappings().all()