Skip to content

Commit 6dfaff0

Browse files
authored
Merge pull request #10 from CrispenGari/relations
Relations
2 parents b2cb16e + c8f673e commit 6dfaff0

24 files changed

+1881
-306
lines changed

Changelog.md

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,82 @@
1+
===
2+
Dataloom **`2.4.0`**
3+
===
4+
5+
### Release Notes - `dataloom`
6+
7+
We have release the new `dataloom` Version `2.4.0` (`2024-02-27`)
8+
9+
##### Features
10+
11+
- `sync` and `connect_and_sync` function can now take in a collection of `Model` or a single `Model` instance.
12+
- Updated documentation.
13+
- Fixing `ForeignKeyColumn` bugs.
14+
- 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.
15+
- Adding the `junction_table` as an argument to the `Include` so that we can use this table as a reference for `N-N` associations.
16+
- Introducing self relations
17+
18+
- now you can define self relations in `dataloom`
19+
20+
```py
21+
class Employee(Model):
22+
__tablename__: TableColumn = TableColumn(name="employees")
23+
id = PrimaryKeyColumn(type="int", auto_increment=True)
24+
name = Column(type="text", nullable=False, default="Bob")
25+
supervisorId = ForeignKeyColumn(
26+
"Employee", maps_to="1-1", type="int", required=False
27+
)
28+
```
29+
30+
- You can also do eager self relations queries
31+
32+
```py
33+
emp_and_sup = mysql_loom.find_by_pk(
34+
instance=Employee,
35+
pk=2,
36+
select=["id", "name", "supervisorId"],
37+
include=Include(
38+
model=Employee,
39+
has="one",
40+
select=["id", "name"],
41+
alias="supervisor",
42+
),
43+
)
44+
print(emp_and_sup) # ? = {'id': 2, 'name': 'Michael Johnson', 'supervisorId': 1, 'supervisor': {'id': 1, 'name': 'John Doe'}}
45+
46+
```
47+
48+
- Introducing `N-N` relationship
49+
50+
- 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.
51+
52+
```py
53+
class Course(Model):
54+
__tablename__: TableColumn = TableColumn(name="courses")
55+
id = PrimaryKeyColumn(type="int", auto_increment=True)
56+
name = Column(type="text", nullable=False, default="Bob")
57+
58+
class Student(Model):
59+
__tablename__: TableColumn = TableColumn(name="students")
60+
id = PrimaryKeyColumn(type="int", auto_increment=True)
61+
name = Column(type="text", nullable=False, default="Bob")
62+
63+
class StudentCourses(Model):
64+
__tablename__: TableColumn = TableColumn(name="students_courses")
65+
studentId = ForeignKeyColumn(table=Student, type="int")
66+
courseId = ForeignKeyColumn(table=Course, type="int")
67+
```
68+
69+
- you can do `eager` data fetching in this type of relationship, however you need to specify the `junction_table`. Here is an example:
70+
71+
```py
72+
english = mysql_loom.find_by_pk(
73+
Course,
74+
pk=engId,
75+
select=["id", "name"],
76+
include=Include(model=Student, junction_table=StudentCourses, has="many"),
77+
)
78+
```
79+
180
===
281
Dataloom **`2.3.0`**
382
===

README.md

Lines changed: 311 additions & 22 deletions
Large diffs are not rendered by default.

dataloom/columns/__init__.py

Lines changed: 12 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
)
1212
from dataclasses import dataclass
1313
from dataloom.exceptions import UnsupportedTypeException, UnsupportedDialectException
14+
from typing import Any
1415

1516

1617
class CreatedAtColumn:
@@ -257,8 +258,8 @@ class ForeignKeyColumn:
257258
258259
Parameters
259260
----------
260-
table : Model
261-
The referenced model to which the foreign key points.
261+
table : Model | str
262+
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.
262263
maps_to : '1-1' | '1-N' | 'N-1' | 'N-N'
263264
The relationship type between the current model and the referenced model. For example, "1-N" for one-to-many.
264265
type : str
@@ -313,7 +314,7 @@ class ForeignKeyColumn:
313314

314315
def __init__(
315316
self,
316-
table,
317+
table: Any | str,
317318
type: MYSQL_SQL_TYPES_LITERAL
318319
| POSTGRES_SQL_TYPES_LITERAL
319320
| SQLITE3_SQL_TYPES_LITERAL,
@@ -393,40 +394,26 @@ def __init__(
393394
def sql_type(self, dialect: DIALECT_LITERAL):
394395
if dialect == "postgres":
395396
if self.type in POSTGRES_SQL_TYPES:
396-
return (
397-
f"{POSTGRES_SQL_TYPES[self.type]}({self.length})"
398-
if self.length
399-
else POSTGRES_SQL_TYPES[self.type]
400-
)
397+
return POSTGRES_SQL_TYPES[self.type]
398+
401399
else:
402400
types = POSTGRES_SQL_TYPES.keys()
403-
raise UnsupportedTypeException(
404-
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
405-
)
401+
raise UnsupportedTypeException(
402+
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
403+
)
406404

407405
elif dialect == "mysql":
408406
if self.type in MYSQL_SQL_TYPES:
409-
if (self.unique or self.default) and self.type == "text":
410-
return f"{MYSQL_SQL_TYPES['varchar']}({self.length if self.length is not None else 255})"
411-
return (
412-
f"{MYSQL_SQL_TYPES[self.type]}({self.length})"
413-
if self.length
414-
else MYSQL_SQL_TYPES[self.type]
415-
)
407+
return MYSQL_SQL_TYPES[self.type]
408+
416409
else:
417410
types = MYSQL_SQL_TYPES.keys()
418411
raise UnsupportedTypeException(
419412
f"Unsupported column type: {self.type} for dialect '{dialect}' supported types are ({', '.join(types)})"
420413
)
421414
elif dialect == "sqlite":
422415
if self.type in SQLITE3_SQL_TYPES:
423-
if self.length and self.type == "text":
424-
return f"{SQLITE3_SQL_TYPES['varchar']}({self.length})"
425-
return (
426-
f"{SQLITE3_SQL_TYPES[self.type]}({self.length})"
427-
if self.length
428-
else SQLITE3_SQL_TYPES[self.type]
429-
)
416+
return SQLITE3_SQL_TYPES[self.type]
430417
else:
431418
types = SQLITE3_SQL_TYPES.keys()
432419
raise UnsupportedTypeException(

dataloom/exceptions/__init__.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@ class InvalidArgumentsException(Exception):
66
pass
77

88

9+
class InvalidReferenceNameException(ValueError):
10+
pass
11+
12+
13+
class IllegalColumnException(ValueError):
14+
pass
15+
16+
917
class InvalidPropertyException(Exception):
1018
pass
1119

@@ -22,6 +30,10 @@ class TooManyPkException(Exception):
2230
pass
2331

2432

33+
class TooManyFkException(Exception):
34+
pass
35+
36+
2537
class UnsupportedDialectException(ValueError):
2638
pass
2739

dataloom/loom/__init__.py

Lines changed: 66 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from dataloom.loom.interfaces import ILoom
2929
from dataloom.loom.math import math
3030
from dataloom.loom.qb import qb
31+
from dataloom.utils import is_collection
3132

3233

3334
class Loom(ILoom):
@@ -1292,7 +1293,7 @@ def connect(
12921293
return self.conn
12931294

12941295
def connect_and_sync(
1295-
self, models: list[Model], drop=False, force=False, alter=False
1296+
self, models: list[Model] | Model, drop=False, force=False, alter=False
12961297
) -> tuple[
12971298
Any | PooledMySQLConnection | MySQLConnectionAbstract | Connection, list[str]
12981299
]:
@@ -1304,8 +1305,8 @@ def connect_and_sync(
13041305
13051306
Parameters
13061307
----------
1307-
models : list[Model]
1308-
A list of Python classes that inherit from a Model class with some Column fields defined as the column names of the table.
1308+
models : list[Model] | Model
1309+
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.
13091310
drop : bool, optional
13101311
Whether or not to drop existing tables when the method is called again. Defaults to False.
13111312
force : bool, optional
@@ -1348,21 +1349,19 @@ def connect_and_sync(
13481349
... conn.close()
13491350
13501351
"""
1351-
try:
1352-
self.conn = self.connect()
1353-
self.sql_obj = SQL(
1354-
conn=self.conn,
1355-
dialect=self.dialect,
1356-
sql_logger=self.sql_logger,
1357-
logs_filename=self.logs_filename,
1358-
)
1359-
tables = self.sync(models=models, drop=drop, force=force, alter=alter)
1360-
return self.conn, tables
1361-
except Exception as e:
1362-
raise Exception(e)
1352+
1353+
self.conn = self.connect()
1354+
self.sql_obj = SQL(
1355+
conn=self.conn,
1356+
dialect=self.dialect,
1357+
sql_logger=self.sql_logger,
1358+
logs_filename=self.logs_filename,
1359+
)
1360+
tables = self.sync(models=models, drop=drop, force=force, alter=alter)
1361+
return self.conn, tables
13631362

13641363
def sync(
1365-
self, models: list[Model], drop=False, force=False, alter=False
1364+
self, models: list[Model] | Model, drop=False, force=False, alter=False
13661365
) -> list[str]:
13671366
"""
13681367
sync
@@ -1372,8 +1371,8 @@ def sync(
13721371
13731372
Parameters
13741373
----------
1375-
models : list[Model]
1376-
A list of Python classes that inherit from a Model class with some Column fields defined as the column names of the table.
1374+
models : list[Model] | Model
1375+
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.
13771376
drop : bool, optional
13781377
Whether or not to drop existing tables before synchronization. Defaults to False.
13791378
force : bool, optional
@@ -1413,52 +1412,59 @@ def sync(
14131412
... )
14141413
14151414
"""
1416-
try:
1417-
for model in models:
1418-
if drop or force:
1415+
if not is_collection(models):
1416+
models = [models]
1417+
1418+
for model in models:
1419+
if force:
1420+
if self.dialect == "mysql":
1421+
# temporarily disable fk checks.
1422+
self._execute_sql("SET FOREIGN_KEY_CHECKS = 0;", _verbose=0)
1423+
self._execute_sql(model._drop_sql(dialect=self.dialect))
1424+
sql = model._create_sql(dialect=self.dialect)
1425+
self._execute_sql(sql)
1426+
self._execute_sql("SET FOREIGN_KEY_CHECKS = 1;", _verbose=0)
1427+
else:
14191428
self._execute_sql(model._drop_sql(dialect=self.dialect))
1420-
for sql in model._create_sql(dialect=self.dialect):
1421-
if sql is not None:
1422-
self._execute_sql(sql)
1423-
elif alter:
1424-
# 1. we only alter the table if it does exists
1425-
# 2. if not we just have to create a new table
1426-
if model._get_table_name() in self.tables:
1427-
sql1 = model._get_describe_stm(
1428-
dialect=self.dialect, fields=["column_name"]
1429-
)
1430-
args = None
1429+
sql = model._create_sql(dialect=self.dialect)
1430+
self._execute_sql(sql)
1431+
elif drop or force:
1432+
self._execute_sql(model._drop_sql(dialect=self.dialect))
1433+
sql = model._create_sql(dialect=self.dialect)
1434+
self._execute_sql(sql)
1435+
elif alter:
1436+
# 1. we only alter the table if it does exists
1437+
# 2. if not we just have to create a new table
1438+
if model._get_table_name() in self.tables:
1439+
sql1 = model._get_describe_stm(
1440+
dialect=self.dialect, fields=["column_name"]
1441+
)
1442+
args = None
1443+
if self.dialect == "mysql":
1444+
args = (self.database, model._get_table_name())
1445+
elif self.dialect == "postgres":
1446+
args = ("public", model._get_table_name())
1447+
elif self.dialect == "sqlite":
1448+
args = ()
1449+
cols = self._execute_sql(sql1, _verbose=0, args=args, fetchall=True)
1450+
if cols is not None:
14311451
if self.dialect == "mysql":
1432-
args = (self.database, model._get_table_name())
1452+
old_columns = [col for (col,) in cols]
14331453
elif self.dialect == "postgres":
1434-
args = ("public", model._get_table_name())
1435-
elif self.dialect == "sqlite":
1436-
args = ()
1437-
cols = self._execute_sql(
1438-
sql1, _verbose=0, args=args, fetchall=True
1439-
)
1440-
if cols is not None:
1441-
if self.dialect == "mysql":
1442-
old_columns = [col for (col,) in cols]
1443-
elif self.dialect == "postgres":
1444-
old_columns = [col for (col,) in cols]
1445-
else:
1446-
old_columns = [col[1] for col in cols]
1447-
sql = model._alter_sql(
1448-
dialect=self.dialect, old_columns=old_columns
1449-
)
1450-
self._execute_sql(sql, _is_script=True)
1451-
else:
1452-
for sql in model._create_sql(dialect=self.dialect):
1453-
if sql is not None:
1454-
self._execute_sql(sql)
1454+
old_columns = [col for (col,) in cols]
1455+
else:
1456+
old_columns = [col[1] for col in cols]
1457+
sql = model._alter_sql(
1458+
dialect=self.dialect, old_columns=old_columns
1459+
)
1460+
self._execute_sql(sql, _is_script=True)
14551461
else:
1456-
for sql in model._create_sql(dialect=self.dialect):
1457-
if sql is not None:
1458-
self._execute_sql(sql)
1459-
return self.tables
1460-
except Exception as e:
1461-
raise Exception(e)
1462+
sql = model._create_sql(dialect=self.dialect)
1463+
self._execute_sql(sql)
1464+
else:
1465+
sql = model._create_sql(dialect=self.dialect)
1466+
self._execute_sql(sql)
1467+
return self.tables
14621468

14631469
def sum(
14641470
self,

0 commit comments

Comments
 (0)