Skip to content

Commit

Permalink
Merge pull request #10 from CrispenGari/relations
Browse files Browse the repository at this point in the history
Relations
  • Loading branch information
CrispenGari authored Feb 27, 2024
2 parents b2cb16e + c8f673e commit 6dfaff0
Show file tree
Hide file tree
Showing 24 changed files with 1,881 additions and 306 deletions.
79 changes: 79 additions & 0 deletions Changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,82 @@
===
Dataloom **`2.4.0`**
===

### Release Notes - `dataloom`

We have release the new `dataloom` Version `2.4.0` (`2024-02-27`)

##### Features

- `sync` and `connect_and_sync` function can now take in a collection of `Model` or a single `Model` instance.
- Updated documentation.
- Fixing `ForeignKeyColumn` bugs.
- Adding the `alias` as an argument to `Include` class so that developers can flexibly use their own alias for eager model inclusion rather than letting `dataloom` decide for them.
- Adding the `junction_table` as an argument to the `Include` so that we can use this table as a reference for `N-N` associations.
- Introducing self relations

- now you can define self relations in `dataloom`

```py
class Employee(Model):
__tablename__: TableColumn = TableColumn(name="employees")
id = PrimaryKeyColumn(type="int", auto_increment=True)
name = Column(type="text", nullable=False, default="Bob")
supervisorId = ForeignKeyColumn(
"Employee", maps_to="1-1", type="int", required=False
)
```

- You can also do eager self relations queries

```py
emp_and_sup = mysql_loom.find_by_pk(
instance=Employee,
pk=2,
select=["id", "name", "supervisorId"],
include=Include(
model=Employee,
has="one",
select=["id", "name"],
alias="supervisor",
),
)
print(emp_and_sup) # ? = {'id': 2, 'name': 'Michael Johnson', 'supervisorId': 1, 'supervisor': {'id': 1, 'name': 'John Doe'}}

```

- Introducing `N-N` relationship

- with this version of `dataloom` `n-n` relationships are now available. However you will need to define a reference table manual. We recommend you to follow our documentation to get the best out of it.

```py
class Course(Model):
__tablename__: TableColumn = TableColumn(name="courses")
id = PrimaryKeyColumn(type="int", auto_increment=True)
name = Column(type="text", nullable=False, default="Bob")

class Student(Model):
__tablename__: TableColumn = TableColumn(name="students")
id = PrimaryKeyColumn(type="int", auto_increment=True)
name = Column(type="text", nullable=False, default="Bob")

class StudentCourses(Model):
__tablename__: TableColumn = TableColumn(name="students_courses")
studentId = ForeignKeyColumn(table=Student, type="int")
courseId = ForeignKeyColumn(table=Course, type="int")
```

- you can do `eager` data fetching in this type of relationship, however you need to specify the `junction_table`. Here is an example:

```py
english = mysql_loom.find_by_pk(
Course,
pk=engId,
select=["id", "name"],
include=Include(model=Student, junction_table=StudentCourses, has="many"),
)
```

===
Dataloom **`2.3.0`**
===
Expand Down
333 changes: 311 additions & 22 deletions README.md

Large diffs are not rendered by default.

37 changes: 12 additions & 25 deletions dataloom/columns/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
)
from dataclasses import dataclass
from dataloom.exceptions import UnsupportedTypeException, UnsupportedDialectException
from typing import Any


class CreatedAtColumn:
Expand Down Expand Up @@ -257,8 +258,8 @@ class ForeignKeyColumn:
Parameters
----------
table : Model
The referenced model to which the foreign key points.
table : Model | str
The referenced model to which the foreign key points. It takes in a model or a string, string only if you are trying to map self relations.
maps_to : '1-1' | '1-N' | 'N-1' | 'N-N'
The relationship type between the current model and the referenced model. For example, "1-N" for one-to-many.
type : str
Expand Down Expand Up @@ -313,7 +314,7 @@ class ForeignKeyColumn:

def __init__(
self,
table,
table: Any | str,
type: MYSQL_SQL_TYPES_LITERAL
| POSTGRES_SQL_TYPES_LITERAL
| SQLITE3_SQL_TYPES_LITERAL,
Expand Down Expand Up @@ -393,40 +394,26 @@ def __init__(
def sql_type(self, dialect: DIALECT_LITERAL):
if dialect == "postgres":
if self.type in POSTGRES_SQL_TYPES:
return (
f"{POSTGRES_SQL_TYPES[self.type]}({self.length})"
if self.length
else POSTGRES_SQL_TYPES[self.type]
)
return POSTGRES_SQL_TYPES[self.type]

else:
types = POSTGRES_SQL_TYPES.keys()
raise UnsupportedTypeException(
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
)
raise UnsupportedTypeException(
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
)

elif dialect == "mysql":
if self.type in MYSQL_SQL_TYPES:
if (self.unique or self.default) and self.type == "text":
return f"{MYSQL_SQL_TYPES['varchar']}({self.length if self.length is not None else 255})"
return (
f"{MYSQL_SQL_TYPES[self.type]}({self.length})"
if self.length
else MYSQL_SQL_TYPES[self.type]
)
return MYSQL_SQL_TYPES[self.type]

else:
types = MYSQL_SQL_TYPES.keys()
raise UnsupportedTypeException(
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
)
elif dialect == "sqlite":
if self.type in SQLITE3_SQL_TYPES:
if self.length and self.type == "text":
return f"{SQLITE3_SQL_TYPES['varchar']}({self.length})"
return (
f"{SQLITE3_SQL_TYPES[self.type]}({self.length})"
if self.length
else SQLITE3_SQL_TYPES[self.type]
)
return SQLITE3_SQL_TYPES[self.type]
else:
types = SQLITE3_SQL_TYPES.keys()
raise UnsupportedTypeException(
Expand Down
12 changes: 12 additions & 0 deletions dataloom/exceptions/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,14 @@ class InvalidArgumentsException(Exception):
pass


class InvalidReferenceNameException(ValueError):
pass


class IllegalColumnException(ValueError):
pass


class InvalidPropertyException(Exception):
pass

Expand All @@ -22,6 +30,10 @@ class TooManyPkException(Exception):
pass


class TooManyFkException(Exception):
pass


class UnsupportedDialectException(ValueError):
pass

Expand Down
126 changes: 66 additions & 60 deletions dataloom/loom/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
from dataloom.loom.interfaces import ILoom
from dataloom.loom.math import math
from dataloom.loom.qb import qb
from dataloom.utils import is_collection


class Loom(ILoom):
Expand Down Expand Up @@ -1292,7 +1293,7 @@ def connect(
return self.conn

def connect_and_sync(
self, models: list[Model], drop=False, force=False, alter=False
self, models: list[Model] | Model, drop=False, force=False, alter=False
) -> tuple[
Any | PooledMySQLConnection | MySQLConnectionAbstract | Connection, list[str]
]:
Expand All @@ -1304,8 +1305,8 @@ def connect_and_sync(
Parameters
----------
models : list[Model]
A list of Python classes that inherit from a Model class with some Column fields defined as the column names of the table.
models : list[Model] | Model
A collection of Python classes or a Python Class that inherit from a Model class with some Column fields defined as the column names of the table.
drop : bool, optional
Whether or not to drop existing tables when the method is called again. Defaults to False.
force : bool, optional
Expand Down Expand Up @@ -1348,21 +1349,19 @@ def connect_and_sync(
... conn.close()
"""
try:
self.conn = self.connect()
self.sql_obj = SQL(
conn=self.conn,
dialect=self.dialect,
sql_logger=self.sql_logger,
logs_filename=self.logs_filename,
)
tables = self.sync(models=models, drop=drop, force=force, alter=alter)
return self.conn, tables
except Exception as e:
raise Exception(e)

self.conn = self.connect()
self.sql_obj = SQL(
conn=self.conn,
dialect=self.dialect,
sql_logger=self.sql_logger,
logs_filename=self.logs_filename,
)
tables = self.sync(models=models, drop=drop, force=force, alter=alter)
return self.conn, tables

def sync(
self, models: list[Model], drop=False, force=False, alter=False
self, models: list[Model] | Model, drop=False, force=False, alter=False
) -> list[str]:
"""
sync
Expand All @@ -1372,8 +1371,8 @@ def sync(
Parameters
----------
models : list[Model]
A list of Python classes that inherit from a Model class with some Column fields defined as the column names of the table.
models : list[Model] | Model
A collection of Python classes or a Python Class that inherit from a Model class with some Column fields defined as the column names of the table.
drop : bool, optional
Whether or not to drop existing tables before synchronization. Defaults to False.
force : bool, optional
Expand Down Expand Up @@ -1413,52 +1412,59 @@ def sync(
... )
"""
try:
for model in models:
if drop or force:
if not is_collection(models):
models = [models]

for model in models:
if force:
if self.dialect == "mysql":
# temporarily disable fk checks.
self._execute_sql("SET FOREIGN_KEY_CHECKS = 0;", _verbose=0)
self._execute_sql(model._drop_sql(dialect=self.dialect))
sql = model._create_sql(dialect=self.dialect)
self._execute_sql(sql)
self._execute_sql("SET FOREIGN_KEY_CHECKS = 1;", _verbose=0)
else:
self._execute_sql(model._drop_sql(dialect=self.dialect))
for sql in model._create_sql(dialect=self.dialect):
if sql is not None:
self._execute_sql(sql)
elif alter:
# 1. we only alter the table if it does exists
# 2. if not we just have to create a new table
if model._get_table_name() in self.tables:
sql1 = model._get_describe_stm(
dialect=self.dialect, fields=["column_name"]
)
args = None
sql = model._create_sql(dialect=self.dialect)
self._execute_sql(sql)
elif drop or force:
self._execute_sql(model._drop_sql(dialect=self.dialect))
sql = model._create_sql(dialect=self.dialect)
self._execute_sql(sql)
elif alter:
# 1. we only alter the table if it does exists
# 2. if not we just have to create a new table
if model._get_table_name() in self.tables:
sql1 = model._get_describe_stm(
dialect=self.dialect, fields=["column_name"]
)
args = None
if self.dialect == "mysql":
args = (self.database, model._get_table_name())
elif self.dialect == "postgres":
args = ("public", model._get_table_name())
elif self.dialect == "sqlite":
args = ()
cols = self._execute_sql(sql1, _verbose=0, args=args, fetchall=True)
if cols is not None:
if self.dialect == "mysql":
args = (self.database, model._get_table_name())
old_columns = [col for (col,) in cols]
elif self.dialect == "postgres":
args = ("public", model._get_table_name())
elif self.dialect == "sqlite":
args = ()
cols = self._execute_sql(
sql1, _verbose=0, args=args, fetchall=True
)
if cols is not None:
if self.dialect == "mysql":
old_columns = [col for (col,) in cols]
elif self.dialect == "postgres":
old_columns = [col for (col,) in cols]
else:
old_columns = [col[1] for col in cols]
sql = model._alter_sql(
dialect=self.dialect, old_columns=old_columns
)
self._execute_sql(sql, _is_script=True)
else:
for sql in model._create_sql(dialect=self.dialect):
if sql is not None:
self._execute_sql(sql)
old_columns = [col for (col,) in cols]
else:
old_columns = [col[1] for col in cols]
sql = model._alter_sql(
dialect=self.dialect, old_columns=old_columns
)
self._execute_sql(sql, _is_script=True)
else:
for sql in model._create_sql(dialect=self.dialect):
if sql is not None:
self._execute_sql(sql)
return self.tables
except Exception as e:
raise Exception(e)
sql = model._create_sql(dialect=self.dialect)
self._execute_sql(sql)
else:
sql = model._create_sql(dialect=self.dialect)
self._execute_sql(sql)
return self.tables

def sum(
self,
Expand Down
Loading

0 comments on commit 6dfaff0

Please sign in to comment.