Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

1048 prideti galimybe apkeisti geometry tipo asis #1051

Merged
merged 11 commits into from
Jan 15, 2025
10 changes: 10 additions & 0 deletions CHANGES.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,22 @@ Backwards incompatible:
0.1.82 (unreleased)
===================

Backwards incompatible:

- `postgresql` `backend` now no longer ignores `prepare` functions. Meaning if there are properties, which has functions
set in `prepare` column, it can cause errors (if those functions are not supported in `postgresql` `backend`) (`#1048`_).

New features:

- Added support for `Object` type with `external` `Sql` `backend` (`#973`_).

.. _#973: https://github.com/atviriduomenys/spinta/issues/973

- Added 'flip` function, which currently only supports `Geometry` type (flips coordinate axis). This features only works
when reading data, meaning, when writing, you still need to provide coordinates in the right order (`#1048`_).

.. _#1048: https://github.com/atviriduomenys/spinta/issues/1048

Improvements:

- Client data and `keymap` is now cached. This will reduce amount of file reads with each request (`#948`_).
Expand Down
42 changes: 38 additions & 4 deletions spinta/backends/postgresql/ufuncs/query/ufuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import uuid
from typing import Union, Any

import geoalchemy2.functions
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import UUID

Expand All @@ -17,7 +18,7 @@
from spinta.core.ufuncs import Expr
from spinta.core.ufuncs import ufunc, GetAttr
from spinta.datasets.backends.sql.ufuncs.components import Selected
from spinta.exceptions import EmptyStringSearch, NoneValueComparison
from spinta.exceptions import EmptyStringSearch, NoneValueComparison, NotImplementedFeature
from spinta.exceptions import FieldNotInResource
from spinta.types.datatype import Array
from spinta.types.datatype import DataType, ExternalRef, Inherit, BackRef, Time, ArrayBackRef, Denorm
Expand All @@ -31,10 +32,11 @@
from spinta.types.datatype import Ref
from spinta.types.datatype import String
from spinta.types.datatype import UUID as UUID_dtype
from spinta.types.geometry.components import Geometry
from spinta.types.text.components import Text
from spinta.types.text.helpers import determine_language_property_for_text
from spinta.ufuncs.basequerybuilder.components import ReservedProperty, \
NestedProperty, ResultProperty
NestedProperty, ResultProperty, Flip
from spinta.ufuncs.basequerybuilder.helpers import get_column_with_extra, get_language_column, \
expandable_not_expanded
from spinta.ufuncs.basequerybuilder.ufuncs import Star
Expand Down Expand Up @@ -106,7 +108,7 @@ def select(env, arg):
prop = _get_property_for_select(env, arg.name)
if expandable_not_expanded(env, prop):
return Selected(None, prop, prep=[])
return env.call('select', prop.dtype)
return env.call('select', prop)


@ufunc.resolver(PgQueryBuilder, ForeignProperty, DataType)
Expand All @@ -128,7 +130,11 @@ def select(
@ufunc.resolver(PgQueryBuilder, Property)
def select(env, prop):
if prop.place not in env.resolved:
result = env.call("select", prop.dtype)
if prop.external and prop.external.prepare:
result = env(this=prop).resolve(prop.external.prepare)
result = env.call("select", prop.dtype, result)
else:
result = env.call("select", prop.dtype)
env.resolved[prop.place] = result
return env.resolved[prop.place]

Expand Down Expand Up @@ -427,6 +433,19 @@ def select(
return super_(env, fpr, dtype)


@ufunc.resolver(PgQueryBuilder, Geometry, Flip)
def select(env: PgQueryBuilder, dtype: Geometry, func_: Flip):
table = env.backend.get_table(env.model)

if dtype.prop.list is None:
column = env.backend.get_column(table, dtype.prop, select=True)
else:
column = env.backend.get_column(table, dtype.prop.list, select=True)

column = geoalchemy2.functions.ST_FlipCoordinates(column)
return Selected(env.add_column(column), prop=dtype.prop)


@ufunc.resolver(PgQueryBuilder, int)
def limit(env, n):
env.limit = n
Expand Down Expand Up @@ -541,6 +560,7 @@ def compare(env, op, dtype, value):
cond = _sa_compare(op, column, value)
return _prepare_condition(env, dtype.prop, cond)


@ufunc.resolver(PgQueryBuilder, UUID_dtype, str, names=COMPARE)
def compare(env, op, dtype, value):
column = env.backend.get_column(env.table, dtype.prop)
Expand Down Expand Up @@ -631,6 +651,7 @@ def eq(env, dtype, value):
cond = _sa_compare('eq', column, value)
return _prepare_condition(env, dtype.prop, cond)


@ufunc.resolver(PgQueryBuilder, DataType, type(None))
def eq(env, dtype, value):
column = env.backend.get_column(env.table, dtype.prop)
Expand Down Expand Up @@ -675,6 +696,7 @@ def _ensure_non_empty(op, s):
if s == '':
raise EmptyStringSearch(op=op)


@ufunc.resolver(PgQueryBuilder, UUID_dtype, str, names=COMPARE_STRING)
def compare(env: PgQueryBuilder, op: str, dtype: UUID, value: str):
if op in ('startswith', 'contains'):
Expand Down Expand Up @@ -834,6 +856,7 @@ def ne(env, dtype, value):
column = env.backend.get_column(env.table, dtype.prop)
return _ne_compare(env, dtype.prop, column, value)


@ufunc.resolver(PgQueryBuilder, UUID_dtype, str)
def ne(env, dtype, value):
column = env.backend.get_column(env.table, dtype.prop)
Expand Down Expand Up @@ -1012,6 +1035,7 @@ def _ne_compare(env: PgQueryBuilder, prop: Property, column, value):
FUNCS = [
'lower',
'upper',
'flip'
]


Expand Down Expand Up @@ -1235,3 +1259,13 @@ def checksum(env: PgQueryBuilder, expr: Expr):
return ResultProperty(
Expr('checksum', *args)
)


@ufunc.resolver(PgQueryBuilder, Geometry)
def flip(env: PgQueryBuilder, dtype: Geometry):
return Flip(dtype)


@ufunc.resolver(PgQueryBuilder, Expr)
def file(env: PgQueryBuilder, expr: Expr) -> Expr:
raise NotImplementedFeature(env.backend, feature="Ability to use file() function with `PostgreSql` backend")
41 changes: 37 additions & 4 deletions spinta/datasets/backends/sql/helpers.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
from typing import Union, Tuple

import geoalchemy2.functions
import sqlalchemy as sa


def extract_dialect(engine: sa.engine.Engine):
def _extract_dialect(engine: sa.engine.Engine):
if engine is not None:
return engine.dialect.name
return _DEFAULT_DIALECT_KEY


def does_dialect_match(src_dialect: str, target_dialect: Union[str, Tuple[str]]):
def _dialect_matches(src_dialect: str, target_dialect: Union[str, Tuple[str]]):
if not target_dialect or not src_dialect:
return False

Expand Down Expand Up @@ -61,6 +62,14 @@ def _default_desc(column: sa.Column):
), column.desc()


def _flip_geometry_postgis(column: sa.Column):
return geoalchemy2.functions.ST_FlipCoordinates(column)


def _default_flip(column: sa.Column):
return column


_DEFAULT_DIALECT_KEY = ""


Expand All @@ -72,17 +81,29 @@ def _default_desc(column: sa.Column):
("postgresql", "oracle", "sqlite"): _nulls_asc,
_DEFAULT_DIALECT_KEY: _default_asc
}
_GEOMETRY_FLIP_DIALECT_MAPPER = {
"postgresql": _flip_geometry_postgis,
_DEFAULT_DIALECT_KEY: _default_flip
}


def _dialect_specific_function(engine: sa.engine.Engine, dialect_function_mapper: dict, **kwargs):
dialect = extract_dialect(engine)
dialect = _extract_dialect(engine)
for key, func in dialect_function_mapper.items():
if key != _DEFAULT_DIALECT_KEY and does_dialect_match(dialect, key):
if key != _DEFAULT_DIALECT_KEY and _dialect_matches(dialect, key):
return func(**kwargs)

return dialect_function_mapper[_DEFAULT_DIALECT_KEY](**kwargs)


def _contains_dialect_function(engine: sa.engine.Engine, dialect_function_mapper: dict) -> bool:
dialect = _extract_dialect(engine)
for key, func in dialect_function_mapper.items():
if key != _DEFAULT_DIALECT_KEY and _dialect_matches(dialect, key):
return True
return False


def dialect_specific_desc(engine: sa.engine.Engine, column: sa.Column):
return _dialect_specific_function(
engine=engine,
Expand All @@ -97,3 +118,15 @@ def dialect_specific_asc(engine: sa.engine.Engine, column: sa.Column):
dialect_function_mapper=_ASC_DIALECT_MAPPER,
column=column
)


def dialect_specific_geometry_flip(engine: sa.engine, column: sa.Column):
return _dialect_specific_function(
engine=engine,
dialect_function_mapper=_GEOMETRY_FLIP_DIALECT_MAPPER,
column=column
)


def contains_geometry_flip_function(engine: sa.engine) -> bool:
return _contains_dialect_function(engine, _GEOMETRY_FLIP_DIALECT_MAPPER)
85 changes: 55 additions & 30 deletions spinta/datasets/backends/sql/ufuncs/query/ufuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
from spinta.core.ufuncs import Negative
from spinta.core.ufuncs import Unresolved
from spinta.core.ufuncs import ufunc
from spinta.datasets.backends.sql.helpers import dialect_specific_desc, dialect_specific_asc
from spinta.datasets.backends.sql.helpers import dialect_specific_desc, dialect_specific_asc, \
contains_geometry_flip_function, dialect_specific_geometry_flip
from spinta.datasets.backends.sql.ufuncs.query.components import SqlQueryBuilder
from spinta.dimensions.enum.helpers import prepare_enum_value
from spinta.exceptions import PropertyNotFound, SourceCannotBeList
Expand All @@ -27,9 +28,10 @@
from spinta.types.datatype import Ref
from spinta.types.datatype import String
from spinta.types.datatype import UUID
from spinta.types.geometry.components import Geometry
from spinta.types.text.components import Text
from spinta.types.text.helpers import determine_language_property_for_text
from spinta.ufuncs.basequerybuilder.components import LiteralProperty, Selected
from spinta.ufuncs.basequerybuilder.components import LiteralProperty, Selected, Flip
from spinta.ufuncs.basequerybuilder.helpers import get_language_column, process_literal_value
from spinta.ufuncs.basequerybuilder.ufuncs import Star
from spinta.ufuncs.components import ForeignProperty
Expand Down Expand Up @@ -287,6 +289,34 @@ def count(env: SqlQueryBuilder):
return sa.func.count()


def _get_property_for_select(
env: SqlQueryBuilder,
name: str,
*,
nested: bool = False,
) -> Property:
# TODO: `name` can refer to (in specified order):
# - var - a defined variable
# - param - a parameter if parametrization is used
# - item - an item of a dict or list
# - prop - a property
# Currently only `prop` is resolved.
prop = env.model.flatprops.get(name)
if prop and (
# Check authorization only for top level properties in select list.
# XXX: Not sure if nested is the right property to user, probably better
# option is to check if this call comes from a prepare context. But
# then how prepare context should be defined? Probably resolvers
# should be called with a different env class?
# tag:resolving_private_properties_in_prepare_context
nested or
authorized(env.context, prop, Action.SEARCH)
):
return prop
else:
raise PropertyNotFound(env.model, property=name)


@ufunc.resolver(SqlQueryBuilder, Expr)
def select(env: SqlQueryBuilder, expr: Expr):
keys = [str(k) for k in expr.args]
Expand Down Expand Up @@ -335,34 +365,6 @@ def select(env: SqlQueryBuilder, item: str, *, nested: bool = False):
return env.call('select', prop)


def _get_property_for_select(
env: SqlQueryBuilder,
name: str,
*,
nested: bool = False,
) -> Property:
# TODO: `name` can refer to (in specified order):
# - var - a defined variable
# - param - a parameter if parametrization is used
# - item - an item of a dict or list
# - prop - a property
# Currently only `prop` is resolved.
prop = env.model.flatprops.get(name)
if prop and (
# Check authorization only for top level properties in select list.
# XXX: Not sure if nested is the right property to user, probably better
# option is to check if this call comes from a prepare context. But
# then how prepare context should be defined? Probably resolvers
# should be called with a different env class?
# tag:resolving_private_properties_in_prepare_context
nested or
authorized(env.context, prop, Action.SEARCH)
):
return prop
else:
raise PropertyNotFound(env.model, property=name)


@ufunc.resolver(SqlQueryBuilder, Property)
def select(env: SqlQueryBuilder, prop: Property) -> Selected:
if prop.place not in env.resolved:
Expand Down Expand Up @@ -581,6 +583,19 @@ def select(
)


@ufunc.resolver(SqlQueryBuilder, Geometry, Flip)
def select(env: SqlQueryBuilder, dtype: Geometry, func_: Flip):
table = env.backend.get_table(env.model)

if dtype.prop.list is None:
column = env.backend.get_column(table, dtype.prop, select=True)
else:
column = env.backend.get_column(table, dtype.prop.list, select=True)

column = dialect_specific_geometry_flip(env.backend.engine, column)
return Selected(env.add_column(column), prop=dtype.prop)


@ufunc.resolver(SqlQueryBuilder, Property)
def join_table_on(env: SqlQueryBuilder, prop: Property) -> Any:
if prop.external.prepare is not NA:
Expand Down Expand Up @@ -808,3 +823,13 @@ def select(
) -> Selected:
super_ = ufunc.resolver[env, fpr, dtype]
return super_(env, fpr, dtype)


@ufunc.resolver(SqlQueryBuilder, Geometry)
def flip(env: SqlQueryBuilder, dtype: Geometry):
if contains_geometry_flip_function(env.backend.engine):
return Flip(dtype)

# Returning expr means, that it will be passed to ResultBuilder to handle it
return Expr('flip')

20 changes: 20 additions & 0 deletions spinta/datasets/backends/sql/ufuncs/result/ufuncs.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,16 @@
from decimal import Decimal
from typing import Any, overload, Optional

import shapely
from shapely.geometry.base import BaseGeometry

from spinta.core.ufuncs import ufunc, Expr
from spinta.datasets.backends.sql.ufuncs.components import FileSelected
from spinta.datasets.backends.sql.ufuncs.result.components import SqlResultBuilder
from spinta.exceptions import UnableToCast
from spinta.types.datatype import Integer, String
from spinta.types.file.components import FileData
from spinta.types.geometry.components import Geometry
from spinta.ufuncs.basequerybuilder.components import Selected


Expand Down Expand Up @@ -79,3 +83,19 @@ def point(env: SqlResultBuilder, x: Selected, y: Selected) -> Expr:
x = env.data[x.item]
y = env.data[y.item]
return f'POINT ({x} {y})'


@ufunc.resolver(SqlResultBuilder)
def flip(env: SqlResultBuilder):
return env.call('flip', env.prop.dtype, env.this)


@ufunc.resolver(SqlResultBuilder, Geometry, str)
def flip(env: SqlResultBuilder, dtype: Geometry, value: str):
values = value.split(';', 1)
shape: BaseGeometry = shapely.from_wkt(values[-1])
inverted: BaseGeometry = shapely.ops.transform(lambda x, y: (y, x), shape)
inverted: str = shapely.to_wkt(inverted)
if len(values) > 1:
return ';'.join([*values[:-1], inverted])
return inverted
Loading
Loading