Skip to content

Commit

Permalink
Merge pull request #9 from CrispenGari/query-builder
Browse files Browse the repository at this point in the history
query builder and the doccumentation
  • Loading branch information
CrispenGari authored Feb 26, 2024
2 parents 081d1d2 + 7f784bd commit b2cb16e
Show file tree
Hide file tree
Showing 12 changed files with 415 additions and 30 deletions.
36 changes: 35 additions & 1 deletion Changelog.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,44 @@
===
Dataloom **`2.3.0`**
===

### Release Notes - `dataloom`

We have release the new `dataloom` Version `2.3.0` (`2024-02-26`)

##### Features

- updated documentation.
- Query Builder in executing queries and SQL Scripts.

```py
qb = loom.getQueryBuilder()
res = qb.run("select id from posts;", fetchall=True)
print(res)
```

We can use the query builder to execute the SQL as follows:

```py
with open("qb.sql", "r") as reader:
sql = reader.read()
res = qb.run(
sql,
fetchall=True,
is_script=True,
)
print(res)
```

> 👍 **Pro Tip:** Executing a script using query builder does not return a result. The result value is always `None`.
===
Dataloom **`2.2.0`**
===

### Release Notes - `dataloom`

We have release the new `dataloom` Version `2.2.0` (`2024-02-24`)
We have release the new `dataloom` Version `2.2.0` (`2024-02-25`)

##### Features

Expand Down
75 changes: 74 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,8 @@
- [4. What about bidirectional queries?](#4-what-about-bidirectional-queries)
- [1. Child to Parent](#1-child-to-parent)
- [2. Parent to Child](#2-parent-to-child)
- [Query Builder.](#query-builder)
- [Why Use Query Builder?](#why-use-query-builder)
- [What is coming next?](#what-is-coming-next)
- [Contributing](#contributing)
- [License](#license)
Expand Down Expand Up @@ -2112,10 +2114,81 @@ print(user_post) """ ? =

```

### Query Builder.

Dataloom exposes a method called `getQueryBuilder`, which allows you to obtain a `qb` object. This object enables you to execute SQL queries directly from SQL scripts.

```py
qb = loom.getQueryBuilder()

print(qb) # ? = Loom QB<mysql>
```

The `qb` object contains the method called `run`, which is used to execute SQL scripts or SQL queries.

```py
ids = qb.run("select id from posts;", fetchall=True)
print(ids) # ? = [(1,), (2,), (3,), (4,)]
```

You can also execute SQL files. In the following example, we will demonstrate how you can execute SQL scripts using the `qb`. Let's say we have an SQL file called `qb.sql` which contains the following SQL code:

```SQL
SELECT id, title FROM posts WHERE id IN (1, 3, 2, 4) LIMIT 4 OFFSET 1;
SELECT COUNT(*) FROM (
SELECT DISTINCT `id`
FROM `posts`
WHERE `id` < 5
LIMIT 3 OFFSET 2
) AS subquery;
```

We can use the query builder to execute the SQL as follows:

```py
with open("qb.sql", "r") as reader:
sql = reader.read()
res = qb.run(
sql,
fetchall=True,
is_script=True,
)
print(res)
```

> 👍 **Pro Tip:** Executing a script using query builder does not return a result. The result value is always `None`.
The `run` method takes the following as arguments:

| Argument | Description | Type | Required | Default |
| --------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------- | -------- | ------- |
| `sql` | SQL query to execute. | `str` | Yes | |
| `args` | Parameters for the SQL query. | `Any \| None` | No | `None` |
| `fetchone` | Whether to fetch only one result. | `bool` | No | `False` |
| `fetchmany` | Whether to fetch multiple results. | `bool` | No | `False` |
| `fetchall` | Whether to fetch all results. | `bool` | No | `False` |
| `mutation` | Whether the query is a mutation (insert, update, delete). | `bool` | No | `True` |
| `bulk` | Whether the query is a bulk operation. | `bool` | No | `False` |
| `affected_rows` | Whether to return affected rows. | `bool` | No | `False` |
| `operation` | Type of operation being performed. | `'insert', 'update', 'delete', 'read' \| None` | No | `None` |
| `verbose` | Verbosity level for logging . Set this option to `0` if you don't want logging at all. | `int` | No | `1` |
| `is_script` | Whether the SQL is a script. | `bool` | No | `False` |

#### Why Use Query Builder?

- The query builder empowers developers to seamlessly execute `SQL` queries directly.
- While Dataloom primarily utilizes `subqueries` for eager data fetching on models, developers may prefer to employ JOIN operations, which are achievable through the `qb` object.

```python
qb = loom.getQueryBuilder()
result = qb.run("SELECT * FROM table1 INNER JOIN table2 ON table1.id = table2.table1_id;")
print(result)
```

### What is coming next?

1. N-N associations
2. Query Builder
2. Self relations

### Contributing

Expand Down
30 changes: 28 additions & 2 deletions dataloom/loom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from dataloom.model import Model
from dataloom.statements import GetStatement
from dataloom.conn import ConnectionOptionsFactory
from typing import Optional, Any
from typing import Optional, Any, Literal
from mysql.connector.pooling import PooledMySQLConnection
from sqlite3 import Connection
from mysql.connector.connection import MySQLConnectionAbstract
Expand All @@ -27,6 +27,7 @@
)
from dataloom.loom.interfaces import ILoom
from dataloom.loom.math import math
from dataloom.loom.qb import qb


class Loom(ILoom):
Expand Down Expand Up @@ -1165,7 +1166,7 @@ def _execute_sql(
mutation=True,
bulk: bool = False,
affected_rows: bool = False,
operation: Optional[str] = None,
operation: Optional[Literal["insert", "update", "delete", "read"]] = None,
_verbose: int = 1,
_is_script: bool = False,
) -> Any:
Expand Down Expand Up @@ -1785,3 +1786,28 @@ def count(
distinct=distinct,
filters=filters,
)

# qb

def getQueryBuilder(self):
"""
getQueryBuilder
---------------
Retrieves a query builder instance.
Parameters
----------
No parameters
Returns
-------
qb
Query builder instance.
Examples
--------
>>> qb = loom.getQueryBuilder()
... print(qb)
"""
builder = qb(_execute_sql=self._execute_sql, dialect=self.dialect)
return builder
15 changes: 0 additions & 15 deletions dataloom/loom/interfaces.py
Original file line number Diff line number Diff line change
Expand Up @@ -218,21 +218,6 @@ def delete_bulk(
def tables(self):
raise NotImplementedError("The tables property was not implemented")

@abstractclassmethod
def _execute_sql(
self,
sql: str,
args=None,
fetchone=False,
fetchmany=False,
fetchall=False,
mutation=True,
bulk: bool = False,
affected_rows: bool = False,
operation: Optional[str] = None,
) -> Any:
raise NotImplementedError("The _execute_sql method was not implemented.")

def connect(
self,
) -> Any | PooledMySQLConnection | MySQLConnectionAbstract | Connection:
Expand Down
88 changes: 88 additions & 0 deletions dataloom/loom/qb.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
from typing import Callable, Any, Literal, Optional

from dataloom.types import DIALECT_LITERAL


class qb:
def __repr__(self) -> str:
return f"Loom QB<{self.dialect}>"

def __str__(self) -> str:
return f"Loom QB<{self.dialect}>"

def __init__(
self, _execute_sql: Callable[..., Any], dialect: DIALECT_LITERAL
) -> None:
self.__exc = _execute_sql
self.dialect = dialect

def run(
self,
sql: str,
args: Any | None = None,
fetchone: bool = False,
fetchmany: bool = False,
fetchall: bool = False,
mutation: bool = True,
bulk: bool = False,
affected_rows: bool = False,
operation: Optional[Literal["insert", "update", "delete", "read"]] = None,
verbose: int = 1,
is_script: bool = False,
):
"""
run
-----------
Execute SQL query with optional parameters.
Parameters
----------
sql : str
SQL query to execute.
args : Any | None, optional
Parameters for the SQL query. Defaults to None.
fetchone : bool, optional
Whether to fetch only one result. Defaults to False.
fetchmany : bool, optional
Whether to fetch multiple results. Defaults to False.
fetchall : bool, optional
Whether to fetch all results. Defaults to False.
mutation : bool, optional
Whether the query is a mutation (insert, update, delete). Defaults to True.
bulk : bool, optional
Whether the query is a bulk operation. Defaults to False.
affected_rows : bool, optional
Whether to return affected rows. Defaults to False.
operation : Literal['insert', 'update', 'delete', 'read'] | None, optional
Type of operation being performed. Defaults to None.
verbose : int, optional
Verbosity level for logging. Defaults to 1.
is_script : bool, optional
Whether the SQL is a script. Defaults to False.
Returns
-------
Any
Query result.
Examples
--------
>>> qb = loom.getQueryBuilder()
... ids = qb.run("select id from posts;", fetchall=True)
... print(ids)
...
"""
return self.__exc(
sql,
args=args,
fetchall=fetchall,
fetchmany=fetchmany,
fetchone=fetchone,
mutation=mutation,
bulk=bulk,
affected_rows=affected_rows,
operation=operation,
_verbose=verbose,
_is_script=is_script,
)
5 changes: 2 additions & 3 deletions dataloom/loom/sql.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from typing import Any, Optional
from typing import Any, Optional, Literal
from dataloom.types import DIALECT_LITERAL, SQL_LOGGER_LITERAL
from dataloom.exceptions import UnsupportedDialectException
from sqlite3 import Connection
Expand Down Expand Up @@ -59,7 +59,7 @@ def execute_sql(
mutation=True,
bulk: bool = False,
affected_rows: bool = False,
operation: Optional[str] = None,
operation: Optional[Literal["insert", "update", "delete", "read"]] = None,
_verbose: int = 1,
_is_script: bool = False,
) -> Any:
Expand Down Expand Up @@ -110,7 +110,6 @@ def execute_sql(
except Exception:
pass
return None

if args is None:
cursor.execute(sql)
else:
Expand Down
63 changes: 63 additions & 0 deletions dataloom/tests/mysql/test_qb_mysql.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
class TestQBMySQL:
def test_qb(self):
from dataloom import (
Loom,
Model,
Column,
PrimaryKeyColumn,
CreatedAtColumn,
UpdatedAtColumn,
TableColumn,
ForeignKeyColumn,
ColumnValue,
)
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),
],
)
qb = mysql_loom.getQueryBuilder()
res = qb.run("select id from posts;", fetchall=True)
assert str(qb) == "Loom QB<mysql>"
assert len(res) == 4
conn.close()
Loading

0 comments on commit b2cb16e

Please sign in to comment.