Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Relations #10

Merged
merged 8 commits into from
Feb 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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