From 5c80db0cf6d60494591596314bb0ebc4a9199109 Mon Sep 17 00:00:00 2001 From: ulysse <ulysse.chosson@obspm.fr> Date: Thu, 19 May 2022 11:11:37 +0200 Subject: [PATCH 1/2] All my past code for MagicDotPathOperator and +, no precommit operator -, no precommit better name, no precommit docstring, tests, linters version, changelog for operator + and - add comment todo in main better just autorebase correct class, name, file name and one case operator mul, div, mod cast in decimal for int and float --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe6af85..00bca38 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -42,8 +42,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- Add documentation for our git workflow. - +- Add `+` operator for MagicDotPath. +- Add `-` operator for MagicDotPath. ## [0.84.0] - 2022-05-18 -- GitLab From 000529c55783914b7b8720326fdb509facb963fa Mon Sep 17 00:00:00 2001 From: ulysse <ulysse.chosson@obspm.fr> Date: Tue, 24 May 2022 14:18:32 +0200 Subject: [PATCH 2/2] New struuct for MagicDP and MagicDPWithOp WIP new struct for MagicDotPath, no precommit Refactor MagicDotPath and MagicDotPathWithOp version correct docstring and function name --- CHANGELOG.md | 13 +- main.py | 12 +- py_linq_sql/build_request/alter.py | 21 +- py_linq_sql/build_request/consult.py | 41 +- py_linq_sql/build_request/consult_context.py | 14 +- py_linq_sql/build_request/one.py | 32 +- py_linq_sql/exception/exception.py | 6 +- py_linq_sql/utils/classes.py | 407 ++++++++++++------- py_linq_sql/utils/functions.py | 190 +++++---- pyproject.toml | 2 +- tests/exceptions/test_sql_commands.py | 7 + tests/operators/test_sub.py | 4 + 12 files changed, 444 insertions(+), 305 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00bca38..64a5231 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,10 +16,21 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Add `square root` operator for MagicDotPath. - Add `cube root` operator for MagicDotPath. - Add `factorial` operator for MagicDotPath. +- Add `-` unaire operator for MagicDotPath. ## [Unreleased] -## [0.87.0] - 2022-05-19 +## [0.88.0] - 2022-05-24 + +### Added + +- Add a parent dataclass for MagicDotPath and MagicDotPathWithOp, `BaseMagicDotPath()` + +### Changed +- Change how we get the column name for a MagicDotPath. +- Change the struct of `MagicDotPathWithOp()` + +## [0.87.0] - 2022-05-24 ### Added diff --git a/main.py b/main.py index 737acc3..88caeda 100644 --- a/main.py +++ b/main.py @@ -645,12 +645,10 @@ with tmp_connection("dummypassword", "./create_db_for_main.sql") as con: ) pretty_print(se) - # TODO: Ensure that we can do several operation without parentheses - with _counted_header() as x: se = ( SQLEnumerable(con, "objects") - .select(lambda x: (x.data.obj.mass, x.data.obj.mass + 1)) + .select(lambda x: (x.data.obj.mass, x.data.obj.mass + 1 + x.data.obj.mass)) .execute() ) pretty_print(se) @@ -686,3 +684,11 @@ with tmp_connection("dummypassword", "./create_db_for_main.sql") as con: .execute() ) pretty_print(se) + + with _counted_header() as x: + se = ( + SQLEnumerable(con, "objects") + .select(lambda x: (x.data.obj.name, x.data.obj.mass - (-10 - 3))) + .execute() + ) + pretty_print(se) diff --git a/py_linq_sql/build_request/alter.py b/py_linq_sql/build_request/alter.py index efa8e4c..919c6ba 100644 --- a/py_linq_sql/build_request/alter.py +++ b/py_linq_sql/build_request/alter.py @@ -5,7 +5,7 @@ from typing import Any, Dict, List, Set, Tuple # Local imports from ..exception.exception import DeleteError, NeedWhereError, TooManyReturnValueError -from ..utils.classes import Command, MagicDotPathWithOp, SQLEnumerableData +from ..utils.classes import Command, MagicDotPath, SQLEnumerableData from ..utils.functions import get_json, get_path, get_update_path from .consult import build_where @@ -182,17 +182,18 @@ def build_update( - TooManyValueError: If length of paths to update > 1 """ fquery = command.args.fquery # pylint: disable=duplicate-code - mdp_w_path = fquery(MagicDotPathWithOp(sqle.connection)) + mdp_w_path = fquery(MagicDotPath(sqle.connection)) path = get_path(mdp_w_path, sqle) if len(path) > 1: raise TooManyReturnValueError("Update") - column = mdp_w_path.column - other = mdp_w_path.other + operand_1 = mdp_w_path.operand_1 + column = operand_1.column + operand_2 = mdp_w_path.operand_2 - json = len(mdp_w_path.attributes) > 1 - path_for_update = "-".join(mdp_w_path.attributes[1:]) + json = len(operand_1.attributes) > 1 + path_for_update = "-".join(operand_1.attributes[1:]) result = [f"UPDATE {sqle.table} SET {column} ="] @@ -202,13 +203,13 @@ def build_update( f"""'{get_update_path(path_for_update)}'::text[], '""", ) - match other: + match operand_2: case str() if not json: - result.append(f"'{other}'") + result.append(f"'{operand_2}'") case str() if json: - result.append(f'"{other}"') + result.append(f'"{operand_2}"') case _: - result.append(f"{other}") + result.append(f"{operand_2}") if json: result.append("', false)") diff --git a/py_linq_sql/build_request/consult.py b/py_linq_sql/build_request/consult.py index 821aade..13b1be8 100644 --- a/py_linq_sql/build_request/consult.py +++ b/py_linq_sql/build_request/consult.py @@ -9,12 +9,12 @@ from dotmap import DotMap # Local imports from ..exception.exception import LengthMismatchError, UnknownCommandType from ..utils.classes import ( + BaseMagicDotPath, Command, CommandType, JoinType, MagicDotPath, MagicDotPathAggregate, - MagicDotPathWithOp, SQLEnumerableData, Terminal, ) @@ -222,14 +222,17 @@ def _get_selected_by( """ selected = [] by = [] # pylint: disable=invalid-name + mdp_for_names = [] match mdps: case MagicDotPath(): by.append(get_path(mdps)[0]) + mdp_for_names.append(mdps) case tuple(): for magic_dp in mdps: match magic_dp: case MagicDotPath(): by.append(get_path(magic_dp)[0]) + mdp_for_names.append(magic_dp) case MagicDotPathAggregate(): selected.append(get_aggregate(magic_dp)) case _: @@ -243,9 +246,10 @@ def _get_selected_by( "tuple of MagicDotPath or tuple of MagicDotPathAggregate.", ) + names = get_columns_name(mdp_for_names) selected.insert( 0, - ", ".join([f"{path.paths} AS {path.name}" for path in get_columns_name(by)]), + ", ".join([f"{path} AS {name}" for path, name in zip(by, names)]), ) return ", ".join(selected), ", ".join(by) @@ -350,8 +354,15 @@ def build_join( # pylint: disable=too-many-locals if not paths.select_paths: result.append(f"{outer.table}.*, {inner.table}.*") else: - selected = get_columns_name(paths.select_paths) - result.append(", ".join([f"{path.paths} AS {path.name}" for path in selected])) + obj_inner = MagicDotPath(inner.connection, with_table=inner.table) + obj_outer = MagicDotPath(outer.connection, with_table=outer.table) + mdp_select_paths = result_function(obj_inner, obj_outer) + names = get_columns_name(mdp_select_paths) + result.append( + ", ".join( + [f"{path} AS {name}" for path, name in zip(paths.select_paths, names)], + ), + ) result.append(f"FROM {outer.table} {join_type.as_str} JOIN {inner.table} ON") @@ -401,18 +412,24 @@ def build_select( if term: result.append(term) + # TODO: erreur si select 1+1 (pas de mdp) if not term or sqle.flags.terminal in [Terminal.DISTINCT]: if not fquery: result.append("*") else: - mdp_w_path = fquery(MagicDotPathWithOp(sqle.connection)) - if isinstance(mdp_w_path, MagicDotPathWithOp): - paths = [get_one_predicate_as_str(sqle, mdp_w_path)] - else: - paths = [get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path] - selected = get_columns_name(paths) + mdp_w_path = fquery(MagicDotPath(sqle.connection)) + match mdp_w_path: + case BaseMagicDotPath(): + paths = [get_one_predicate_as_str(sqle, mdp_w_path)] + case tuple(): + paths = [get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path] + case _: + raise TypeError( + "You must put a MagicDotPath in lambda, see the documentation.", + ) + names = get_columns_name(mdp_w_path) result.append( - ", ".join([f"{path.paths} AS {path.name}" for path in selected]), + ", ".join([f"{path} AS {name}" for path, name in zip(paths, names)]), ) # If we have a Group_by we build this @@ -433,6 +450,6 @@ def build_select( if sqle.flags.terminal == Terminal.GROUP_BY: result.append("GROUP BY") - result.append(", ".join([path.paths for path in selected])) + result.append(", ".join(list(path for path in paths))) return " ".join(filter(None, result)) diff --git a/py_linq_sql/build_request/consult_context.py b/py_linq_sql/build_request/consult_context.py index a144be9..d30e024 100644 --- a/py_linq_sql/build_request/consult_context.py +++ b/py_linq_sql/build_request/consult_context.py @@ -5,8 +5,8 @@ from typing import Set, Tuple # Local imports from ..exception.exception import ReturnEmptyEnumerable -from ..utils.classes import Command, CommandType, MagicDotPathWithOp, SQLEnumerableData -from ..utils.functions import get_one_predicate_as_str, get_paths +from ..utils.classes import Command, CommandType, SQLEnumerableData +from ..utils.functions import get_paths, get_predicates_as_str # -------------------- # | Context builds | @@ -188,15 +188,7 @@ def _build_one_where( result = ["WHERE"] if first else ["AND"] - mdp_w_path = fquery(MagicDotPathWithOp(sqle.connection)) - - # TODO: duplicate code reduce this - if isinstance(mdp_w_path, MagicDotPathWithOp): # pylint: disable=duplicate-code - result.append(get_one_predicate_as_str(sqle, mdp_w_path)) - else: - result.append( - " AND ".join([get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path]), - ) + get_predicates_as_str(result, fquery, sqle) return " ".join(result) diff --git a/py_linq_sql/build_request/one.py b/py_linq_sql/build_request/one.py index d72389f..b5e895a 100644 --- a/py_linq_sql/build_request/one.py +++ b/py_linq_sql/build_request/one.py @@ -4,8 +4,8 @@ from typing import Any, Dict # Local imports -from ..utils.classes import Command, MagicDotPathWithOp, SQLEnumerableData -from ..utils.functions import get_json, get_one_predicate_as_str +from ..utils.classes import Command, SQLEnumerableData +from ..utils.functions import get_json, get_predicates_as_str def build_any(command: Command, sqle: SQLEnumerableData) -> str: @@ -20,7 +20,7 @@ def build_any(command: Command, sqle: SQLEnumerableData) -> str: Request to execute. Raises : - - TypeError if arguments of get_path (in get_one_predicate_as_str) are not + - TypeError if arguments of get_path (in get_predicates_as_str) are not MagicDotPath or tuple of MagicDotPath """ fquery = command.args.fquery @@ -30,19 +30,9 @@ def build_any(command: Command, sqle: SQLEnumerableData) -> str: if not fquery: return result[0] - mdp_w_path = fquery(MagicDotPathWithOp(sqle.connection)) - result.append("WHERE") - # TODO: duplicate code reuduce it - if isinstance(mdp_w_path, MagicDotPathWithOp): - result.append( - get_one_predicate_as_str(sqle, mdp_w_path), - ) # pylint: disable=duplicate-code - else: - result.append( - " AND ".join([get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path]), - ) + get_predicates_as_str(result, fquery, sqle) return " ".join(result) @@ -59,24 +49,14 @@ def build_all(command: Command, sqle: SQLEnumerableData) -> str: Request to execute. Raises : - - TypeError if arguments of get_path (in build_one_predicate) are not + - TypeError if arguments of get_path (in get_predicates_as_str) are not MagicDotPath or tuple of MagicDotPath """ fquery = command.args.fquery result = [f"SELECT CASE WHEN ((SELECT COUNT(*) FROM {sqle.table} WHERE"] - # TODO: duplicate code reuduce it - mdp_w_path = fquery( - MagicDotPathWithOp(sqle.connection), - ) # pylint: disable=duplicate-code - - if isinstance(mdp_w_path, MagicDotPathWithOp): - result.append(get_one_predicate_as_str(sqle, mdp_w_path)) - else: - result.append( - " AND ".join([get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path]), - ) + get_predicates_as_str(result, fquery, sqle) result.append( f") = (SELECT COUNT(*) FROM {sqle.table})) THEN 1 ELSE 0 END FROM {sqle.table}", diff --git a/py_linq_sql/exception/exception.py b/py_linq_sql/exception/exception.py index d5b05e2..f29f8d5 100644 --- a/py_linq_sql/exception/exception.py +++ b/py_linq_sql/exception/exception.py @@ -51,12 +51,8 @@ class TypeOperatorError(TypeError): def __init__(self, expected: List[type], actual: type) -> None: """Initialize a TypeOperatorError.""" - # HACK: can't use a \ in a fstring expr - type_list = "\n- ".join(str(expected)) super().__init__( - f"Wrong type, only :\n" - f"- {type_list}\n" - f"can be used here. Not {actual}.", + f"Wrong type, only :\n" f"{expected} " f"can be used here. Not {actual}.", ) diff --git a/py_linq_sql/utils/classes.py b/py_linq_sql/utils/classes.py index b3635c2..cc5a63e 100644 --- a/py_linq_sql/utils/classes.py +++ b/py_linq_sql/utils/classes.py @@ -42,12 +42,35 @@ def equality(obj: Any, other: Any) -> bool: return True +def clean_number_str(str_to_clean: str) -> str: + """ + Clear a string representing a number (int or float). + + Replace all '.' by '_', '+' by 'plus' and '-' by 'minus'. + + Args: + str_to_clean: string to reformat. + + Returns: + The string at the good format. + + Examples: + >>> clean_number_str("2.0") + '2_0' + >>> clean_number_str("-8") + 'minus8' + >>> clean_number_str("+12") + 'plus12' + >>> clean_number_str("-22.075") + 'minus22_075' + """ + return str_to_clean.replace(".", "_").replace("+", "plus").replace("-", "minus") + + # --------------- # | NameTuple | # --------------- -PathsAndName = namedtuple("PathsAndName", "paths name") - _Type = namedtuple("_Type", ["is_intersect", "as_str"]) @@ -221,10 +244,9 @@ class Flags: ) -@dataclass -class MagicDotPath: +class BaseMagicDotPath: """ - Magical object that can have any attribute. + Abstract base for Magical object with operator methods. Inspired by The very useful [DotMap module](https://github.com/drgrib/dotmap) this object allow to write predicate in the lambda function used in SQL clause like @@ -232,145 +254,28 @@ class MagicDotPath: See the LINQ documentation for more in depth explanation. - Attributes: - - connection: Connection to a db. Useful for safe. - - attributes: Attributes for build the jsonb path. - - with_table: Table on which we want to get paths. - - column: Column on which we will want to make the request. - """ - connection: Connection - attributes: list[str] = field(default_factory=list) - with_table: str | None = None - column: str | None = None - - def __getattr__(self, attribute_name: str) -> MagicDotPath: - """ - Get attribute in a list for a MagicDotPath objects. - - Args : - attribute_name : Names of all attributes. - - Returns : - A MagicDotPath objects with attributes names in attributes list. - """ - return MagicDotPath( - self.connection, - attributes=self.attributes + [f"'{attribute_name}'"], - with_table=self.with_table, - ) - - def safe(self, name: str) -> str: - """ - Secure a column or a table for a request. - - Args : - - name : Name of the column or table we want to secure. - - Returns : - Name but verified by psycopg.sql.Identifier - """ - return sql.Identifier(name).as_string(self.connection) - - def jsonb_path(self, as_str: bool) -> str: - """ - Get the corresponding jsonb path from a MagicDotPath. - - Args : - - as_str : boolean to force the request with json in str. - - Returns : - a path with the correct jsonb syntax - """ - self.column = self.safe(self.attributes[0][1:-1]) - - res = "" - - if self.with_table: - res += f"{self.with_table}." - - if len(self.attributes) == 1: - res += self.column - else: - if self.my_type == str or as_str: - res += ( - f"{self.column}->" - f'{"->".join(self.attributes[1:-1])}->>{self.attributes[-1]}' - ) - else: - res += f'{self.column}->{"->".join(self.attributes[1:])}' - return res - - -@dataclass -class MagicDotPathWithOp(MagicDotPath): - """ - Magical object that can have any attribute and can be subject to many comparison. - - This class inherited from MagicDotPath. - - Inspired by The very useful [DotMap module](https://github.com/drgrib/dotmap) this - object allow to write predicate in the lambda function used in SQL clause like - `.select()` or `.join()`. This is very useful to express SQL constraint. - - See the LINQ documentation for more in depth explanation. - - Attributes: - - connection: Connection to a db. Useful for safe. - - attributes: Attributes for build the jsonb path. - - with_table: Table on which we want to get paths. - - column: Column on which we will want to make the request. - - other: Element to which we compare during a comparison. - - operator: Operator for comparison. - - my_type: Type of other. - - """ - - other: Any = None - operator: str | None = None - my_type: Type[Any] | None = None - - def __getattr__(self, attribute_name: str) -> MagicDotPathWithOp: - """ - Get attribute in a list for a MagicDotPathWithOp objects. - - Args : - attribute_name : Names of all attributes. - - Returns : - A MagicDotPathWithOp objects with attributes names in attributes list. - """ - mdp = super().__getattr__(attribute_name) - return MagicDotPathWithOp( - mdp.connection, - attributes=mdp.attributes, - with_table=mdp.with_table, - ) - def _get_number_operator( self, other: _OTHER_TYPE, operator: str, ) -> MagicDotPathWithOp: - other_type: Type[Any] | None = None + other_type: Type[Any] match other: case float(): other_type = float case int(): other_type = int - case MagicDotPathWithOp(): - other_type = MagicDotPathWithOp + case BaseMagicDotPath(): + other_type = BaseMagicDotPath case _: - raise TypeOperatorError([int, float, MagicDotPathWithOp], type(other)) + raise TypeOperatorError([int, float, BaseMagicDotPath], type(other)) return MagicDotPathWithOp( - self.connection, - attributes=self.attributes, - with_table=self.with_table, - column=self.column, + operand_1=self, operator=operator, + operand_2=other, my_type=other_type, - other=other, ) def _get_generic_operator( @@ -390,19 +295,18 @@ class MagicDotPathWithOp(MagicDotPath): other_type = list case dict(): other_type = dict + case BaseMagicDotPath(): + other_type = BaseMagicDotPath case _: raise TypeOperatorError( - [float, int, str, list, dict, MagicDotPath], + [float, int, str, list, dict, BaseMagicDotPath], type(other), ) return MagicDotPathWithOp( - self.connection, - attributes=self.attributes, - with_table=self.with_table, - column=self.column, + operand_1=self, operator=operator, + operand_2=other, my_type=other_type, - other=other, ) def __gt__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: @@ -507,7 +411,7 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_generic_operator(other, "<>") - def __add__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: # type: ignore + def __add__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: """ Get a MagicDotPathWithOp objects with a `+` operator and the correct types. @@ -524,7 +428,7 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_number_operator(other, "+") - def __sub__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: # type: ignore + def __sub__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: """ Get a MagicDotPathWithOp objects with a `-` operator and the correct types. @@ -541,7 +445,7 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_number_operator(other, "-") - def __mul__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: # type: ignore + def __mul__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: """ Get a MagicDotPathWithOp objects with a `*` operator and the correct types. @@ -558,7 +462,7 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_number_operator(other, "*") - def __truediv__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: # type: ignore + def __truediv__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: """ Get a MagicDotPathWithOp objects with a `/` operator and the correct types. @@ -575,7 +479,7 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_number_operator(other, "/") - def __mod__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: # type: ignore + def __mod__(self, other: _OTHER_TYPE) -> MagicDotPathWithOp: """ Get a MagicDotPathWithOp objects with a `%` operator and the correct types. @@ -592,28 +496,227 @@ class MagicDotPathWithOp(MagicDotPath): """ return self._get_number_operator(other, "%") + def jsonb_path(self, as_str: bool) -> str: + """ + Get the corresponding jsonb path from a MagicDotPath. + + Args : + - as_str : Boolean to force the request with json in str. + + Returns : + A path with the correct jsonb syntax + """ + raise NotImplementedError + + def col_name(self) -> str: + """ + Get the corresponding column name form a a MagicDotPath. + + Returns: + A column name with the correct format. + """ + raise NotImplementedError + + +@dataclass(eq=False) +class MagicDotPath(BaseMagicDotPath): + """ + Magical object that can have any attribute. + + Inspired by The very useful [DotMap module](https://github.com/drgrib/dotmap) this + object allow to write predicate in the lambda function used in SQL clause like + `.select()` or `.join()`. This is very useful to express SQL constraint. + + See the LINQ documentation for more in depth explanation. + + Attributes: + - connection: Connection to a db. Useful for safe. + - attributes: Attributes for build the jsonb path. + - with_table: Table on which we want to get paths. + - column: Column on which we will want to make the request. + + """ + + connection: Connection + attributes: list[str] = field(default_factory=list) + with_table: str | None = None + column: str | None = None + + def __getattr__(self, attribute_name: str) -> MagicDotPath: + """ + Get attribute in a list for a MagicDotPath objects. + + Args : + attribute_name : Names of all attributes. + + Returns : + A MagicDotPath objects with attributes names in attributes list. + """ + return MagicDotPath( + self.connection, + attributes=self.attributes + [f"'{attribute_name}'"], + with_table=self.with_table, + ) + + def safe(self, name: str) -> str: + """ + Secure a column or a table for a request. + + Args : + - name : Name of the column or table we want to secure. + + Returns : + Name but verified by psycopg.sql.Identifier + """ + return sql.Identifier(name).as_string(self.connection) + + def jsonb_path(self, as_str: bool) -> str: + """ + Get the corresponding jsonb path from a MagicDotPath. + + Args : + - as_str : Boolean to force the request with json in str. + + Returns : + A path with the correct jsonb syntax + """ + self.column = self.safe(self.attributes[0][1:-1]) + + res = "" + + if self.with_table: + res += f"{self.with_table}." + + if len(self.attributes) == 1: + res += self.column + else: + if as_str: + res += ( + f"{self.column}->" + f'{"->".join(self.attributes[1:-1])}->>{self.attributes[-1]}' + ) + else: + res += f'{self.column}->{"->".join(self.attributes[1:])}' + return res + + def col_name(self) -> str: + """ + Get the corresponding column name form a a MagicDotPath. + + Returns: + A column name with the correct format. + """ + result = [] + + if self.with_table: + result.append(self.with_table[1:-1]) + + for att in self.attributes: + result.append(att[1:-1]) + + return "_".join(result) + + +@dataclass(eq=False) +class MagicDotPathWithOp(BaseMagicDotPath): + """ + Magical object that can have any attribute and can be subject to many comparison. + + This class inherited from MagicDotPath. + + Inspired by The very useful [DotMap module](https://github.com/drgrib/dotmap) this + object allow to write predicate in the lambda function used in SQL clause like + `.select()` or `.join()`. This is very useful to express SQL constraint. + + See the LINQ documentation for more in depth explanation. + + Attributes: + - operand_1: First operand for the operation. + - operand_2: Second operand for the operation. + - operator: Operator for comparison. + - my_type: Type of other. + + """ + + operand_1: BaseMagicDotPath | _OTHER_TYPE + operator: str + operand_2: BaseMagicDotPath | _OTHER_TYPE + my_type: Type[Any] + def jsonb_path(self, as_str: bool) -> str: """ Get the corresponding jsonb path from a MagicDotPathWithOp. Args : - - as_str : boolean to force the request with json in str. + - as_str : Boolean to force the request with json in str. Returns : - a path with the correct jsonb syntax + A path with the correct jsonb syntax """ - if not self.operator: - return super().jsonb_path(as_str) + as_str = self.my_type == str - path = super().jsonb_path(as_str) + match self.operand_1: + case MagicDotPath() | MagicDotPathWithOp(): + path_op_1 = self.operand_1.jsonb_path(as_str) + case _: + path_op_1 = str(self.operand_1) - match self.other: - case (int() | float()): - return f"CAST({path} AS decimal) {self.operator} {self.other}" - case str(): - return f"{path} {self.operator} '{self.other}'" + match self.operand_2: + case MagicDotPath() | MagicDotPathWithOp(): + path_op_2 = self.operand_2.jsonb_path(as_str) + case _: + path_op_2 = str(self.operand_2) + + match self.my_type: + case type() if self.my_type == BaseMagicDotPath: + return ( + f"CAST({path_op_1} AS decimal) " + f"{self.operator} " + f"CAST({path_op_2} AS decimal)" + ) + case type() if self.my_type in (int, float): + return f"CAST({path_op_1} AS decimal) {self.operator} {path_op_2}" + case type() if self.my_type == str: + return f"{path_op_1} {self.operator} '{path_op_2}'" + + return f"{path_op_1} {self.operator} {path_op_2}" + + def col_name(self) -> str: + """ + Get the corresponding column name form a a MagicDotPath. + + Returns: + A column name with the correct format. + """ + match self.operand_1: + case BaseMagicDotPath(): + name_op_1 = self.operand_1.col_name() + case float(): + name_op_1 = clean_number_str(str(self.operand_1)) + case _: + name_op_1 = str(self.operand_1) + + match self.operand_2: + case BaseMagicDotPath(): + name_op_2 = self.operand_2.col_name() + case float() | int(): + name_op_2 = clean_number_str(str(self.operand_2)) + case _: + name_op_2 = str(self.operand_2) + + match self.operator: + case "+": + return f"{name_op_1}_add_{name_op_2}" + case "-": + return f"{name_op_1}_sub_{name_op_2}" + case "*": + return f"{name_op_1}_mul_{name_op_2}" + case "/": + return f"{name_op_1}_div_{name_op_2}" + case "%": + return f"{name_op_1}_mod_{name_op_2}" case _: - return f"{path} {self.operator} {self.other}" + return f"{name_op_1}_???_{name_op_2}" @dataclass @@ -663,8 +766,8 @@ class SQLEnumerableData: # or Tuple of MagicDotPath # or Dict of MagicDotPath LambdaMagicDotPath: TypeAlias = Callable[ - [MagicDotPath], - MagicDotPath | Tuple[MagicDotPath] | Dict[str, MagicDotPath], + [BaseMagicDotPath], + BaseMagicDotPath | Tuple[BaseMagicDotPath] | Dict[str, BaseMagicDotPath], ] LambdaMagicDotPathAggregate: TypeAlias = Callable[ diff --git a/py_linq_sql/utils/functions.py b/py_linq_sql/utils/functions.py index 1d523cb..b99aef9 100644 --- a/py_linq_sql/utils/functions.py +++ b/py_linq_sql/utils/functions.py @@ -19,6 +19,7 @@ from py_linq_sql.exception.exception import TableError # Local imports from .classes import ( AggregateType, + BaseMagicDotPath, CommandType, Connection, JoinType, @@ -26,7 +27,6 @@ from .classes import ( MagicDotPath, MagicDotPathAggregate, MagicDotPathWithOp, - PathsAndName, SQLEnumerableData, ) @@ -53,7 +53,7 @@ def _check_table(sqle: SQLEnumerableData, magic_dp: MagicDotPath) -> None: tables = [outer.table[1:-1], inner.table[1:-1]] - if magic_dp.attributes[0][1:-1] not in tables: + if magic_dp.with_table not in tables: raise TableError(magic_dp.attributes[0], tables) @@ -99,7 +99,12 @@ def get_aggregate( return " ".join(result) -def get_columns_name(paths: List[str]) -> List[PathsAndName]: +def get_columns_name( + mdps: MagicDotPath + | List[MagicDotPath] + | Tuple[MagicDotPath] + | Dict[str, MagicDotPath], +) -> List[str]: """ Get all column name. @@ -109,13 +114,17 @@ def get_columns_name(paths: List[str]) -> List[PathsAndName]: Returns : All of paths and columns. """ - result: List[PathsAndName] = [] + result = [] - for path in paths: - if "CAST" in path: - result.append(PathsAndName(path, _get_name_for_path_w_cast(path))) - else: - result.append(PathsAndName(path, _get_name_from_path(path))) + match mdps: + case MagicDotPath(): + result.append(mdps.col_name()) + case tuple() | list(): + for element in mdps: + result.append(element.col_name()) + case dict(): + for key in mdps: + result.append(key) return result @@ -163,47 +172,6 @@ def get_json(data: Dict[Any, Any]) -> str: return json.dumps(data) -def _get_name_from_path(path: str) -> str: - tmp = re.split(r""">|-|"|'|\.""", path) - - # Join all not None str in tmp with the function filter() - # (https://docs.python.org/3/library/functions.html#filter) - name = "_".join(filter(None, tmp)) - - return name - - -def _get_formated_suffix(suffix: str) -> str: - tmp = suffix.split(" ") - other = tmp[-1] - operator = tmp[-2] - result = [] - - match operator: - case "+": - result.append("plus") - case "-": - result.append("minus") - case "*": - result.append("mul") - case "/": - result.append("true_div") - case "%": - result.append("mod") - - result.append(other) - - return "_".join(result) - - -def _get_name_for_path_w_cast(path: str) -> str: - tmp = re.split(r"CAST|\(|\)", path) - suffix = _get_formated_suffix(tmp.pop()) - name_w_as = "".join(filter(None, tmp)) - name = _get_name_from_path(name_w_as.split("AS", maxsplit=1)[0]) - return f'"{name}{suffix}"' - - def get_one_aggregate( mdpa: MagicDotPathAggregate, cast_type: type, @@ -263,14 +231,14 @@ def get_one_concat_aggregate( def get_one_predicate_as_str( sqle: SQLEnumerableData, - mdpo: MagicDotPathWithOp, + mdpo: BaseMagicDotPath, ) -> str: """ Get one predicate as string with the correct cast type and the correct preffix. Args : - sqle : SQLEnumerable with connection, flags, list of commands and a table. - - mdpo: MagicDotPathWithOp to build the predicate. + - mdpo: BasemagicDotPath to build the predicate. Returns : A predicate as a string with the correct cast type and prefix. @@ -279,19 +247,48 @@ def get_one_predicate_as_str( - TypeError if arguments of get_path are not MagicDotPath or tuple of MagicDotPath """ - if isinstance(mdpo, MagicDotPathWithOp): - return get_path(mdpo, sqle)[0] + return get_path(mdpo, sqle)[0] + + +def _get_path_base_mdp( + magic_dp: BaseMagicDotPath, + sqle: SQLEnumerableData | None, + force: bool, + paths: List[str], +) -> None: - return " AND ".join([get_path(mdp, sqle) for mdp in mdpo]) + match magic_dp: + case MagicDotPath(): + if sqle and sqle.flags.join: + magic_dp.with_table = magic_dp.attributes[0][1:-1] + del magic_dp.attributes[0] + _check_table(sqle, magic_dp) + paths.append(magic_dp.jsonb_path(force)) + case MagicDotPathWithOp(): + type_error = True + if sqle and sqle.flags.join: + operand_1, operand_2 = magic_dp.operand_1, magic_dp.operand_2 + if isinstance(operand_1, MagicDotPath): + operand_1.with_table = operand_1.attributes[0][1:-1] + del operand_1.attributes[0] + _check_table(sqle, operand_1) + type_error = False + if isinstance(operand_2, MagicDotPath): + operand_2.with_table = operand_2.attributes[0][1:-1] + del operand_2.attributes[0] + _check_table(sqle, operand_2) + type_error = False + if type_error: + raise TypeError("Operand_1 or Operand_2 must be BaseMagicDotPath.") + paths.append(magic_dp.jsonb_path(force)) + case _: + raise TypeError( + "get_path take only BaseMagicDotPath or tuple of BaseMagicDotPath", + ) def get_path( - magic_dp: MagicDotPath - | Tuple[MagicDotPath] - | Dict[str, MagicDotPath] - | MagicDotPathWithOp - | Tuple[MagicDotPathWithOp] - | Dict[str, MagicDotPathWithOp], + magic_dp: BaseMagicDotPath | Tuple[BaseMagicDotPath] | Dict[str, BaseMagicDotPath], sqle: SQLEnumerableData | None = None, force: bool = False, ) -> List[str]: @@ -310,33 +307,20 @@ def get_path( Raises : - TypeError if magic_dp isn't a MagicDotPath or tuple of MagicDotPath """ - path = [] + paths: List[str] = [] match magic_dp: - case MagicDotPath(): - if sqle and sqle.flags.join: - _check_table(sqle, magic_dp) - magic_dp.with_table = magic_dp.attributes[0][1:-1] - del magic_dp.attributes[0] - path.append(magic_dp.jsonb_path(force)) + case BaseMagicDotPath(): + _get_path_base_mdp(magic_dp, sqle, force, paths) case tuple(): for element in magic_dp: - if not isinstance(element, MagicDotPath) and not isinstance( - element, - MagicDotPathWithOp, - ): - raise TypeError( - "get_path take only MagicDotPath or tuple of MagicDotPath", - ) - if sqle and sqle.flags.join: - _check_table(sqle, element) - element.with_table = element.attributes[0][1:-1] - del element.attributes[0] - path.append(element.jsonb_path(force)) + _get_path_base_mdp(element, sqle, force, paths) case _: - raise TypeError("get_path take only MagicDotPath or tuple of MagicDotPath") + raise TypeError( + "get_path take only BaseMagicDotPath or tuple of BaseMagicDotPath", + ) - return path + return paths def get_paths( @@ -364,6 +348,44 @@ def get_paths( return get_path(fquery(MagicDotPath(sqle.connection)), sqle, as_str) +def get_predicates_as_str( + result: List[str], + fquery: LambdaMagicDotPath, + sqle: SQLEnumerableData, +) -> None: + """ + Get all predicates as string with the correct cast type and the correct preffix. + + Args : + - result: List contains the request. + - fquery: Lambda function to get paths. + - sqle: SQLEnumerableData with connection, flags, list of commands and a table. + + Returns : + Predicates as a string with the correct cast type and prefix. + + Raises : + - TypeError if arguments of get_path are not MagicDotPath + or tuple of MagicDotPath, + or fquery produce other of BaseMagicDotPath or Tuple of BaseMagicDotPath + """ + mdp_w_path = fquery(MagicDotPath(sqle.connection)) + + match mdp_w_path: + case BaseMagicDotPath(): + result.append(get_one_predicate_as_str(sqle, mdp_w_path)) + case tuple(): + result.append( + " AND ".join( + [get_one_predicate_as_str(sqle, mdp) for mdp in mdp_w_path], + ), + ) + case _: + raise TypeError( + "You must put a MagicDotPath in lambda, see the documentation.", + ) + + def get_update_path(path: str) -> str: """ Get the correct format of path for UPDATE. @@ -436,8 +458,8 @@ def join_get_paths( inner_key: LambdaMagicDotPath, outer_key: LambdaMagicDotPath, result_function: Callable[ - [MagicDotPath, MagicDotPath], - MagicDotPath | Tuple[MagicDotPath] | Dict[str, MagicDotPath], + [BaseMagicDotPath, BaseMagicDotPath], + BaseMagicDotPath | Tuple[BaseMagicDotPath] | Dict[str, BaseMagicDotPath], ], ) -> DotMap: """ diff --git a/pyproject.toml b/pyproject.toml index fd3e434..3a08b70 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "py-linq-sql" -version = "0.87.0" +version = "0.88.0" description = "" authors = ["CHOSSON Ulysse <ulysse.chosson@obspm.fr>"] diff --git a/tests/exceptions/test_sql_commands.py b/tests/exceptions/test_sql_commands.py index 091e357..d1134b7 100644 --- a/tests/exceptions/test_sql_commands.py +++ b/tests/exceptions/test_sql_commands.py @@ -157,3 +157,10 @@ def test_exception_wrong_table_name_after_join( with pytest.raises(TableError): sqle.where(lambda x: x.toto.data.obj.name == "moon").execute() + + +def test_execution_lambda_without_mdp(db_connection_with_data, table_objects): + sqle = SQLEnumerable(db_connection_with_data, table_objects).select(lambda x: 1 + 1) + + with pytest.raises(TypeError): + sqle.get_command() diff --git a/tests/operators/test_sub.py b/tests/operators/test_sub.py index 1e6e275..57f5c71 100644 --- a/tests/operators/test_sub.py +++ b/tests/operators/test_sub.py @@ -23,6 +23,10 @@ from py_linq_sql.sql_enumerable.sql_enumerable import SQLEnumerable lambda x: (x.data.obj.name, x.data.obj.mass - 10), {"earth": -9, "saturn": 42, "jupiter": 90}, ), + ( + lambda x: (x.data.obj.name, x.data.obj.mass - 10 - 3), + {"earth": -12, "saturn": 39, "jupiter": 87}, + ), ( lambda x: (x.data.obj.name, x.data.obj.mass - (-10 - 3)), {"earth": 14, "saturn": 65, "jupiter": 113}, -- GitLab