Skip to content

Commit

Permalink
nested-relational-queries
Browse files Browse the repository at this point in the history
  • Loading branch information
CrispenGari committed Feb 16, 2024
1 parent c74355c commit d0fa850
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 279 deletions.
163 changes: 98 additions & 65 deletions dataloom/loom/subqueries.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from dataloom.model import Model
from dataclasses import dataclass
from typing import Callable, Any
import re


@dataclass(kw_only=True)
Expand All @@ -11,77 +12,109 @@ class subquery:
_execute_sql: Callable[..., Any]

def get_find_by_pk_relations(self, parent: Model, pk, includes: list[Include] = []):
_, parent_pk_name, parent_fks, _ = get_table_fields(
parent, dialect=self.dialect
)
relations = dict()
for include in includes:
fields, pk_name, fks, _ = get_table_fields(
_, parent_pk_name, fks, _ = get_table_fields(
include.model, dialect=self.dialect
)
# if foreign key are {} meaning that we are querying from user
# we need to think about it ??? we select records based on the primary key of the orphan table
# how about subqueries
has_one = include.has == "one"
has_many = include.has == "many"

table_name = include.model._get_table_name().lower()
key = include.model.__name__.lower() if has_one else table_name
if len(fks) == 0:
# this table is a child table meaning that we don't have a foreign key here
fk = parent_fks[table_name]
sql, selected = include.model._get_select_child_by_pk_stm(
dialect=self.dialect,
select=include.select,
parent_pk_name=parent_pk_name,
parent_table_name=parent._get_table_name(),
child_foreign_key_name=fk,
limit=None if has_one else include.limit,
offset=None if has_one else include.offset,
order=None if has_one else include.order,
)
if has_one:
rows = self._execute_sql(sql, args=(pk,), fetchone=has_one)
relations[key] = (
dict(zip(selected, rows)) if rows is not None else None
)
elif has_many:
args = [
arg
for arg in [pk, include.limit, include.offset]
if arg is not None
]
rows = self._execute_sql(sql, args=args, fetchone=has_one)
relations[key] = [dict(zip(selected, row)) for row in rows]

if len(include.include) == 0:
relations = {
**relations,
**self.get_one(
parent=parent, pk=pk, include=include, foreign_keys=fks
),
}
else:
# this table is a parent table. then the child is now the parent
parent_table_name = parent._get_table_name()
fk = fks[parent_table_name]
child_pk_name = parent_pk_name
sql, selected = include.model._get_select_parent_by_pk_stm(
dialect=self.dialect,
select=include.select,
child_pk_name=child_pk_name,
child_table_name=parent._get_table_name(),
parent_fk_name=fk,
limit=None if has_one else include.limit,
offset=None if has_one else include.offset,
order=None if has_one else include.order,
has_one = include.has == "one"
table_name = include.model._get_table_name().lower()
key = include.model.__name__.lower() if has_one else table_name
relations = {
**relations,
**self.get_one(
parent=parent, pk=pk, include=include, foreign_keys=fks
),
}
_, parent_pk_name, parent_fks, _ = get_table_fields(
parent, dialect=self.dialect
)
_pk = relations[key][re.sub(r'`|"', "", parent_pk_name)]

relations[key] = {
**relations[key],
**self.get_find_by_pk_relations(
include.model, _pk, includes=include.include
),
}

return relations

def get_one(
self, parent: Model, include: Include, pk: Any, foreign_keys: list[dict]
):
_, parent_pk_name, parent_fks, _ = get_table_fields(
parent, dialect=self.dialect
)
here = [fk for fk in foreign_keys if parent._get_table_name() in fk]
fks = here[0] if len(here) == 1 else dict()
relations = dict()

has_one = include.has == "one"
has_many = include.has == "many"
table_name = include.model._get_table_name().lower()
key = include.model.__name__.lower() if has_one else table_name
if len(fks) == 0:
here = [fk for fk in parent_fks if include.model._get_table_name() in fk]
parent_fks = dict() if len(here) == 0 else here[0]
# this table is a child table meaning that we don't have a foreign key here
fk = parent_fks[table_name]
sql, selected = include.model._get_select_child_by_pk_stm(
dialect=self.dialect,
select=include.select,
parent_pk_name=parent_pk_name,
parent_table_name=parent._get_table_name(),
child_foreign_key_name=fk,
limit=None if has_one else include.limit,
offset=None if has_one else include.offset,
order=None if has_one else include.order,
)
if has_one:
rows = self._execute_sql(sql, args=(pk,), fetchone=has_one)
relations[key] = dict(zip(selected, rows)) if rows is not None else None
elif has_many:
args = [
arg
for arg in [pk, include.limit, include.offset]
if arg is not None
]
rows = self._execute_sql(sql, args=args, fetchone=has_one)
relations[key] = [dict(zip(selected, row)) for row in rows]

else:
# this table is a parent table. then the child is now the parent
parent_table_name = parent._get_table_name()
fk = fks[parent_table_name]
child_pk_name = parent_pk_name
sql, selected = include.model._get_select_parent_by_pk_stm(
dialect=self.dialect,
select=include.select,
child_pk_name=child_pk_name,
child_table_name=parent._get_table_name(),
parent_fk_name=fk,
limit=None if has_one else include.limit,
offset=None if has_one else include.offset,
order=None if has_one else include.order,
)

if has_one:
rows = self._execute_sql(sql, args=(pk,), fetchone=has_one)
relations[key] = (
dict(zip(selected, rows)) if rows is not None else None
)
elif has_many:
args = [
arg
for arg in [pk, include.limit, include.offset]
if arg is not None
]
rows = self._execute_sql(sql, args=args, fetchall=True)
relations[key] = [dict(zip(selected, row)) for row in rows]
if has_one:
rows = self._execute_sql(sql, args=(pk,), fetchone=has_one)
relations[key] = dict(zip(selected, rows)) if rows is not None else None
elif has_many:
args = [
arg
for arg in [pk, include.limit, include.offset]
if arg is not None
]
rows = self._execute_sql(sql, args=args, fetchall=True)
relations[key] = [dict(zip(selected, row)) for row in rows]

return relations
183 changes: 3 additions & 180 deletions dataloom/types/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,66 +86,6 @@ class Filter:
value: Any = field(repr=False)
join_next_filter_with: Optional[SLQ_OPERAND_LITERAL] = field(default="AND")

def __init__(
self,
column: str,
value: Any,
operator: OPERATOR_LITERAL = "eq",
join_next_filter_with: Optional[SLQ_OPERAND_LITERAL] = "AND",
) -> None:
"""
Filter
------
Constructor method for the Filter class.
Parameters
----------
column : str
The name of the column to filter on.
operator : "eq" |"neq" |"lt" |"gt" |"leq" |"geq" |"in" |"notIn" |"like"
The operator to use for the filter.
value : Any
The value to compare against.
join_next_filter_with : "AND" | "OR" | None, optional
The SQL operand to join the next filter with. Default is "AND".
Returns
-------
None
This method does not return any value.
See Also
--------
ColumnValue : Class for defining column values.
Order : Class for defining order specifications.
Examples
--------
>>> from dataloom import Filter, ColumnValue, Order, User
...
... # Creating a filter for users with id equals 1 or username equals 'miller'
... affected_rows = loom.update_one(
... User,
... filters=[
... Filter(column="id", value=1, operator="eq", join_next_filter_with="OR"),
... Filter(column="username", value="miller"),
... ],
... values=[
... [
... ColumnValue(name="username", value="Mario"),
... ColumnValue(name="name", value="Mario"),
... ]
... ],
... )
... print(affected_rows)
"""
self.column = column
self.value = value
self.join_next_filter_with = join_next_filter_with
self.operator = operator


@dataclass(kw_only=True, repr=False)
class ColumnValue[T]:
Expand Down Expand Up @@ -208,65 +148,6 @@ class ColumnValue[T]:
name: str = field(repr=False)
value: T = field(repr=False)

def __init__(self, name: str, value: T) -> None:
"""
ColumnValue
-----------
Constructor method for the ColumnValue class.
Parameters
----------
name : str
The name of the column.
value : Any
The value to assign to the column.
Returns
-------
None
This method does not return any value.
See Also
--------
Filter : Class for defining filters.
Order : Class for defining order specifications.
Examples
--------
>>> from dataloom import ColumnValue, Filter, Order
...
... # Model definitions
... class User(Model):
... __tablename__: Optional[TableColumn] = TableColumn(name="users")
... id = PrimaryKeyColumn(type="int", auto_increment=True)
... name = Column(type="text", nullable=False)
... username = Column(type="varchar", unique=True, length=255)
...
... class Post(Model):
... __tablename__: Optional[TableColumn] = TableColumn(name="posts")
... id = PrimaryKeyColumn(type="int", auto_increment=True)
... title = Column(type="text", nullable=False)
... content = Column(type="text", nullable=False)
... userId = ForeignKeyColumn(User, maps_to="1-N", type="int", required=False, onDelete="CASCADE", onUpdate="CASCADE")
...
... # Updating the username and name columns for the user with ID 1
... affected_rows = loom.update_one(
... User,
... filters=Filter(column="id", value=1),
... values=[
... [
... ColumnValue(name="username", value="Mario"),
... ColumnValue(name="name", value="Mario"),
... ]
... ],
... )
... print(affected_rows)
"""
self.name = name
self.value = value


@dataclass(kw_only=True, repr=False)
class Order:
Expand Down Expand Up @@ -428,70 +309,12 @@ class Include[Model]:

model: Model = field(repr=False)
order: list[Order] = field(repr=False, default_factory=list)
limit: Optional[int] = field(default=0)
offset: Optional[int] = field(default=0)
limit: Optional[int] = field(default=None)
offset: Optional[int] = field(default=None)
select: Optional[list[str]] = field(default_factory=list)
include: list["Include"] = field(default_factory=list)
has: INCLUDE_LITERAL = field(default="many")

def __init__(
self,
model: Model,
order: list[Order] = [],
limit: Optional[int] = 0,
offset: Optional[int] = 0,
select: Optional[list[str]] = [],
has: INCLUDE_LITERAL = "many",
):
"""
Include
-------
Constructor method for the Include class.
Parameters
----------
model : Model
The model to be included when eger fetching records.
order : list[Order], optional
The list of order specifications for sorting the included data. Default is an empty list.
limit : int | None, optional
The maximum number of records to include. Default is 0 (no limit).
offset : int | None, optional
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).
Returns
-------
None
This method does not return any value.
See Also
--------
Order: Class for defining order specifications.
Filter : Class for defining filters.
ColumnValue : Class for defining column values.
Examples
--------
>>> from dataloom import Include, Model, Order
...
... # Including posts for a user with ID 1
... loom.find_by_pk(
... User, pk=1, include=[Include(Post, limit=2, offset=0, select=["id", "title"], maps_to="1-N")]
... )
"""

self.select = select
self.model = model
self.order = order
self.limit = limit
self.offset = offset
self.has = has


POSTGRES_SQL_TYPES = {
"int": "INTEGER",
Expand Down
Loading

0 comments on commit d0fa850

Please sign in to comment.