diff --git a/Changelog.md b/Changelog.md index 1df27ea..fd1fc90 100644 --- a/Changelog.md +++ b/Changelog.md @@ -24,30 +24,61 @@ We have release the new `dataloom` Version `1.1.0` (`2024-02-12`) - You can apply limits, offsets, filters and orders to your child associations during queries ```py - post = mysql_loom.find_one( - instance=Post, - filters=[Filter(column="userId", value=userId)], - select=["title", "id"], - include=[ - Include( - model=User, - select=["id", "username"], - has="one", - include=[Include(model=Profile, select=["avatar", "id"], has="one")], - ), - Include( - model=Category, - select=["id", "type"], - has="many", - order=[Order(column="id", order="DESC")], - limit=2, - ), - ], + post = mysql_loom.find_one( + instance=Post, + filters=[Filter(column="userId", value=userId)], + select=["title", "id"], + include=[ + Include( + model=User, + select=["id", "username"], + has="one", + include=[Include(model=Profile, select=["avatar", "id"], has="one")], + ), + Include( + model=Category, + select=["id", "type"], + has="many", + order=[Order(column="id", order="DESC")], + limit=2, + ), + ], + ) + ``` + +- Now `return_dict` has bee removed as an option in dataloom in the query functions like `find_by_pk`, `find_one`, `find_many` and `find_all` now works starting from this version. If you enjoy working with python objects you have to maneuver them manually using experimental features. + + ```py + from dataloom import experimental_decorators + + @experimental_decorators.initialize( + repr=True, to_dict=True, init=True, repr_identifier="id" ) + class Profile(Model): + __tablename__: Optional[TableColumn] = TableColumn(name="profiles") + id = PrimaryKeyColumn(type="int", auto_increment=True) + avatar = Column(type="text", nullable=False) + userId = ForeignKeyColumn( + User, + maps_to="1-1", + type="int", + required=True, + onDelete="CASCADE", + onUpdate="CASCADE", + ) + + # now you can do this + + profile = mysql_loom.find_many( + instance=Profile, + ) + print([Profile(**p) for p in profile]) # ? = [] + print([Profile(**p) for p in profile][0].id) # ? = 1 ``` -- `N-N` relational mapping -- Now you can return python objects when querying data meaning that the option `return_dict` in the query functions like `find_by_pk`, `find_one`, `find_many` and `find_all` now works starting from this version + - These experimental decorators as we name them `"experimental"` they are little bit slow and they work perfect in a single instance, you can not nest relationships on them. + - You can use them if you know how your data is structured and also if you know how to manipulate dictionaries + - Updated the documentation. - Grouping data in queries will also be part of this release, using the class `Group` @@ -99,3 +130,7 @@ We are pleased to release `dataloom` ORM for python version `3.12` and above. Th - Filter Records - Select field in records - etc. + +``` + +``` diff --git a/README.md b/README.md index 90a6f7b..5230c0e 100644 --- a/README.md +++ b/README.md @@ -78,8 +78,12 @@ - [Utilities](#utilities) - [`inspect`](#inspect) - [Associations](#associations) - - [`1-1` Association](#1-1-association) - - [`N-1` Association](#n-1-association) + - [1. `1-1` Association](#1-1-1-association) + - [Inserting](#inserting) + - [Retrieving Records](#retrieving-records) + - [2. `N-1` Association](#2-n-1-association) + - [Inserting](#inserting-1) + - [Retrieving Records](#retrieving-records-1) - [`1-N` Association](#1-n-association) - [What is coming next?](#what-is-coming-next) - [Contributing](#contributing) @@ -506,6 +510,18 @@ posts = pg_loom.find_all( #### `Include` +The `Include` class facilitates eager loading for models with relationships. Below is a table detailing the parameters available for the `Include` class: + +| Argument | Description | Type | Default | Required | +| --------- | ----------------------------------------------------------------------- | --------------------------- | --------------- | -------- | --- | +| `model` | The model to be included when eagerly fetching records. | `Model` | - | Yes | +| `order` | The list of order specifications for sorting the included data. | `list[Order]`, optional | `[]` | No | +| `limit` | The maximum number of records to include. | `int | None`, optional | `0` | No | +| `offset` | The number of records to skip before including. | `int | None`, optional | `0` | No | +| `select` | The list of columns to include. | `list[str] | None`, optional | `None` | No | +| `has` | The relationship type between the current model and the included model. | `INCLUDE_LITERAL`, optional | `"many"` | No | +| `include` | The extra included models. | `list[Include]`, optional | `[]` | No | + ### Syncing Tables Syncing tables involves the process of creating tables from models and saving them to a database. After defining your tables, you will need to synchronize your database tables using the `sync` method. @@ -702,14 +718,13 @@ print(user) # ? {'id': 1, 'username': '@miller'} This method take the following as arguments -| Argument | Description | Type | Default | Required | -| ------------- | -------------------------------------------------------------------- | -------------------------------- | ------- | -------- | -| `instance` | The model class to retrieve instances from. | `Model` | | `Yes` | -| `filters` | Filter or list of filters to apply to the query. | `Filter \| list[Filter] \| None` | `None` | `No` | -| `select` | List of fields to select from the instances. | `list[str]` | `[]` | `No` | -| `include` | List of related models to eagerly load. | `list[Include]` | `[]` | `No` | -| `return_dict` | Flag indicating whether to return the result as a dictionary or not. | `bool` | `True` | `No` | -| `offset` | Number of instances to skip before retrieving. | `int \| None` | `None` | `No` | +| Argument | Description | Type | Default | Required | +| ---------- | ------------------------------------------------ | -------------------------------- | ------- | -------- | +| `instance` | The model class to retrieve instances from. | `Model` | | `Yes` | +| `filters` | Filter or list of filters to apply to the query. | `Filter \| list[Filter] \| None` | `None` | `No` | +| `select` | List of fields to select from the instances. | `list[str]` | `[]` | `No` | +| `include` | List of related models to eagerly load. | `list[Include]` | `[]` | `No` | +| `offset` | Number of instances to skip before retrieving. | `int \| None` | `None` | `No` | ##### 4. `find_by_pk()` @@ -722,13 +737,12 @@ print(user) # ? {'id': 1, 'username': '@miller'} The method takes the following as arguments: -| Argument | Description | Type | Default | Required | -| ------------- | -------------------------------------------------------------------- | --------------- | ------- | -------- | -| `instance` | The model class to retrieve instances from. | `Model` | | `Yes` | -| `pk` | The primary key value to use for retrieval. | `Any` | | `Yes` | -| `select` | List of fields to select from the instances. | `list[str]` | `[]` | `No` | -| `include` | List of related models to eagerly load. | `list[Include]` | `[]` | `No` | -| `return_dict` | Flag indicating whether to return the result as a dictionary or not. | `bool` | `True` | `No` | +| Argument | Description | Type | Default | Required | +| ---------- | -------------------------------------------- | --------------- | ------- | -------- | +| `instance` | The model class to retrieve instances from. | `Model` | | `Yes` | +| `pk` | The primary key value to use for retrieval. | `Any` | | `Yes` | +| `select` | List of fields to select from the instances. | `list[str]` | `[]` | `No` | +| `include` | List of related models to eagerly load. | `list[Include]` | `[]` | `No` | #### 3. Updating a record @@ -1175,52 +1189,11 @@ The `inspect` function take the following arguments. 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: -#### `1-1` Association - -```py -from dataloom import ( - Dataloom, - Model, - PrimaryKeyColumn, - Column, - CreatedAtColumn, - UpdatedAtColumn, - TableColumn, - ForeignKeyColumn, - Filter, - ColumnValue, - Include, - Order, -) -from typing import Optional - -sqlite_loom = Dataloom( - dialect="sqlite", - database="hi.db", - logs_filename="sqlite-logs.sql", - sql_logger="console", -) - -pg_loom = Dataloom( - dialect="postgres", - database="hi", - password="root", - user="postgres", - sql_logger="console", -) - -mysql_loom = Dataloom( - dialect="mysql", - database="hi", - password="root", - user="root", - host="localhost", - logs_filename="logs.sql", - port=3306, - sql_logger="console", -) +#### 1. `1-1` Association +Let's consider an example where we want to map the relationship between a `User` and a `Profile`: +```py class User(Model): __tablename__: Optional[TableColumn] = TableColumn(name="users") id = PrimaryKeyColumn(type="int", auto_increment=True) @@ -1228,7 +1201,6 @@ class User(Model): username = Column(type="varchar", unique=True, length=255) tokenVersion = Column(type="int", default=0) - class Profile(Model): __tablename__: Optional[TableColumn] = TableColumn(name="profiles") id = PrimaryKeyColumn(type="int", auto_increment=True) @@ -1242,7 +1214,115 @@ class Profile(Model): onUpdate="CASCADE", ) +``` + +The above code demonstrates how to establish a `one-to-one` relationship between a `User` and a `Profile` using the `dataloom`. + +- `User` and `Profile` are two model classes inheriting from `Model`. +- Each model is associated with a corresponding table in the database, defined by the `__tablename__` attribute. +- Both models have a primary key column (`id`) defined using `PrimaryKeyColumn`. +- Additional columns (`name`, `username`, `tokenVersion` for `User` and `avatar`, `userId` for `Profile`) are defined using `Column`. +- The `userId` column in the `Profile` model establishes a foreign key relationship with the `id` column of the `User` model using `ForeignKeyColumn`. This relationship is specified to be a `one-to-one` relationship (`maps_to="1-1"`). +- Various constraints such as `nullable`, `unique`, `default`, and foreign key constraints (`onDelete`, `onUpdate`) are specified for the columns. +##### Inserting + +In the following code example we are going to demonstrate how we can create a `user` with a `profile`, first we need to create a user first so that we get reference to the user of the profile that we will create. + +```py +userId = mysql_loom.insert_one( + instance=User, + values=ColumnValue(name="username", value="@miller"), +) + +profileId = mysql_loom.insert_one( + instance=Profile, + values=[ + ColumnValue(name="userId", value=userId), + ColumnValue(name="avatar", value="hello.jpg"), + ], +) +``` + +This Python code snippet demonstrates how to insert data into the database using the `mysql_loom.insert_one` method, it also work on other methods like `insert_bulk`. + +1. **Inserting a User Record**: + + - The `mysql_loom.insert_one` method is used to insert a new record into the `User` table. + - The `instance=User` parameter specifies that the record being inserted belongs to the `User` model. + - The `values=ColumnValue(name="username", value="@miller")` parameter specifies the values to be inserted into the `User` table, where the `username` column will be set to `"@miller"`. + - The ID of the newly inserted record is obtained and assigned to the variable `userId`. + +2. **Inserting a Profile Record**: + - Again, the `mysql_loom.insert_one` method is called to insert a new record into the `Profile` table. + - The `instance=Profile` parameter specifies that the record being inserted belongs to the `Profile` model. + - The `values` parameter is a list containing two `ColumnValue` objects: + - The first `ColumnValue` object specifies that the `userId` column of the `Profile` table will be set to the `userId` value obtained from the previous insertion. + - The second `ColumnValue` object specifies that the `avatar` column of the `Profile` table will be set to `"hello.jpg"`. + - The ID of the newly inserted record is obtained and assigned to the variable `profileId`. + +##### Retrieving Records + +The following example shows you how you can retrieve the data in a associations + +```py +profile = mysql_loom.find_one( + instance=Profile, + select=["id", "avatar"], + filters=Filter(column="userId", value=userId), +) +user = mysql_loom.find_by_pk( + instance=User, + pk=userId, + select=["id", "username"], +) +user_with_profile = {**user, "profile": profile} +print(user_with_profile) # ? = {'id': 1, 'username': '@miller', 'profile': {'id': 1, 'avatar': 'hello.jpg'}} +``` + +This Python code snippet demonstrates how to query data from the database using the `mysql_loom.find_one` and `mysql_loom.find_by_pk` methods, and combine the results of these two records that have association. + +1. **Querying a Profile Record**: + + - The `mysql_loom.find_one` method is used to retrieve a single record from the `Profile` table. + - The `filters=Filter(column="userId", value=userId)` parameter filters the results to only include records where the `userId` column matches the `userId` value obtained from a previous insertion. + +2. **Querying a User Record**: + + - The `mysql_loom.find_by_pk` method is used to retrieve a single record from the `User` table based on its primary key (`pk=userId`). + - The `instance=User` parameter specifies that the record being retrieved belongs to the `User` model. + - The `select=["id", "username"]` parameter specifies that only the `id` and `username` columns should be selected. + - The retrieved user data is assigned to the variable `user`. + +3. **Combining User and Profile Data**: + - The user data (`user`) and profile data (`profile`) are combined into a single dictionary (`user_with_profile`) using dictionary unpacking (`{**user, "profile": profile}`). + - This dictionary represents a user with their associated profile. + +> 🏒 We have realized that we are performing three steps when querying records, which can be verbose. However, in dataloom, we have introduced `eager` data fetching for all methods that retrieve data from the database. The following example demonstrates how we can achieve the same result as before using eager loading: + +```python +# With eager loading +user_with_profile = mysql_loom.find_by_pk( + instance=User, + pk=userId, + select=["id", "username"], + include=[Include(model=Profile, select=["id", "avatar"], has="one")], +) +print(user_with_profile) # ? = {'id': 1, 'username': '@miller', 'profile': {'id': 1, 'avatar': 'hello.jpg'}} +``` + +This Python code snippet demonstrates how to use eager loading with the `mysql_loom.find_by_pk` method to efficiently retrieve data from the `User` and `Profile` tables in a single query. + +- Eager loading allows us to retrieve related data from multiple tables in a single database query, reducing the need for multiple queries and improving performance. +- In this example, the `include` parameter is used to specify eager loading for the `Profile` model associated with the `User` model. +- By including the `Profile` model with the `User` model in the `find_by_pk` method call, we instruct the database to retrieve both the user data (`id` and `username`) and the associated profile data (`id` and `avatar`) in a single query. +- This approach streamlines the data retrieval process and minimizes unnecessary database calls, leading to improved efficiency and performance in applications. + +#### 2. `N-1` Association + +Models can have `Many` to `One` relationship, it depends on how you define them. Let's have a look at the relationship between a `Category` and a `Post`. Many categories can belong to a single post. + +```py class Post(Model): __tablename__: Optional[TableColumn] = TableColumn(name="posts") id = PrimaryKeyColumn(type="int", auto_increment=True, nullable=False, unique=True) @@ -1260,7 +1340,6 @@ class Post(Model): onUpdate="CASCADE", ) - class Category(Model): __tablename__: Optional[TableColumn] = TableColumn(name="categories") id = PrimaryKeyColumn(type="int", auto_increment=True, nullable=False, unique=True) @@ -1275,29 +1354,24 @@ class Category(Model): onUpdate="CASCADE", ) +``` -conn, tables = mysql_loom.connect_and_sync( - [User, Profile, Post, Category], drop=True, force=True -) +In the provided code, we have two models: `Post` and `Category`. The relationship between these two models can be described as a `Many-to-One` relationship. +This means that many categories can belong to a single post. In other words: -userId = mysql_loom.insert_one( - instance=User, - values=ColumnValue(name="username", value="@miller"), -) +- For each `Post` instance, there can be multiple `Category` instances associated with it. +- However, each `Category` instance can only be associated with one `Post`. -userId2 = mysql_loom.insert_one( - instance=User, - values=ColumnValue(name="username", value="bob"), -) +For example, consider a blogging platform where each `Post` represents an article and each `Category` represents a topic or theme. Each article (post) can be assigned to multiple topics (categories), such as "Technology", "Travel", "Food", etc. However, each topic (category) can only be associated with one specific article (post). -profileId = mysql_loom.insert_one( - instance=Profile, - values=[ - ColumnValue(name="userId", value=userId), - ColumnValue(name="avatar", value="hello.jpg"), - ], -) +This relationship allows for a hierarchical organization of data, where posts can be categorized into different topics or themes represented by categories. + +##### Inserting + +Let's illustrate the following example where we insert categories into a post with the `id` 1. + +```py for title in ["Hey", "Hello", "What are you doing", "Coding"]: mysql_loom.insert_one( instance=Post, @@ -1307,7 +1381,6 @@ for title in ["Hey", "Hello", "What are you doing", "Coding"]: ], ) - for cat in ["general", "education", "tech", "sport"]: mysql_loom.insert_one( instance=Category, @@ -1316,91 +1389,17 @@ for cat in ["general", "education", "tech", "sport"]: ColumnValue(name="type", value=cat), ], ) - print() - - -profile = mysql_loom.find_by_pk( - instance=Profile, - pk=profileId, - include=[Include(model=User, select=["id", "username", "tokenVersion"], has="one")], -) -print(profile) - -user = mysql_loom.find_by_pk( - instance=User, - pk=userId, - include=[Include(model=Profile, select=["id", "avatar"], has="one")], -) -print(user) - -user = mysql_loom.find_by_pk( - instance=User, - pk=userId, - include=[ - Include( - model=Post, - select=["id", "title"], - has="many", - offset=0, - limit=2, - order=[ - Order(column="createdAt", order="DESC"), - Order(column="id", order="DESC"), - ], - ), - Include(model=Profile, select=["id", "avatar"], has="one"), - ], -) -print(user) - -post = mysql_loom.find_by_pk( - instance=Post, - pk=1, - select=["title", "id"], - include=[ - Include( - model=User, - select=["id", "username"], - has="one", - include=[Include(model=Profile, select=["avatar", "id"], has="one")], - ), - Include( - model=Category, - select=["id", "type"], - has="many", - order=[Order(column="id", order="DESC")], - ), - ], -) +``` -print(post) +- **Inserting Posts** + We're inserting new posts into the `Post` table. Each post is associated with a user (`userId`), and we're iterating over a list of titles to insert multiple posts. +- **Inserting Categories** + We're inserting new categories into the `Category` table. Each category is associated with a specific post (`postId`), and we're inserting categories for a post with `id` 1. -user = mysql_loom.find_by_pk( - instance=User, - pk=userId2, - select=["username", "id"], - include=[ - Include( - model=Post, - select=["id", "title"], - has="many", - include=[ - Include( - model=Category, - select=["type", "id"], - has="many", - order=[Order(column="id", order="DESC")], - limit=2, - offset=0, - ) - ], - ), - ], -) +> In summary, we're creating a relationship between posts and categories by inserting records into their respective tables. Each category record is linked to a specific post record through the `postId` attribute. -print(user) -``` +##### Retrieving Records ```py @@ -1619,15 +1618,13 @@ print(posts) ``` -#### `N-1` Association - #### `1-N` Association ### What is coming next? -1. Associations -2. Grouping -3. Altering tables +1. N-N associations +2. Altering tables +3. Grouping ### Contributing diff --git a/dataloom/__init__.py b/dataloom/__init__.py index b4e4bf3..0bcb10f 100644 --- a/dataloom/__init__.py +++ b/dataloom/__init__.py @@ -1,5 +1,5 @@ from dataloom.loom import Dataloom - +from dataloom.decorators import experimental_decorators from dataloom.types import Order, Include, Filter, ColumnValue from dataloom.model import Model from dataloom.columns import ( @@ -24,4 +24,5 @@ Dataloom, TableColumn, Model, + experimental_decorators, ] diff --git a/dataloom/decorators/__init__.py b/dataloom/decorators/__init__.py new file mode 100644 index 0000000..798d8c0 --- /dev/null +++ b/dataloom/decorators/__init__.py @@ -0,0 +1,134 @@ +from dataloom.exceptions import InvalidPropertyException +from dataloom.columns import ( + PrimaryKeyColumn, + Column, + CreatedAtColumn, + UpdatedAtColumn, + ForeignKeyColumn, +) +import typing, dataloom # noqa +import inspect + + +class experimental_decorators: + @staticmethod + def initialize( + to_dict: bool = False, + init: bool = True, + repr: bool = False, + repr_identifier: str | None = None, + ): + """ + initialize + ---------- + Constructor method for the initialize decorator. + + Parameters + ---------- + to_dict : bool, optional + If True, generates a to_dict method for the decorated class. Default is False. + init : bool, optional + If True, generates an __init__ method for the decorated class. Default is True. + repr : bool, optional + If True, generates a __repr__ method for the decorated class. Default is False. + repr_identifier : str | None, optional + The identifier to be used in the __repr__ method. Default is None. + + Returns + ------- + Callable[[Any], type[wrapper]] + A callable that takes a class and returns a wrapped version of it. + + Examples + -------- + >>> from dataloom import ( + ... Dataloom, + ... Model, + ... PrimaryKeyColumn, + ... Column, + ... CreatedAtColumn, + ... UpdatedAtColumn, + ... TableColumn, + ... ForeignKeyColumn, + ... Filter, + ... ColumnValue, + ... Include, + ... Order, + ... experimental_decorators, + ... ) + ... + ... @initialize(repr=True, to_dict=True, init=True, repr_identifier="id") + ... class Profile(Model): + ... __tablename__: Optional[TableColumn] = TableColumn(name="profiles") + ... id = PrimaryKeyColumn(type="int", auto_increment=True) + ... avatar = Column(type="text", nullable=False) + ... userId = ForeignKeyColumn( + ... User, + ... maps_to="1-1", + ... type="int", + ... required=True, + ... onDelete="CASCADE", + ... onUpdate="CASCADE", + ... ) + + + """ + + def fn(cls): + args = [] + for name, field in inspect.getmembers(cls): + if isinstance(field, PrimaryKeyColumn): + args.append(name) + elif isinstance(field, Column): + args.append(name) + elif isinstance(field, CreatedAtColumn): + args.append(name) + elif isinstance(field, UpdatedAtColumn): + args.append(name) + elif isinstance(field, ForeignKeyColumn): + args.append(name) + + class wrapper(cls): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + init_code = "" + + if init: + init_args = ", ".join([f"{p} = None" for p in args]) + init_code += f"def __init__(self, {init_args}) -> None:\n" + for attr_name in args: + init_code += f" self.{attr_name} = {attr_name}\n" + + init_code += "\n" + if to_dict: + init_code += "@property\n" + init_code += "def to_dict(self) -> dict:\n" + init_code += " return {\n" + for key in args: + init_code += f" '{key}' : self.{key},\n" + init_code += " }\n\n" + + if repr_identifier is None: + identifier = args[0] + else: + identifier = repr_identifier + if repr_identifier not in args: + raise InvalidPropertyException( + f"'{cls.__name__}' has no property '{repr_identifier}'." + ) + + if repr: + init_code += "def __repr__(self) -> str:\n" + init_code += f" return f'<{cls.__name__}:{identifier}={{self.{identifier}}}>'\n\n" + + local_ns = {} + # Execute the dynamically generated methods + exec(init_code, globals(), local_ns) + wrapper.__init__ = local_ns["__init__"] + wrapper.__repr__ = local_ns.get("__repr__") + wrapper.to_dict = local_ns.get("to_dict") + wrapper.__name__ = cls.__name__ + return wrapper + + return fn diff --git a/dataloom/exceptions/__init__.py b/dataloom/exceptions/__init__.py index 8dea18c..d52c74b 100644 --- a/dataloom/exceptions/__init__.py +++ b/dataloom/exceptions/__init__.py @@ -6,6 +6,10 @@ class InvalidArgumentsException(Exception): pass +class InvalidPropertyException(Exception): + pass + + class TooManyPkException(Exception): pass diff --git a/dataloom/keys.py b/dataloom/keys.py index a519747..6129f81 100644 --- a/dataloom/keys.py +++ b/dataloom/keys.py @@ -1,4 +1,4 @@ -push = False +push = True class PgConfig: diff --git a/dataloom/loom/__init__.py b/dataloom/loom/__init__.py index ad51b3e..fe24c22 100644 --- a/dataloom/loom/__init__.py +++ b/dataloom/loom/__init__.py @@ -258,7 +258,6 @@ def find_many( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Model] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -279,8 +278,7 @@ def find_many( Columns to select in the query. Default is an empty list, which selects all columns. include : list[Model], optional Models to include in the query (e.g., for JOIN operations). - return_dict : bool, optional - If True, returns results as dictionaries. If False, returns results as instances of the Model class. Default is True. + limit : int | None, optional The maximum number of rows to retrieve. Default is None. offset : int | None, optional @@ -331,7 +329,6 @@ def find_many( include=include, offset=offset, filters=filters, - return_dict=return_dict, order=order, ) @@ -340,7 +337,6 @@ def find_all( instance: Model, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -359,8 +355,6 @@ def find_all( Columns to select in the query. Default is an empty list, which selects all columns. include : list[Include], optional Models to include in the query (e.g., for JOIN operations). - return_dict : bool, optional - If True, returns results as dictionaries. If False, returns results as instances of the Model class. Default is True. limit : int | None, optional The maximum number of rows to retrieve. Default is None. offset : int | None, optional @@ -408,7 +402,6 @@ def find_all( instance=instance, select=select, include=include, - return_dict=return_dict, limit=limit, offset=offset, order=order, @@ -420,7 +413,6 @@ def find_by_pk( pk, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, ): """ find_by_pk @@ -438,8 +430,6 @@ def find_by_pk( Columns to select in the query. Default is an empty list, which selects all columns. include : list[Include], optional Models to include in the query (e.g., for JOIN operations). - return_dict : bool, optional - If True, returns the result as a dictionary. If False, returns the result as an instance of the Model class. Default is True. Returns ------- @@ -480,7 +470,6 @@ def find_by_pk( return query(dialect=self.dialect, _execute_sql=self._execute_sql).find_by_pk( include=include, pk=pk, - return_dict=return_dict, select=select, instance=instance, ) @@ -491,7 +480,6 @@ def find_one( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, offset: Optional[int] = None, ): """ @@ -510,8 +498,6 @@ def find_one( Columns to select in the query. Default is an empty list, which selects all columns. include : list[Include], optional Models to include in the query (e.g., for JOIN operations). - return_dict : bool, optional - If True, returns the result as a dictionary. If False, returns the result as an instance of the Model class. Default is True. offset : int | None, optional The offset of the row to retrieve, useful for pagination. Default is None. @@ -557,7 +543,6 @@ def find_one( filters=filters, offset=offset, include=include, - return_dict=return_dict, ) def update_by_pk( diff --git a/dataloom/loom/interfaces.py b/dataloom/loom/interfaces.py index 58041c3..42b4516 100644 --- a/dataloom/loom/interfaces.py +++ b/dataloom/loom/interfaces.py @@ -72,7 +72,6 @@ def find_many( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Model] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -85,7 +84,6 @@ def find_all( instance: Model, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -99,7 +97,6 @@ def find_by_pk( pk, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, ) -> dict | None: raise NotImplementedError("The find_by_pk method was not implemented.") @@ -110,7 +107,6 @@ def find_one( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, offset: Optional[int] = None, ) -> dict | None: raise NotImplementedError("The find_one method was not implemented.") diff --git a/dataloom/loom/query.py b/dataloom/loom/query.py index 6030b98..1cf8980 100644 --- a/dataloom/loom/query.py +++ b/dataloom/loom/query.py @@ -15,7 +15,6 @@ def find_many( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Model] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -28,7 +27,6 @@ def find_all( instance: Model, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], @@ -42,7 +40,6 @@ def find_by_pk( pk, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, ) -> dict | None: raise NotImplementedError("The find_by_pk method was not implemented.") @@ -53,7 +50,6 @@ def find_one( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, offset: Optional[int] = None, ) -> dict | None: raise NotImplementedError("The find_one method was not implemented.") @@ -72,12 +68,10 @@ def find_many( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Model] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], ) -> list: - return_dict = True data = [] if len(include) == 0: sql, params, fields = instance._get_select_where_stm( @@ -92,7 +86,8 @@ def find_many( args = get_args(params) rows = self._execute_sql(sql, fetchall=True, args=args) for row in rows: - data.append(dict(zip(fields, row))) + d = dict(zip(fields, row)) + data.append(d) else: # run sub queries instead data = subquery( @@ -113,12 +108,10 @@ def find_all( instance: Model, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, limit: Optional[int] = None, offset: Optional[int] = None, order: Optional[list[Order]] = [], ) -> list: - return_dict = True data = [] if len(include) == 0: sql, params, fields = instance._get_select_where_stm( @@ -152,13 +145,7 @@ def find_by_pk( pk, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, ) -> dict | None: - # """ - # This part will be added in the future version. - # """ - return_dict = True - # what is the name of the primary key column? well we will find out sql, fields, _includes = instance._get_select_by_pk_stm( dialect=self.dialect, select=select, include=[] @@ -178,10 +165,8 @@ def find_one( filters: Optional[Filter | list[Filter]] = None, select: list[str] = [], include: list[Include] = [], - return_dict: bool = True, offset: Optional[int] = None, ) -> dict | None: - return_dict = True sql, params, fields = instance._get_select_where_stm( dialect=self.dialect, filters=filters, diff --git a/dataloom/loom/subqueries.py b/dataloom/loom/subqueries.py index 691e59e..c25e40f 100644 --- a/dataloom/loom/subqueries.py +++ b/dataloom/loom/subqueries.py @@ -57,7 +57,6 @@ def get_find_many_relations( order=order, ) args = get_args(params) - print(sql) pks = self._execute_sql(sql, fetchall=True, args=args, _verbose=0) data = [] for (pk,) in pks: diff --git a/dataloom/types/__init__.py b/dataloom/types/__init__.py index 62f5824..c976ff1 100644 --- a/dataloom/types/__init__.py +++ b/dataloom/types/__init__.py @@ -282,8 +282,10 @@ class Include[Model]: The number of records to skip before including. Default is 0 (no offset). select : list[str] | None, optional The list of columns to include. Default is None (include all columns). - maps_to : RELATIONSHIP_LITERAL, optional - The relationship type between the current model and the included model. Default is "1-N" (one-to-many). + has : INCLUDE_LITERAL, optional + The relationship type between the current model and the included model. Default is "many". + include : list[Include], optional + The extra included models. Returns ------- @@ -300,11 +302,11 @@ class Include[Model]: -------- >>> from dataloom import Include, Model, Order ... - ... # Including posts for a user with ID 1 - ... mysql_loom.find_by_pk( - ... User, pk=1, include=[Include(Post, limit=2, offset=0, select=["id", "title"], maps_to="1-N")] + ... # get the profile and the user of that profile in one eager query. + ... profile = mysql_loom.find_many( + ... instance=Profile, + ... include=[Include(model=User, select=["id", "username", "tokenVersion"], has="one")], ... ) - """ model: Model = field(repr=False) diff --git a/hi.db b/hi.db index 4658421..111ab6f 100644 Binary files a/hi.db and b/hi.db differ diff --git a/playground.py b/playground.py index 2f576cc..00132fb 100644 --- a/playground.py +++ b/playground.py @@ -11,9 +11,11 @@ ColumnValue, Include, Order, + experimental_decorators, ) -import json +import json, time from typing import Optional +from dataclasses import dataclass sqlite_loom = Dataloom( dialect="sqlite", @@ -38,7 +40,7 @@ host="localhost", logs_filename="logs.sql", port=3306, - sql_logger="console", + sql_logger=None, ) @@ -50,6 +52,9 @@ class User(Model): tokenVersion = Column(type="int", default=0) +@experimental_decorators.initialize( + repr=True, to_dict=True, init=True, repr_identifier="id" +) class Profile(Model): __tablename__: Optional[TableColumn] = TableColumn(name="profiles") id = PrimaryKeyColumn(type="int", auto_increment=True) @@ -138,12 +143,107 @@ class Category(Model): ], ) +user_with_profile = mysql_loom.find_by_pk( + instance=User, + pk=userId, + select=["id", "username"], + include=[Include(model=Profile, select=["id", "avatar"], has="one")], +) +print(user_with_profile) +print(user_with_profile) + +# user = mysql_loom.find_all( +# instance=User, +# include=[Include(model=Profile, select=["id", "avatar"], has="one")], +# ) +# print(user) + +# user = mysql_loom.find_all( +# instance=User, +# include=[ +# Include( +# model=Post, +# select=["id", "title"], +# has="many", +# offset=0, +# limit=2, +# order=[ +# Order(column="createdAt", order="DESC"), +# Order(column="id", order="DESC"), +# ], +# ), +# Include(model=Profile, select=["id", "avatar"], has="one"), +# ], +# ) +# print(user) + +# post = mysql_loom.find_all( +# instance=Post, +# select=["title", "id"], +# limit=5, +# offset=0, +# order=[Order(column="id", order="DESC")], +# include=[ +# Include( +# model=User, +# select=["id", "username"], +# has="one", +# include=[Include(model=Profile, select=["avatar", "id"], has="one")], +# ), +# Include( +# model=Category, +# select=["id", "type"], +# has="many", +# order=[Order(column="id", order="DESC")], +# limit=2, +# include=[Include(model=Post, has="one")], +# ), +# ], +# ) + +# print(json.dumps(post, indent=2)) + -# profile = mysql_loom.find_all( -# instance=Profile, -# include=[Include(model=User, select=["id", "username", "tokenVersion"], has="one")], +# user = mysql_loom.find_many( +# instance=User, +# filters=[Filter(column="id", value=1)], +# select=["username", "id"], +# limit=1, +# offset=0, +# order=[Order(column="id", order="ASC")], +# include=[ +# Include( +# model=Post, +# select=["id", "title"], +# has="many", +# limit=1, +# offset=0, +# order=[Order(column="id", order="ASC")], +# include=[ +# Include( +# model=Category, +# select=["type", "id"], +# has="many", +# order=[Order(column="id", order="DESC")], +# limit=2, +# offset=0, +# ), +# Include( +# model=User, +# select=["username", "id"], +# has="one", +# ), +# ], +# ), +# ], # ) -# print(profile) + +# print(json.dumps(user, indent=2)) + + +# posts = mysql_loom.find_all(Post, select=["id", "completed"]) +# print(posts) + # user = mysql_loom.find_all( # instance=User, @@ -197,41 +297,41 @@ class Category(Model): # print(json.dumps(post, indent=2)) -user = mysql_loom.find_many( - instance=User, - filters=[Filter(column="id", value=1)], - select=["username", "id"], - limit=1, - offset=0, - order=[Order(column="id", order="ASC")], - include=[ - Include( - model=Post, - select=["id", "title"], - has="many", - limit=1, - offset=0, - order=[Order(column="id", order="ASC")], - include=[ - Include( - model=Category, - select=["type", "id"], - has="many", - order=[Order(column="id", order="DESC")], - limit=2, - offset=0, - ), - Include( - model=User, - select=["username", "id"], - has="one", - ), - ], - ), - ], -) +# user = mysql_loom.find_many( +# instance=User, +# filters=[Filter(column="id", value=1)], +# select=["username", "id"], +# limit=1, +# offset=0, +# order=[Order(column="id", order="ASC")], +# include=[ +# Include( +# model=Post, +# select=["id", "title"], +# has="many", +# limit=1, +# offset=0, +# order=[Order(column="id", order="ASC")], +# include=[ +# Include( +# model=Category, +# select=["type", "id"], +# has="many", +# order=[Order(column="id", order="DESC")], +# limit=2, +# offset=0, +# ), +# Include( +# model=User, +# select=["username", "id"], +# has="one", +# ), +# ], +# ), +# ], +# ) -print(json.dumps(user, indent=2)) +# print(json.dumps(user, indent=2)) # posts = mysql_loom.find_all(Post, select=["id", "completed"])