diff --git a/Changelog.md b/Changelog.md index 42cd444..0daaa81 100644 --- a/Changelog.md +++ b/Changelog.md @@ -1,10 +1,81 @@ === -Dataloom **`2.1.1`** +Dataloom **`2.2.0`** === ### Release Notes - `dataloom` -We have release the new `dataloom` Version `2.1.0` (`2024-02-24`) +We have release the new `dataloom` Version `2.2.0` (`2024-02-24`) + +##### Features + +- updated documentation. +- Added operators `BETWEEN` and `NOT` in filters now ypu can use them. + +```py +post = loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + select=["id"], +) +post = loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=3, + ), + select=["id"], +) +``` + +> Note that the `between` operator works on value ranges that are numbers. + +- Distinct row selection has been added for the method `find_all()` and `find_many()` + +```py +post = loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + select=["completed"], + distinct=True, +) + +post = loom.find_all( + Post, + select=["completed"], + distinct=True, +) +``` + +> The result will return the `distinct` rows of data based on the completed value. + +- added utility functions `sum`, `avg`, `min`, `max` and `count` to the loom object. + ```py + count = loom.count( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + ) + ``` +- Updated logger colors and formatting. + +# Dataloom **`2.1.1`** + +### Release Notes - `dataloom` + +We have release the new `dataloom` Version `2.1.1` (`2024-02-24`) ##### Features @@ -70,7 +141,7 @@ We have release the new `dataloom` Version `2.0.0` (`2024-02-21`) - Now you can fetch your child relationship together in your query ```py - user = mysql_loom.find_one( + user = loom.find_one( instance=User, filters=[Filter(column="id", value=userId)], include=[Include(model=Profile, select=["id", "avatar"], has="one")], @@ -81,7 +152,7 @@ We have release the new `dataloom` Version `2.0.0` (`2024-02-21`) - You can apply limits, offsets, filters and orders to your child associations during queries ```py - post = mysql_loom.find_one( + post = loom.find_one( instance=Post, filters=[Filter(column="userId", value=userId)], select=["title", "id"], @@ -124,7 +195,7 @@ We have release the new `dataloom` Version `2.0.0` (`2024-02-21`) # now you can do this - profile = mysql_loom.find_many( + profile = loom.find_many( instance=Profile, ) print([Profile(**p) for p in profile]) # ? = [] @@ -140,20 +211,20 @@ We have release the new `dataloom` Version `2.0.0` (`2024-02-21`) - **Before** ```py - res = mysql_loom.find_by_pk(Profile, pk=profileId, select={"id", "avatar"}) # invalid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select=("id", "avatar")) # invalid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select="id") # invalid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select=["id"]) # valid + res = loom.find_by_pk(Profile, pk=profileId, select={"id", "avatar"}) # invalid + res = loom.find_by_pk(Profile, pk=profileId, select=("id", "avatar")) # invalid + res = loom.find_by_pk(Profile, pk=profileId, select="id") # invalid + res = loom.find_by_pk(Profile, pk=profileId, select=["id"]) # valid ``` - **Now** ```py - res = mysql_loom.find_by_pk(Profile, pk=profileId, select={"id", "avatar"}) # valid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select=("id", "avatar")) # valid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select="id") # valid - res = mysql_loom.find_by_pk(Profile, pk=profileId, select=["id"]) # valid + res = loom.find_by_pk(Profile, pk=profileId, select={"id", "avatar"}) # valid + res = loom.find_by_pk(Profile, pk=profileId, select=("id", "avatar")) # valid + res = loom.find_by_pk(Profile, pk=profileId, select="id") # valid + res = loom.find_by_pk(Profile, pk=profileId, select=["id"]) # valid ``` diff --git a/README.md b/README.md index c320eac..65a897c 100644 --- a/README.md +++ b/README.md @@ -80,13 +80,18 @@ - [Guidelines for Safe Usage](#guidelines-for-safe-usage) - [Ordering](#ordering) - [Filters](#filters) - - [Operators](#operators) + - [Operators](#operators) - [Data Aggregation](#data-aggregation) - [Aggregation Functions](#aggregation-functions) - [Utilities](#utilities) - [1. `inspect`](#1-inspect) - [2. `decorators`](#2-decorators) - [`@initialize`](#initialize) + - [3. `count()`](#3-count) + - [4. `min()`](#4-min) + - [5. `max()`](#5-max) + - [6. `avg()`](#6-avg) + - [7. `sum()`](#7-sum) - [Associations](#associations) - [1. `1-1` Association](#1-1-1-association) - [Inserting](#inserting) @@ -793,6 +798,7 @@ The `find_all()` method takes in the following arguments: | `order` | Collection of columns to order the documents by. | `list[Order]` | `None` | `No` | | `include` | Collection or a `Include` of related models to eagerly load. | `list[Include]\|Include` | `None` | `No` | | `group` | Collection of `Group` which specifies how you want your data to be grouped during queries. | `list[Group]\|Group` | `None` | `No` | +| `distinct` | Boolean telling dataloom to return distinct row values based on selected fields or not. | `bool` | `False` | `No` | > 👍 **Pro Tip**: A collection can be any python iterable, the supported iterables are `list`, `set`, `tuple`. @@ -824,6 +830,7 @@ The `find_many()` method takes in the following arguments: | `include` | Collection or a `Include` of related models to eagerly load. | `list[Include]\|Include` | `None` | `No` | | `group` | Collection of `Group` which specifies how you want your data to be grouped during queries. | `list[Group]\|Group` | `None` | `No` | | `filters` | Collection of `Filter` or a `Filter` to apply to the query. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to return distinct row values based on selected fields or not. | `bool` | `False` | `No` | > 👍 **Pro Tip**: The distinction between the `find_all()` and `find_many()` methods lies in the fact that `find_many()` enables you to apply specific filters, whereas `find_all()` retrieves all the documents within the specified model. @@ -1115,17 +1122,19 @@ res2 = mysql_loom.delete_one( You can use the `operator` to match the values. Here is the table of description for these filters. -| Operator | Explanation | Expect | -| --------- | ------------------------------------------------------------------------------------------------------------ | --------------------- | -| `'eq'` | Indicates equality. It checks if the value is equal to the specified criteria. | Value == Criteria | -| `'lt'` | Denotes less than. It checks if the value is less than the specified criteria. | Value < Criteria | -| `'gt'` | Denotes greater than. It checks if the value is greater than the specified criteria. | Value > Criteria | -| `'leq'` | Denotes less than or equal to. It checks if the value is less than or equal to the specified criteria. | Value <= Criteria | -| `'geq'` | Denotes greater than or equal to. It checks if the value is greater than or equal to the specified criteria. | Value >= Criteria | -| `'in'` | Checks if the value is included in a specified list of values. | Value in List | -| `'notIn'` | Checks if the value is not included in a specified list of values. | Value not in List | -| `'like'` | Performs a pattern matching operation. It checks if the value is similar to a specified pattern. | Value matches Pattern | -| `'neq'` | Indicates non-equality. It checks if the value is not equal to the specified criteria. | Value != Criteria | +| Operator | Explanation | Expect | +| ----------- | ------------------------------------------------------------------------------------------------------------ | --------------------- | +| `'eq'` | Indicates equality. It checks if the value is equal to the specified criteria. | Value == Criteria | +| `'lt'` | Denotes less than. It checks if the value is less than the specified criteria. | Value < Criteria | +| `'gt'` | Denotes greater than. It checks if the value is greater than the specified criteria. | Value > Criteria | +| `'leq'` | Denotes less than or equal to. It checks if the value is less than or equal to the specified criteria. | Value <= Criteria | +| `'geq'` | Denotes greater than or equal to. It checks if the value is greater than or equal to the specified criteria. | Value >= Criteria | +| `'in'` | Checks if the value is included in a specified list of values. | Value in List | +| `'notIn'` | Checks if the value is not included in a specified list of values. | Value not in List | +| `'like'` | Performs a pattern matching operation. It checks if the value is similar to a specified pattern. | Value matches Pattern | +| `'not'` | Indicates non-equality. It checks if the column value that does not equal to the specified criteria. | NOT id = Criteria | +| `'neq'` | Indicates non-equality. It checks if the value is not equal to the specified criteria. | Value != Criteria | +| `'between'` | It checks range values that matches a given range between the minimum and maximum. | id BETWEEN (min, max) | Let's talk about these filters in detail of code by example. Let's say you want to update a `Post` where the `id` matches `1` you can do it as follows: @@ -1458,6 +1467,166 @@ print(profile) # ? = print(profile.avatar) # ? hello.jpg ``` +#### 3. `count()` + +This is a utility function that comes within the `loom` object that is used to count rows in a database table that meets a specific criteria. Here is an example on how to use this utility function. + +```py +# example +count = mysql_loom.count( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, +) +print(count) +``` + +The `count` function takes the following arguments: + +| Argument | Description | Type | Default | Required | +| ---------- | ------------------------------------------------------------------------------------------ | ------------------------ | ------- | -------- | +| `instance` | The model class to retrieve documents from. | `Model` | `None` | `Yes` | +| `column` | A string of column to count values based on. | `str` | `None` | `Yes` | +| `limit` | Maximum number of documents to retrieve. | `int` | `None` | `No` | +| `offset` | Number of documents to skip before counting. | `int` | `0` | `No` | +| `filters` | Collection of `Filter` or a `Filter` to apply to the rows to be counted. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to count distinct rows of values based on selected column or not. | `bool` | `False` | `No` | + +#### 4. `min()` + +This is a utility function that comes within the `loom` object that is used to find the minimum value in rows of data in a database table that meets a specific criteria. Here is an example on how to use this utility function. + +```py +# example +_min = mysql_loom.min( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, +) +print(_min) +``` + +The `min` function takes the following arguments: + +| Argument | Description | Type | Default | Required | +| ---------- | ------------------------------------------------------------------------------------------------------- | ------------------------ | ------- | -------- | +| `instance` | The model class to retrieve documents from. | `Model` | `None` | `Yes` | +| `column` | A string of column to find minimum values based on. | `str` | `None` | `Yes` | +| `limit` | Maximum number of documents to retrieve. | `int` | `None` | `No` | +| `offset` | Number of documents to skip before finding the minimum. | `int` | `0` | `No` | +| `filters` | Collection of `Filter` or a `Filter` to apply to the rows to be used. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to find minimum value in distinct rows values based on selected column or not. | `bool` | `False` | `No` | + +#### 5. `max()` + +This is a utility function that comes within the `loom` object that is used to find the maximum value in rows of data in a database table that meets a specific criteria. Here is an example on how to use this utility function. + +```py +# example +_max = mysql_loom.max( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, +) +print(_max) +``` + +The `max` function takes the following arguments: + +| Argument | Description | Type | Default | Required | +| ---------- | ---------------------------------------------------------------------------------------------------------- | ------------------------ | ------- | -------- | +| `instance` | The model class to retrieve documents from. | `Model` | `None` | `Yes` | +| `column` | A string of column to find maximum values based on. | `str` | `None` | `Yes` | +| `limit` | Maximum number of documents to retrieve. | `int` | `None` | `No` | +| `offset` | Number of documents to skip before finding the maximum. | `int` | `0` | `No` | +| `filters` | Collection of `Filter` or a `Filter` to apply to the rows to be used. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to find maximum value in distinct rows of values based on selected column or not. | `bool` | `False` | `No` | + +#### 6. `avg()` + +This is a utility function that comes within the `loom` object that is used to calculate the average value in rows of data in a database table that meets a specific criteria. Here is an example on how to use this utility function. + +```py +# example +_avg = mysql_loom.avg( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, +) +print(_avg) +``` + +The `max` function takes the following arguments: + +| Argument | Description | Type | Default | Required | +| ---------- | ------------------------------------------------------------------------------------------------------------------- | ------------------------ | ------- | -------- | +| `instance` | The model class to retrieve documents from. | `Model` | `None` | `Yes` | +| `column` | A string of column to calculate average values based on. | `str` | `None` | `Yes` | +| `limit` | Maximum number of documents to retrieve. | `int` | `None` | `No` | +| `offset` | Number of documents to skip before finding the calculating the average. | `int` | `0` | `No` | +| `filters` | Collection of `Filter` or a `Filter` to apply to the rows to be used. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to calculate the average value in distinct rows of values based on selected column or not. | `bool` | `False` | `No` | + +#### 7. `sum()` + +This is a utility function that comes within the `loom` object that is used to find the total sum in rows of data in a database table that meets a specific criteria. Here is an example on how to use this utility function. + +```py +# example +_sum = mysql_loom.sum( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, +) +print(_sum) +``` + +The `sum` function takes the following arguments: + +| Argument | Description | Type | Default | Required | +| ---------- | ---------------------------------------------------------------------------------------------- | ------------------------ | ------- | -------- | +| `instance` | The model class to retrieve documents from. | `Model` | `None` | `Yes` | +| `column` | A string of column to sum values based on. | `str` | `None` | `Yes` | +| `limit` | Maximum number of documents to retrieve. | `int` | `None` | `No` | +| `offset` | Number of documents to skip before summing. | `int` | `0` | `No` | +| `filters` | Collection of `Filter` or a `Filter` to apply to the rows to be used. | `list[Filter] \| Filter` | `None` | `No` | +| `distinct` | Boolean telling dataloom to sum value in distinct rows values based on selected column or not. | `bool` | `False` | `No` | + ### Associations In dataloom you can create association using the `foreign-keys` column during model creation. You just have to specify a single model to have a relationship with another model using the [`ForeignKeyColum`](#foreignkeycolumn-class). Just by doing that dataloom will be able to learn bidirectional relationship between your models. Let's have a look at the following examples: @@ -1946,7 +2115,7 @@ print(user_post) """ ? = ### What is coming next? 1. N-N associations -2. utilities functions +2. Query Builder ### Contributing diff --git a/dataloom/loom/__init__.py b/dataloom/loom/__init__.py index 621d609..9bb32ca 100644 --- a/dataloom/loom/__init__.py +++ b/dataloom/loom/__init__.py @@ -26,6 +26,7 @@ Group, ) from dataloom.loom.interfaces import ILoom +from dataloom.loom.math import math class Loom(ILoom): @@ -243,6 +244,7 @@ def find_many( offset: Optional[int] = None, order: Optional[list[Order] | Order] = [], group: Optional[list[Group] | Group] = [], + distinct: bool = False, ) -> list: """ find_many @@ -268,6 +270,8 @@ def find_many( The order in which to retrieve rows. Default is an empty list. group : list[Group] | Group, optional The grouping of the retrieved rows. Default is an empty list. + distinct: + Weather to return the distinct row values for the selected columns or not. Returns ------- @@ -313,6 +317,7 @@ def find_many( filters=filters, order=order, group=group, + distinct=distinct, ) def find_all( @@ -324,6 +329,7 @@ def find_all( offset: Optional[int] = None, order: Optional[list[Order] | Order] = [], group: Optional[list[Group] | Group] = [], + distinct: bool = False, ) -> list: """ find_all @@ -345,6 +351,8 @@ def find_all( The offset of the rows to retrieve, useful for pagination. Default is None. order : list[Order] | None, optional The order in which to retrieve rows. Default is an empty list. + distinct: + Weather to return the distinct row values for the selected columns or not. Returns ------- @@ -390,6 +398,7 @@ def find_all( offset=offset, order=order, group=group, + distinct=distinct, ) def find_by_pk( @@ -1330,7 +1339,7 @@ def connect_and_sync( ... id = PrimaryKeyColumn(type="int", auto_increment=True, nullable=False, unique=True) ... type = Column(type="varchar", length=255, nullable=False) ... # connecting and syncing tables - ... conn, tables = mysql_loom.connect_and_sync( + ... conn, tables = loom.connect_and_sync( ... [Category], drop=True, force=True ... ) ... # closing the connection @@ -1449,3 +1458,330 @@ def sync( return self.tables except Exception as e: raise Exception(e) + + def sum( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + """ + sum + --- + + Computes the sum of values in the specified column of a table. + + Parameters + ---------- + instance : Model + An instance of a Model class representing the table from which the sum will be computed. + column : str + The name of the column for which the sum will be calculated. + limit : int | None, optional + The maximum number of rows to consider for sum calculation. If None, all rows are considered. (default is None) + offset : int | None, optional + The number of rows to skip before starting to count for sum calculation. (default is None) + distinct : bool, optional + If True, only distinct values in the column will be summed. (default is False) + filters : Filter | list[Filter] | None, optional + Filters to apply to the rows before calculating the sum. It can be a single Filter object, a list of Filter objects, or None. (default is None) + + Returns + ------- + sum_value : float | int + The sum of values in the specified column. The return type depends on the type of the column. + + See Also + -------- + avg : Computes the average of values in a column. + min : Finds the minimum value in a column. + max : Finds the maximum value in a column. + count : Counts the number of rows in a table or satisfying certain conditions. + + Examples + -------- + >>> # Compute the sum of 'id' column in 'Post' table where id is between 1 and 7 + ... res = loom.sum( + ... instance=Post, + ... filters=Filter( + ... column="id", + ... operator="between", + ... value=[1, 7], + ... ), + ... column="id", + ... distinct=False, + ... limit=2, + ... offset=0, + ... ) + ... + ... print(res) + + """ + return math(dialect=self.dialect, _execute_sql=self._execute_sql).sum( + instance=instance, + column=column, + limit=limit, + offset=offset, + distinct=distinct, + filters=filters, + ) + + def avg( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float: + """ + avg + --- + + Computes the average of values in the specified column of a table. + + Parameters + ---------- + instance : Model + An instance of a Model class representing the table from which the average will be computed. + column : str + The name of the column for which the average will be calculated. + limit : int | None, optional + The maximum number of rows to consider for average calculation. If None, all rows are considered. (default is None) + offset : int | None, optional + The number of rows to skip before starting to count for average calculation. (default is None) + distinct : bool, optional + If True, only distinct values in the column will be averaged. (default is False) + filters : Filter | list[Filter] | None, optional + Filters to apply to the rows before calculating the average. It can be a single Filter object, a list of Filter objects, or None. (default is None) + + Returns + ------- + avg_value : float + The average of values in the specified column. + + See Also + -------- + sum : Computes the sum of values in a column. + min : Finds the minimum value in a column. + max : Finds the maximum value in a column. + count : Counts the number of rows in a table or satisfying certain conditions. + + Examples + -------- + >>> # Compute the average of 'score' column in 'Student' table + ... average_score = loom.avg( + ... instance=Student, + ... column="score", + ... distinct=True, + ... limit=100, + ... offset=10, + ... ) + ... + ... print(average_score) + + """ + return math(dialect=self.dialect, _execute_sql=self._execute_sql).avg( + instance=instance, + column=column, + limit=limit, + offset=offset, + distinct=distinct, + filters=filters, + ) + + def min( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + """ + min + --- + + Finds the minimum value in the specified column of a table. + + Parameters + ---------- + instance : Model + An instance of a Model class representing the table from which the minimum value will be found. + column : str + The name of the column in which the minimum value will be searched. + limit : int | None, optional + The maximum number of rows to consider for finding the minimum value. If None, all rows are considered. (default is None) + offset : int | None, optional + The number of rows to skip before starting to search for the minimum value. (default is None) + distinct : bool, optional + If True, only distinct values in the column will be considered. (default is False) + filters : Filter | list[Filter] | None, optional + Filters to apply to the rows before searching for the minimum value. It can be a single Filter object, a list of Filter objects, or None. (default is None) + + Returns + ------- + min_value : float | int + The minimum value found in the specified column. + + See Also + -------- + sum : Computes the sum of values in a column. + avg : Computes the average of values in a column. + max : Finds the maximum value in a column. + count : Counts the number of rows in a table or satisfying certain conditions. + + Examples + -------- + >>> # Find the minimum value in the 'price' column of 'Product' table + ... min_price = loom.min( + ... instance=Product, + ... column="price", + ... distinct=True, + ... limit=1000, + ... offset=0, + ... ) + ... + ... print(min_price) + + """ + return math(dialect=self.dialect, _execute_sql=self._execute_sql).min( + instance=instance, + column=column, + limit=limit, + offset=offset, + distinct=distinct, + filters=filters, + ) + + def max( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + """ + max + --- + + Finds the maximum value in the specified column of a table. + + Parameters + ---------- + instance : Model + An instance of a Model class representing the table from which the maximum value will be found. + column : str + The name of the column in which the maximum value will be searched. + limit : int | None, optional + The maximum number of rows to consider for finding the maximum value. If None, all rows are considered. (default is None) + offset : int | None, optional + The number of rows to skip before starting to search for the maximum value. (default is None) + distinct : bool, optional + If True, only distinct values in the column will be considered. (default is False) + filters : Filter | list[Filter] | None, optional + Filters to apply to the rows before searching for the maximum value. It can be a single Filter object, a list of Filter objects, or None. (default is None) + + Returns + ------- + max_value : float | int + The maximum value found in the specified column. + + See Also + -------- + sum : Computes the sum of values in a column. + avg : Computes the average of values in a column. + min : Finds the minimum value in a column. + count : Counts the number of rows in a table or satisfying certain conditions. + + Examples + -------- + >>> # Find the maximum value in the 'price' column of 'Product' table + ... max_price = loom.max( + ... instance=Product, + ... column="price", + ... distinct=True, + ... limit=1000, + ... offset=0, + ... ) + ... + ... print(max_price) + + """ + return math(dialect=self.dialect, _execute_sql=self._execute_sql).max( + instance=instance, + column=column, + limit=limit, + offset=offset, + distinct=distinct, + filters=filters, + ) + + def count( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int: + """ + count + ----- + + Counts the number of rows in a table or satisfying certain conditions. + + Parameters + ---------- + instance : Model + An instance of a Model class representing the table for which the row count will be computed. + column : str + The name of the column to count rows from. It can be any column, or "*" to count all rows. + limit : int | None, optional + The maximum number of rows to consider for counting. If None, all rows are considered. (default is None) + offset : int | None, optional + The number of rows to skip before starting to count rows. (default is None) + distinct : bool, optional + If True, only distinct values in the column will be counted. (default is False) + filters : Filter | list[Filter] | None, optional + Filters to apply to the rows before counting. It can be a single Filter object, a list of Filter objects, or None. (default is None) + + Returns + ------- + row_count : int + The number of rows that match the specified conditions. + + See Also + -------- + sum : Computes the sum of values in a column. + avg : Computes the average of values in a column. + min : Finds the minimum value in a column. + max : Finds the maximum value in a column. + + Examples + -------- + >>> # Count the total number of users based on the specified column + ... total_users = loom.count( + ... instance=User, + ... column="id", + ... ) + ... + ... print(total_users) + + """ + return math(dialect=self.dialect, _execute_sql=self._execute_sql).count( + instance=instance, + column=column, + limit=limit, + offset=offset, + distinct=distinct, + filters=filters, + ) diff --git a/dataloom/loom/interfaces.py b/dataloom/loom/interfaces.py index 911168f..7cacd4a 100644 --- a/dataloom/loom/interfaces.py +++ b/dataloom/loom/interfaces.py @@ -8,6 +8,64 @@ class ILoom(ABC): + @abstractclassmethod + def sum( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> list: + raise NotImplementedError("The sum method was not implemented.") + + @abstractclassmethod + def max( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> list: + raise NotImplementedError("The max method was not implemented.") + + @abstractclassmethod + def min( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> list: + raise NotImplementedError("The min method was not implemented.") + + def count( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> list: + raise NotImplementedError("The count method was not implemented.") + + def avg( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> list: + raise NotImplementedError("The count method was not implemented.") + @abstractclassmethod def increment( self, diff --git a/dataloom/loom/math.py b/dataloom/loom/math.py new file mode 100644 index 0000000..878f07b --- /dev/null +++ b/dataloom/loom/math.py @@ -0,0 +1,183 @@ +from dataloom.model import Model +from dataloom.types import Filter, DIALECT_LITERAL +from typing import Callable, Any, Optional +from dataloom.utils import get_args +from abc import ABC, abstractclassmethod + + +class Math(ABC): + @abstractclassmethod + def sum( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int | float: + raise NotImplementedError("The sum method was not implemented.") + + @abstractclassmethod + def max( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int | float: + raise NotImplementedError("The max method was not implemented.") + + @abstractclassmethod + def min( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int | float: + raise NotImplementedError("The min method was not implemented.") + + def count( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int: + raise NotImplementedError("The count method was not implemented.") + + def avg( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int | float: + raise NotImplementedError("The count method was not implemented.") + + +class math(Math): + def __init__( + self, dialect: DIALECT_LITERAL, _execute_sql: Callable[..., Any] + ) -> None: + self._execute_sql = _execute_sql + self.dialect = dialect + + def sum( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + sql, params, _, _ = instance._get_select_where_stm( + dialect=self.dialect, + filters=filters, + select=column, + offset=offset, + distinct=distinct, + limit=limit, + function="sum", + ) + args = list(get_args(params)) + row = self._execute_sql(sql, fetchone=True, args=args) + return 0 if row is None else row[0] + + def count( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> int: + sql, params, _, _ = instance._get_select_where_stm( + dialect=self.dialect, + filters=filters, + select=column, + offset=offset, + distinct=distinct, + limit=limit, + function="count", + ) + args = list(get_args(params)) + row = self._execute_sql(sql, fetchone=True, args=args) + return 0 if row is None else row[0] + + def avg( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float: + sql, params, _, _ = instance._get_select_where_stm( + dialect=self.dialect, + filters=filters, + select=column, + offset=offset, + distinct=distinct, + limit=limit, + function="avg", + ) + args = list(get_args(params)) + row = self._execute_sql(sql, fetchone=True, args=args) + return 0.0 if row is None else row[0] + + def max( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + sql, params, _, _ = instance._get_select_where_stm( + dialect=self.dialect, + filters=filters, + select=column, + offset=offset, + distinct=distinct, + limit=limit, + function="max", + ) + args = list(get_args(params)) + row = self._execute_sql(sql, fetchone=True, args=args) + return 0 if row is None else row[0] + + def min( + self, + instance: Model, + column: str, + limit: Optional[int] = None, + offset: Optional[int] = None, + distinct: bool = False, + filters: Optional[Filter | list[Filter]] = None, + ) -> float | int: + sql, params, _, _ = instance._get_select_where_stm( + dialect=self.dialect, + filters=filters, + select=column, + offset=offset, + distinct=distinct, + limit=limit, + function="min", + ) + args = list(get_args(params)) + row = self._execute_sql(sql, fetchone=True, args=args) + return 0 if row is None else row[0] diff --git a/dataloom/loom/query.py b/dataloom/loom/query.py index 3a8caf4..2e084ea 100644 --- a/dataloom/loom/query.py +++ b/dataloom/loom/query.py @@ -71,6 +71,7 @@ def find_many( offset: Optional[int] = None, order: Optional[list[Order] | Order] = [], group: Optional[list[Group] | Group] = [], + distinct: bool = False, ) -> list: data = [] @@ -88,6 +89,7 @@ def find_many( offset=offset, order=order, group=group, + distinct=distinct, ) args = list(get_args(params)) + having_values rows = self._execute_sql(sql, fetchall=True, args=args) @@ -119,6 +121,7 @@ def find_all( offset: Optional[int] = None, order: Optional[list[Order] | Order] = [], group: Optional[list[Group] | Group] = [], + distinct: bool = False, ) -> list: data = [] if not is_collection(include): @@ -134,6 +137,7 @@ def find_all( offset=offset, order=order, group=group, + distinct=distinct, ) args = list(get_args(params)) + having_values rows = self._execute_sql(sql, fetchall=True, args=args) @@ -188,7 +192,6 @@ def find_one( select=select, offset=offset, ) - args = get_args(params) row = self._execute_sql(sql, args=args, fetchone=True) if row is None: diff --git a/dataloom/model/__init__.py b/dataloom/model/__init__.py index ae02f0a..90000fb 100644 --- a/dataloom/model/__init__.py +++ b/dataloom/model/__init__.py @@ -16,6 +16,7 @@ Order, Include, Group, + UTILS_FUNCTION_LITERAL, ) from dataloom.utils import ( get_table_filters, @@ -158,12 +159,13 @@ def _get_select_where_stm( offset: Optional[int] = None, order: Optional[list[Order] | Order] = [], group: Optional[list[Group] | Group] = [], + distinct: bool = False, + function: Optional[UTILS_FUNCTION_LITERAL] = None, ): if not is_collection(select): select = [select] orders = [] # what are the foreign keys? - fields, pk_name, fks, updatedAtColumName = get_table_fields( cls, dialect=dialect ) @@ -184,6 +186,10 @@ def _get_select_where_stm( f'The table "{cls._get_table_name()}" does not have a column "{column}".' ) + if function is not None: + select = [ + f'{"DISTINCT " if distinct else ''}{f'"{column}"' if dialect == 'postgres' else f"`{column}`"}' + ] placeholder_filters, placeholder_filter_values = get_table_filters( table_name=cls._get_table_name(), dialect=dialect, @@ -216,6 +222,8 @@ def _get_select_where_stm( pk_name=pk_name, groups=(group_columns, group_fns), having=having_columns, + distinct=distinct, + function=function, ) else: sql = GetStatement( @@ -228,6 +236,8 @@ def _get_select_where_stm( orders=orders, groups=(group_columns, group_fns), having=having_columns, + distinct=distinct, + function=function, ) else: raise UnsupportedDialectException( diff --git a/dataloom/statements/__init__.py b/dataloom/statements/__init__.py index cc1614d..a2a6be8 100644 --- a/dataloom/statements/__init__.py +++ b/dataloom/statements/__init__.py @@ -12,7 +12,11 @@ PgStatements, Sqlite3Statements, ) -from dataloom.types import DIALECT_LITERAL, INCREMENT_DECREMENT_LITERAL +from dataloom.types import ( + DIALECT_LITERAL, + INCREMENT_DECREMENT_LITERAL, + UTILS_FUNCTION_LITERAL, +) from dataloom.utils import ( get_formatted_query, get_relationships, @@ -213,6 +217,8 @@ def _get_select_where_command( orders: Optional[list[str]] = [], groups: list[tuple[str]] = [], having: list[str] = [], + distinct: bool = False, + function: Optional[UTILS_FUNCTION_LITERAL] = None, ): (group_columns, group_fns) = groups options = [ @@ -223,26 +229,62 @@ def _get_select_where_command( "" if offset is None else f"OFFSET { offset}", ] if self.dialect == "postgres": - sql = PgStatements.SELECT_WHERE_COMMAND.format( - column_names=", ".join([f'"{f}"' for f in fields] + group_fns), - table_name=f'"{self.table_name}"', - filters=" ".join(placeholder_filters), - options=" ".join(options), - ) + if function is None: + sql = PgStatements.SELECT_WHERE_COMMAND.format( + column_names=", ".join([f'"{f}"' for f in fields] + group_fns), + table_name=f'"{self.table_name}"', + filters=" ".join(placeholder_filters), + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = PgStatements.SELECT_WHERE_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f'"{self.table_name}"', + filters=" ".join(placeholder_filters), + options=" ".join(options), + ) elif self.dialect == "mysql": - sql = MySqlStatements.SELECT_WHERE_COMMAND.format( - column_names=", ".join([f"`{name}`" for name in fields] + group_fns), - table_name=f"`{self.table_name}`", - filters=" ".join(placeholder_filters), - options=" ".join(options), - ) + if function is None: + sql = MySqlStatements.SELECT_WHERE_COMMAND.format( + column_names=", ".join( + [f"`{name}`" for name in fields] + group_fns + ), + table_name=f"`{self.table_name}`", + filters=" ".join(placeholder_filters), + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = MySqlStatements.SELECT_WHERE_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f"`{self.table_name}`", + filters=" ".join(placeholder_filters), + options=" ".join(options), + ) + elif self.dialect == "sqlite": - sql = Sqlite3Statements.SELECT_WHERE_COMMAND.format( - column_names=", ".join([f"`{name}`" for name in fields] + group_fns), - table_name=f"`{self.table_name}`", - filters=" ".join(placeholder_filters), - options=" ".join(options), - ) + if function is None: + sql = Sqlite3Statements.SELECT_WHERE_COMMAND.format( + column_names=", ".join( + [f"`{name}`" for name in fields] + group_fns + ), + table_name=f"`{self.table_name}`", + filters=" ".join(placeholder_filters), + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = Sqlite3Statements.SELECT_WHERE_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f"`{self.table_name}`", + filters=" ".join(placeholder_filters), + options=" ".join(options), + ) + else: raise UnsupportedDialectException( "The dialect passed is not supported the supported dialects are: {'postgres', 'mysql', 'sqlite'}" @@ -258,6 +300,8 @@ def _get_select_command( orders: Optional[list[str]] = [], groups: list[tuple[str]] = [], having: list[str] = [], + distinct: bool = False, + function: Optional[UTILS_FUNCTION_LITERAL] = None, ): (group_columns, group_fns) = groups options = [ @@ -269,23 +313,57 @@ def _get_select_command( ] if self.dialect == "postgres": - sql = PgStatements.SELECT_COMMAND.format( - column_names=", ".join([f'"{name}"' for name in fields] + group_fns), - table_name=f'"{self.table_name}"', - options=" ".join(options), - ) + if function is None: + sql = PgStatements.SELECT_COMMAND.format( + column_names=", ".join( + [f'"{name}"' for name in fields] + group_fns + ), + table_name=f'"{self.table_name}"', + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = PgStatements.SELECT_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f'"{self.table_name}"', + options=" ".join(options), + ) elif self.dialect == "mysql": - sql = MySqlStatements.SELECT_COMMAND.format( - column_names=", ".join([f"`{name}`" for name in fields] + group_fns), - table_name=f"`{self.table_name}`", - options=" ".join(options), - ) + if function is None: + sql = MySqlStatements.SELECT_COMMAND.format( + column_names=", ".join(fields) + if function is not None + else ", ".join([f"`{name}`" for name in fields] + group_fns), + table_name=f"`{self.table_name}`", + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = MySqlStatements.SELECT_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f"`{self.table_name}`", + options=" ".join(options), + ) + elif self.dialect == "sqlite": - sql = Sqlite3Statements.SELECT_COMMAND.format( - column_names=", ".join([f"`{name}`" for name in fields] + group_fns), - table_name=f"`{self.table_name}`", - options=" ".join(options), - ) + if function is None: + sql = Sqlite3Statements.SELECT_COMMAND.format( + column_names=", ".join( + [f"`{name}`" for name in fields] + group_fns + ), + table_name=f"`{self.table_name}`", + options=" ".join(options), + distinct="DISTINCT" if distinct else "", + ) + else: + sql = Sqlite3Statements.SELECT_FN_COMMAND.format( + fn=function.upper(), + column_names=", ".join(fields), + table_name=f"`{self.table_name}`", + options=" ".join(options), + ) else: raise UnsupportedDialectException( "The dialect passed is not supported the supported dialects are: {'postgres', 'mysql', 'sqlite'}" diff --git a/dataloom/statements/statements.py b/dataloom/statements/statements.py index 854a1a0..8d7f942 100644 --- a/dataloom/statements/statements.py +++ b/dataloom/statements/statements.py @@ -87,11 +87,26 @@ class MySqlStatements: ) # selecting data - SELECT_COMMAND = "SELECT {column_names} FROM {table_name} {options};".strip() - SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" - SELECT_WHERE_COMMAND = ( - "SELECT {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_COMMAND = ( + "SELECT {distinct} {column_names} FROM {table_name} {options};".strip() ) + SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" + SELECT_WHERE_COMMAND = "SELECT {distinct} {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_WHERE_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + WHERE {filters} + {options} + ) AS subquery; + """ + SELECT_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + {options} + ) AS subquery; + """ # ------------- child parent bidirectional sub queries SELECT_CHILD_BY_PK = """ @@ -214,11 +229,26 @@ class Sqlite3Statements: ) # selecting data - SELECT_COMMAND = "SELECT {column_names} FROM {table_name} {options};".strip() - SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" - SELECT_WHERE_COMMAND = ( - "SELECT {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_COMMAND = ( + "SELECT {distinct} {column_names} FROM {table_name} {options};".strip() ) + SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" + SELECT_WHERE_COMMAND = "SELECT {distinct} {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_WHERE_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + WHERE {filters} + {options} + ) AS subquery; + """ + SELECT_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + {options} + ) AS subquery; + """ # ------------- child parent bidirectional sub queries SELECT_CHILD_BY_PK = """ @@ -339,11 +369,26 @@ class PgStatements: """ # select - SELECT_COMMAND = "SELECT {column_names} FROM {table_name} {options};".strip() - SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" - SELECT_WHERE_COMMAND = ( - "SELECT {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_COMMAND = ( + "SELECT {distinct} {column_names} FROM {table_name} {options};".strip() ) + SELECT_BY_PK = "SELECT {column_names} FROM {table_name} WHERE {pk_name} = {pk};" + SELECT_WHERE_COMMAND = "SELECT {distinct} {column_names} FROM {table_name} WHERE {filters} {options};".strip() + SELECT_WHERE_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + WHERE {filters} + {options} + ) AS subquery; + """ + SELECT_FN_COMMAND = """ + SELECT {fn}({column_names}) FROM ( + SELECT {column_names} + FROM {table_name} + {options} + ) AS subquery; + """ # ------------- child parent bidirectional sub queries SELECT_CHILD_BY_PK = """ diff --git a/dataloom/tests/mysql/test_query_msql.py b/dataloom/tests/mysql/test_query_msql.py index abd0308..5feb221 100644 --- a/dataloom/tests/mysql/test_query_msql.py +++ b/dataloom/tests/mysql/test_query_msql.py @@ -389,3 +389,313 @@ class Post(Model): assert [u for u in many_4] == [{"id": 1, "name": "Bob", "username": "@miller"}] conn.close() + + def test_find_one_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import MySQLConfig + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + mysql_loom = Loom( + dialect="mysql", + database=MySQLConfig.database, + password=MySQLConfig.password, + user=MySQLConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = mysql_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + mysql_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + mysql_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + mysql_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = mysql_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert post == {"id": 2} + post = mysql_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert post == {"id": 1} + + conn.close() + + def test_find_many_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import MySQLConfig + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + mysql_loom = Loom( + dialect="mysql", + database=MySQLConfig.database, + password=MySQLConfig.password, + user=MySQLConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = mysql_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + mysql_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + mysql_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + mysql_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = mysql_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert len(post) == 3 + post = mysql_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert len(post) == 3 + + conn.close() + + def test_distinct(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import MySQLConfig + + mysql_loom = Loom( + dialect="mysql", + database=MySQLConfig.database, + password=MySQLConfig.password, + user=MySQLConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = mysql_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + mysql_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + post = mysql_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + post = mysql_loom.find_all( + Post, + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + conn.close() diff --git a/dataloom/tests/mysql/test_utils_fns_mysql.py b/dataloom/tests/mysql/test_utils_fns_mysql.py new file mode 100644 index 0000000..edc366e --- /dev/null +++ b/dataloom/tests/mysql/test_utils_fns_mysql.py @@ -0,0 +1,99 @@ +class TestUtilsFunctionsMySQL: + def test_util_fns(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + UpdatedAtColumn, + TableColumn, + ForeignKeyColumn, + ColumnValue, + Filter, + ) + from dataloom.keys import MySQLConfig + from typing import Optional + + mysql_loom = Loom( + dialect="mysql", + database=MySQLConfig.database, + password=MySQLConfig.password, + user=MySQLConfig.user, + ) + + class User(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + + class Post(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + updatedAt = UpdatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, type="int", required=True, onDelete="CASCADE", onUpdate="CASCADE" + ) + + conn, _ = mysql_loom.connect_and_sync([Post, User], drop=True, force=True) + userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + mysql_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + count = mysql_loom.count( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, + ) + assert count == 3 + + avg = mysql_loom.avg( + instance=Post, + column="id", + distinct=True, + ) + assert avg == 2.5 + sum = mysql_loom.sum( + instance=Post, + column="id", + distinct=True, + ) + assert sum == 10 + min = mysql_loom.min( + instance=Post, + column="id", + distinct=True, + ) + assert min == 1 + max = mysql_loom.max( + instance=Post, + column="id", + distinct=True, + ) + assert max == 4 + conn.close() diff --git a/dataloom/tests/postgres/test_query_pg.py b/dataloom/tests/postgres/test_query_pg.py index 6555d02..f66fa3f 100644 --- a/dataloom/tests/postgres/test_query_pg.py +++ b/dataloom/tests/postgres/test_query_pg.py @@ -379,3 +379,313 @@ class Post(Model): assert [u for u in many_4] == [{"id": 1, "name": "Bob", "username": "@miller"}] conn.close() + + def test_find_one_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import PgConfig + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + pg_loom = Loom( + dialect="postgres", + database=PgConfig.database, + password=PgConfig.password, + user=PgConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = pg_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = pg_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + pg_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + pg_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + pg_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = pg_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert post == {"id": 2} + post = pg_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert post == {"id": 1} + + conn.close() + + def test_find_many_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import PgConfig + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + pg_loom = Loom( + dialect="postgres", + database=PgConfig.database, + password=PgConfig.password, + user=PgConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = pg_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = pg_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + pg_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + pg_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + pg_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = pg_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert len(post) == 3 + post = pg_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert len(post) == 3 + + conn.close() + + def test_distinct(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + from dataloom.keys import PgConfig + + pg_loom = Loom( + dialect="postgres", + database=PgConfig.database, + password=PgConfig.password, + user=PgConfig.user, + ) + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = pg_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = pg_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + pg_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + post = pg_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + post = pg_loom.find_all( + Post, + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + conn.close() diff --git a/dataloom/tests/postgres/test_utils_fns_pg.py b/dataloom/tests/postgres/test_utils_fns_pg.py new file mode 100644 index 0000000..65b854e --- /dev/null +++ b/dataloom/tests/postgres/test_utils_fns_pg.py @@ -0,0 +1,101 @@ +class TestUtilsFunctionsPG: + def test_util_fns(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + UpdatedAtColumn, + TableColumn, + ForeignKeyColumn, + ColumnValue, + Filter, + ) + + from typing import Optional + + from dataloom.keys import PgConfig + + pg_loom = Loom( + dialect="postgres", + database=PgConfig.database, + password=PgConfig.password, + user=PgConfig.user, + ) + + class User(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + + class Post(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + updatedAt = UpdatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, type="int", required=True, onDelete="CASCADE", onUpdate="CASCADE" + ) + + conn, _ = pg_loom.connect_and_sync([Post, User], drop=True, force=True) + userId = pg_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + pg_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + count = pg_loom.count( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, + ) + assert count == 3 + + avg = pg_loom.avg( + instance=Post, + column="id", + distinct=True, + ) + assert avg == 2.5 + sum = pg_loom.sum( + instance=Post, + column="id", + distinct=True, + ) + assert sum == 10 + min = pg_loom.min( + instance=Post, + column="id", + distinct=True, + ) + assert min == 1 + max = pg_loom.max( + instance=Post, + column="id", + distinct=True, + ) + assert max == 4 + conn.close() diff --git a/dataloom/tests/sqlite3/test_query_sqlite.py b/dataloom/tests/sqlite3/test_query_sqlite.py index 8ac05dd..897edb7 100644 --- a/dataloom/tests/sqlite3/test_query_sqlite.py +++ b/dataloom/tests/sqlite3/test_query_sqlite.py @@ -363,3 +363,295 @@ class Post(Model): assert [u for u in many_4] == [{"id": 1, "name": "Bob", "username": "@miller"}] conn.close() + + def test_find_one_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + sqlite_loom = Loom(dialect="sqlite", database="hi.db") + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = sqlite_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = sqlite_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + sqlite_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + sqlite_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + sqlite_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = sqlite_loom.find_one( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert post == {"id": 2} + post = sqlite_loom.find_one( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert post == {"id": 1} + + conn.close() + + def test_find_many_op_not_and_between(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + import pytest + from dataloom.exceptions import InvalidFilterValuesException + + sqlite_loom = Loom(dialect="sqlite", database="hi.db") + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = sqlite_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = sqlite_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + sqlite_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + with pytest.raises(InvalidFilterValuesException) as exc_info: + sqlite_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == "The operator BETWEEN expects a collection of two range values but got 3." + ) + with pytest.raises(InvalidFilterValuesException) as exc_info: + sqlite_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=[2, 8, 9], + ), + select=["id"], + ) + assert ( + str(exc_info.value) + == 'The column "id" value can not be a collection for the operator NOT.' + ) + + post = sqlite_loom.find_many( + Post, + filters=Filter( + column="id", + operator="between", + value=[2, 6], + ), + select=["id"], + ) + assert len(post) == 3 + post = sqlite_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["id"], + ) + assert len(post) == 3 + + conn.close() + + def test_distinct(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + TableColumn, + ForeignKeyColumn, + Filter, + ColumnValue, + ) + + sqlite_loom = Loom(dialect="sqlite", database="hi.db") + + class User(Model): + __tablename__: TableColumn = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + tokenVersion = Column(type="int", default=0) + + class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + conn, tables = sqlite_loom.connect_and_sync([User, Post], drop=True, force=True) + userId = sqlite_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + sqlite_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + post = sqlite_loom.find_many( + Post, + filters=Filter( + column="id", + operator="not", + value=2, + ), + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + post = sqlite_loom.find_all( + Post, + select=["completed"], + distinct=True, + ) + assert len(post) == 1 + conn.close() diff --git a/dataloom/tests/sqlite3/test_utils_fns_sqlite.py b/dataloom/tests/sqlite3/test_utils_fns_sqlite.py new file mode 100644 index 0000000..9c05533 --- /dev/null +++ b/dataloom/tests/sqlite3/test_utils_fns_sqlite.py @@ -0,0 +1,94 @@ +class TestUtilsFunctionsSQLite: + def test_util_fns(self): + from dataloom import ( + Loom, + Model, + Column, + PrimaryKeyColumn, + CreatedAtColumn, + UpdatedAtColumn, + TableColumn, + ForeignKeyColumn, + ColumnValue, + Filter, + ) + + from typing import Optional + + sqlite_loom = Loom(dialect="sqlite", database="hi.db") + + class User(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="users") + id = PrimaryKeyColumn(type="int", auto_increment=True) + name = Column(type="text", nullable=False, default="Bob") + username = Column(type="varchar", unique=True, length=255) + + class Post(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="posts") + id = PrimaryKeyColumn( + type="int", auto_increment=True, nullable=False, unique=True + ) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + updatedAt = UpdatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, type="int", required=True, onDelete="CASCADE", onUpdate="CASCADE" + ) + + conn, _ = sqlite_loom.connect_and_sync([Post, User], drop=True, force=True) + userId = sqlite_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), + ) + + for title in ["Hey", "Hello", "What are you doing", "Coding"]: + sqlite_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + count = sqlite_loom.count( + instance=Post, + filters=Filter( + column="id", + operator="between", + value=[1, 7], + ), + column="id", + limit=3, + offset=0, + distinct=True, + ) + assert count == 3 + + avg = sqlite_loom.avg( + instance=Post, + column="id", + distinct=True, + ) + assert avg == 2.5 + sum = sqlite_loom.sum( + instance=Post, + column="id", + distinct=True, + ) + assert sum == 10 + min = sqlite_loom.min( + instance=Post, + column="id", + distinct=True, + ) + assert min == 1 + max = sqlite_loom.max( + instance=Post, + column="id", + distinct=True, + ) + assert max == 4 + conn.close() diff --git a/dataloom/types/__init__.py b/dataloom/types/__init__.py index d68c4e0..b4b53ca 100644 --- a/dataloom/types/__init__.py +++ b/dataloom/types/__init__.py @@ -2,10 +2,13 @@ from dataclasses import dataclass, field from typing import Optional -OPERATOR_LITERAL = Literal["eq", "neq", "lt", "gt", "leq", "geq", "in", "notIn", "like"] +OPERATOR_LITERAL = Literal[ + "eq", "neq", "lt", "gt", "leq", "geq", "in", "notIn", "like", "not", "between" +] SLQ_OPERAND_LITERAL = Literal["AND", "OR"] INCREMENT_DECREMENT_LITERAL = Literal["increment", "decrement"] SQL_LOGGER_LITERAL = Literal["console", "file"] +UTILS_FUNCTION_LITERAL = Literal["avg", "count", "min", "max", "sum"] CASCADE_LITERAL = Literal["NO ACTION", "CASCADE", "SET NULL"] DIALECT_LITERAL = Literal["postgres", "mysql", "sqlite"] @@ -23,6 +26,8 @@ "in": "IN", "notIn": "NOT IN", "like": "LIKE", + "not": "NOT", + "between": "BETWEEN", } SLQ_OPERAND = { "AND": "AND", diff --git a/dataloom/utils/logger.py b/dataloom/utils/logger.py index 3c845fb..c34c764 100644 --- a/dataloom/utils/logger.py +++ b/dataloom/utils/logger.py @@ -1,5 +1,6 @@ from datetime import datetime from dataloom.types import DIALECT_LITERAL +import re class Colors: @@ -20,6 +21,7 @@ class logger: def file(fn): def wrapper(*args, **kwargs): sql_statement, file_name, dialect = fn(*args, **kwargs) + sql_statement = re.sub(r"\s+", " ", sql_statement) with open(file_name, "a+") as f: f.write( "[{time}] : Loom[{dialect}]: {sql_statement}\n".format( @@ -36,10 +38,11 @@ def wrapper(*args, **kwargs): def console(fn): def wrapper(*args, **kwargs): index, sql_statement, dialect = fn(*args, **kwargs) + sql_statement = re.sub(r"\s+", " ", sql_statement) if index % 2 == 0: print( Colors.BOLD - + Colors.CYAN + + Colors.PURPLE + f"[{dialect}:{datetime.now().time()}] " + Colors.RESET + Colors.BOLD @@ -50,7 +53,7 @@ def wrapper(*args, **kwargs): else: print( Colors.BOLD - + Colors.CYAN + + Colors.PURPLE + f"[{dialect}:{datetime.now().time()}] " + Colors.RESET + Colors.BOLD diff --git a/dataloom/utils/tables.py b/dataloom/utils/tables.py index 9f77c92..b02930b 100644 --- a/dataloom/utils/tables.py +++ b/dataloom/utils/tables.py @@ -42,7 +42,7 @@ def get_table_filters( op = get_operator(filter.operator) join = "" if len(filters) == idx + 1 else f" {filter.join_next_with}" - if op == "IN" or op == "NOT IN": + if op == "IN" or op == "NOT IN" or op == "BETWEEN": if is_collection(filter.value): _list = ", ".join( ["?" if dialect == "sqlite" else "%s" for i in filter.value] @@ -91,6 +91,33 @@ def get_table_filters( raise InvalidFilterValuesException( f'The column "{filter.column}" value can only be a list, tuple or dictionary but got {type(filter.value)} .' ) + elif op == "BETWEEN": + if is_collection(filter.value): + if len(filter.value) != 2: + raise InvalidFilterValuesException( + f"The operator BETWEEN expects a collection of two range values but got {len(filter.value)}." + ) + _min, _max = ["?", "?"] if dialect == "sqlite" else ["%s", "%s"] + _key = ( + f'"{key}" {op} {_min} AND {_max}' + if dialect == "postgres" + else f"`{key}` {op} {_min} AND {_max}" + ) + else: + raise InvalidFilterValuesException( + f'The column "{filter.column}" value can only be a list, tuple or dictionary but got {type(filter.value)} .' + ) + elif op == "NOT": + if not is_collection(filter.value): + _key = ( + f'{op} "{key}" = %s' + if dialect == "postgres" + else f"{op} `{key}` = {'%s' if dialect == 'mysql' else '?'}" + ) + else: + raise InvalidFilterValuesException( + f'The column "{filter.column}" value can not be a collection for the operator NOT.' + ) else: if not is_collection(filter.value): _key = ( diff --git a/hi.db b/hi.db index 1323039..2d389e7 100644 Binary files a/hi.db and b/hi.db differ diff --git a/playground.py b/playground.py index ae37944..1c2828c 100644 --- a/playground.py +++ b/playground.py @@ -52,33 +52,53 @@ ) -class Profile(Model): - __tablename__: Optional[TableColumn] = TableColumn(name="profiles") - _id = PrimaryKeyColumn(type="int", auto_increment=True) - name = Column(type="text", nullable=False, default="Bob") - - class User(Model): - __tablename__: Optional[TableColumn] = TableColumn(name="users") + __tablename__: TableColumn = TableColumn(name="users") id = PrimaryKeyColumn(type="int", auto_increment=True) name = Column(type="text", nullable=False, default="Bob") username = Column(type="varchar", unique=True, length=255) - bio = Column(type="varchar", unique=False, length=200, default="Hello world") tokenVersion = Column(type="int", default=0) - createdAt = CreatedAtColumn() - updatedAt = UpdatedAtColumn() - profileId = ForeignKeyColumn( - Profile, +class Post(Model): + __tablename__: TableColumn = TableColumn(name="posts") + id = PrimaryKeyColumn(type="int", auto_increment=True, nullable=False, unique=True) + completed = Column(type="boolean", default=False) + title = Column(type="varchar", length=255, nullable=False) + # timestamps + createdAt = CreatedAtColumn() + # relations + userId = ForeignKeyColumn( + User, + maps_to="1-N", type="int", - maps_to="1-1", + required=True, onDelete="CASCADE", onUpdate="CASCADE", - required=False, ) -conn, tables = pg_loom.connect_and_sync([Profile, User], alter=True) +conn, tables = mysql_loom.connect_and_sync([User, Post], drop=True, force=True) + +userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), +) + +for title in ["Hey", "Hello", "What are you doing", "Coding"]: + mysql_loom.insert_one( + instance=Post, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="title", value=title), + ], + ) + + +avg = mysql_loom.avg( + instance=Post, + column="id", + distinct=True, +) -print(pg_loom.inspect(Profile, print_table=False)) +print(avg) diff --git a/pyproject.toml b/pyproject.toml index 7e9e2be..bc0ea31 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "dataloom" -version = "2.1.1" +version = "2.2.0" authors = [ {name = "Crispen Gari", email = "crispengari@gmail.com"}, ] diff --git a/todo.txt b/todo.txt index 85d1fce..67cb63e 100644 --- a/todo.txt +++ b/todo.txt @@ -1,4 +1,4 @@ -1. altering tables +1. altering tables ✅ 2. inserting data ✅ 3. created at and updated at field ✅ 4. foreign key and primary key ✅ @@ -9,13 +9,16 @@ 10. querying with operations like (OR LIKE > < etc) ✅ 11. limit and pagination ✅ 12. in & not in ✅ -13. grouping data +13. grouping data ✅ 14. updating data ✅ 15. apply delete with limit ✅ -16. between -16. distict -17. sum, avg,count, min & max -18. Not +16. between ✅ +16. distinct ✅ +17. sum, avg,count, min & max, mean ✅ +18. Not ✅ +19. self relations +20. N-N relations +21. query builder --------- conn @@ -28,4 +31,4 @@ ---------- bugs -1. fixing logger index \ No newline at end of file +1. fixing logger index ✅ \ No newline at end of file