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