FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

First pass at adding support for foreign keys · NeoTim/python-spanner-orm@3f72113 · GitHub

Commit 3f72113

Browse files
committed
First pass at adding support for foreign keys
1 parent 954de67 commit 3f72113

7 files changed

Lines changed: 199 additions & 2 deletions

File tree

‎spanner_orm/admin/update.py‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,13 +49,24 @@ def __init__(self, model_: Type[model.Model]):
4949
self._model = model_
5050

5151
def ddl(self) -> str:
52-
fields = [
52+
key_fields = [
5353
'{} {}'.format(name, field.ddl())
5454
for name, field in self._model.fields.items()
5555
]
56+
key_fields_ddl = ', '.join(key_fields)
57+
if self._model.foreign_key_relations:
58+
fk = list(self._model.foreign_key_relations.values())[0]
59+
for referencing_table_col, referenced_table_col in fk.constraints.items():
60+
key_fields_ddl += (
61+
', FOREIGN KEY ({referencing_table_col}) REFERENCES'
62+
' {parent} ({referenced_table_col})').format(
63+
parent=fk.destination,
64+
referencing_table_col=referencing_table_col,
65+
referenced_table_col=referenced_table_col,
66+
)
5667
index_ddl = 'PRIMARY KEY ({})'.format(', '.join(self._model.primary_keys))
5768
statement = 'CREATE TABLE {} ({}) {}'.format(self._model.table,
58-
', '.join(fields), index_ddl)
69+
key_fields_ddl, index_ddl)
5970

6071
if self._model.interleaved:
6172
statement += ', INTERLEAVE IN PARENT {parent} ON DELETE CASCADE'.format(
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
# python3
2+
# Copyright 2019 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Helps define a foreign key relationship between two models."""
16+
17+
from typing import Any, List, Mapping, Type, Union
18+
19+
import dataclasses
20+
from spanner_orm import error
21+
from spanner_orm import registry
22+
23+
24+
@dataclasses.dataclass
25+
class RelationshipConstraint:
26+
destination_class: Type[Any]
27+
destination_column: str
28+
origin_class: Type[Any]
29+
origin_column: str
30+
31+
32+
class ForeignKeyRelationship(object):
33+
"""Helps define a foreign key relationship between two models."""
34+
35+
def __init__(self,
36+
referenced_table_name: str,
37+
constraints: Mapping[str, str]):
38+
"""Creates a ForeignKeyRelationship.
39+
40+
Args:
41+
referenced_table_name: Destination model class or fully qualified class
42+
name of the destination model.
43+
constraints: Dictionary where the keys are names of columns from the
44+
referencing table and the values are the names of the columns in the
45+
referenced table.
46+
"""
47+
self.origin = None
48+
self.name = None
49+
self._referenced_table_name = referenced_table_name
50+
self._constraints = constraints
51+
52+
@property
53+
def constraints(self) -> List[RelationshipConstraint]:
54+
return self._constraints
55+
56+
@property
57+
def destination(self) -> Type[Any]:
58+
return self._referenced_table_name
59+
if not self._destination:
60+
self._destination = registry.model_registry().get(
61+
self._referenced_table_name)
62+
return self._destination
63+
64+
@property
65+
def single(self) -> bool:
66+
return self._single
67+
68+
def _parse_constraints(self) -> List[RelationshipConstraint]:
69+
"""Validates the dictionary of constraints and turns it into Conditions."""
70+
constraints = []
71+
for origin_column, destination_column in self._constraints.items():
72+
if origin_column not in self.origin.fields:
73+
raise error.ValidationError(
74+
'Origin column must be present in origin model')
75+
76+
if destination_column not in self.destination.fields:
77+
raise error.ValidationError(
78+
'Destination column must be present in destination model')
79+
80+
# TODO(dbrandao): remove when pytype #234 is fixed
81+
constraints.append(
82+
RelationshipConstraint(self.destination, destination_column,
83+
self.origin, origin_column)) # type: ignore
84+
85+
return constraints

‎spanner_orm/metadata.py‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232

3333
from spanner_orm import error
3434
from spanner_orm import field
35+
from spanner_orm import foreign_key_relationship
3536
from spanner_orm import index
3637
from spanner_orm import registry
3738
from spanner_orm import relationship
@@ -44,6 +45,7 @@ def __init__(self,
4445
table: Optional[str] = None,
4546
fields: Optional[Dict[str, field.Field]] = None,
4647
relations: Optional[Dict[str, relationship.Relationship]] = None,
48+
foreign_key_relations: Optional[Dict[str, foreign_key_relationship.ForeignKeyRelationship]] = None,
4749
indexes: Optional[Dict[str, index.Index]] = None,
4850
interleaved: Optional[str] = None,
4951
model_class: Optional[Type[Any]] = None):
@@ -55,6 +57,7 @@ def __init__(self,
5557
self.model_class = model_class
5658
self.primary_keys = []
5759
self.relations = dict(relations or {})
60+
self.foreign_key_relations = dict(foreign_key_relations or {})
5861
self.table = table or ''
5962

6063
def finalize(self) -> None:
@@ -101,6 +104,14 @@ def add_relation(self, name: str,
101104
new_relation.name = name
102105
self.relations[name] = new_relation
103106

107+
def add_foreign_key_relation(
108+
self,
109+
name: str,
110+
new_relation: foreign_key_relationship.ForeignKeyRelationship,
111+
) -> None:
112+
new_relation.name = name
113+
self.foreign_key_relations[name] = new_relation
114+
104115
def add_index(self, name: str, new_index: index.Index) -> None:
105116
new_index.name = name
106117
self.indexes[name] = new_index

‎spanner_orm/model.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
from spanner_orm import api
2222
from spanner_orm import condition
2323
from spanner_orm import error
24+
from spanner_orm import foreign_key_relationship
2425
from spanner_orm import field
2526
from spanner_orm import index
2627
from spanner_orm import metadata
@@ -52,12 +53,19 @@ def __new__(mcs, name: str, bases: Any, attrs: Dict[str, Any], **kwargs: Any):
5253
model_metadata.table = value
5354
elif key == '__interleaved__':
5455
model_metadata.interleaved = value
56+
elif key == '__foreign_key__':
57+
model_metadata.foreign_key = value
5558
if isinstance(value, field.Field):
5659
model_metadata.add_field(key, value)
5760
elif isinstance(value, index.Index):
5861
model_metadata.add_index(key, value)
5962
elif isinstance(value, relationship.Relationship):
6063
model_metadata.add_relation(key, value)
64+
elif isinstance(
65+
value,
66+
foreign_key_relationship.ForeignKeyRelationship,
67+
):
68+
model_metadata.add_foreign_key_relation(key, value)
6169
else:
6270
non_model_attrs[key] = value
6371

@@ -112,6 +120,14 @@ def primary_keys(cls) -> List[str]:
112120
def relations(cls) -> Dict[str, relationship.Relationship]:
113121
return cls.meta.relations
114122

123+
@property
124+
def foreign_key_relations(cls) -> Dict[str, foreign_key_relationship.ForeignKeyRelationship]:
125+
return cls.meta.foreign_key_relations
126+
#if cls.meta.foreign_key:
127+
# return registry.model_registry().get(cls.meta.foreign_key)
128+
#return None
129+
130+
115131
@property
116132
def fields(cls) -> Dict[str, field.Field]:
117133
return cls.meta.fields
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Lint as: python3
2+
# Copyright 2020 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
"""Creates table with SmallTestModel.
16+
17+
Migration ID: 'f735d6b706d3'
18+
Created: 2020-07-10 16:24
19+
"""
20+
21+
import spanner_orm
22+
from spanner_orm import field
23+
24+
migration_id = 'f735d6b706d3'
25+
prev_migration_id = 'f735d6b706d2'
26+
27+
28+
class OriginalForeignKeyTestModelTable(spanner_orm.model.Model):
29+
"""ORM Model with the original schema for the ForeignKeyTestModel table."""
30+
31+
__table__ = 'ForeignKeyTestModel'
32+
__foreign_key__ = 'SmallTestModel'
33+
key = field.Field(field.String, primary_key=True)
34+
child_key = field.Field(field.String, primary_key=True)
35+
36+
37+
def upgrade() -> spanner_orm.CreateTable:
38+
"""See ORM migrations interface."""
39+
return spanner_orm.CreateTable(OriginalForeignKeyTestModelTable)
40+
41+
42+
def downgrade() -> spanner_orm.DropTable:
43+
"""See ORM migrations interface."""
44+
return spanner_orm.DropTable(OriginalForeignKeyTestModelTable.__table__)

‎spanner_orm/tests/models.py‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""Models used by unit tests."""
1616

1717
from spanner_orm import field
18+
from spanner_orm import foreign_key_relationship
1819
from spanner_orm import index
1920
from spanner_orm import model
2021
from spanner_orm import relationship
@@ -61,6 +62,18 @@ class RelationshipTestModel(model.Model):
6162
parents = relationship.Relationship('spanner_orm.tests.models.SmallTestModel',
6263
{'parent_key': 'key'})
6364

65+
class ForeignKeyTestModel(model.Model):
66+
"""Model class for testing foreign keys."""
67+
68+
__table__ = 'ForeignKeyTestModel'
69+
# __foreign_key__ = 'SmallTestModel'
70+
referencing_key = field.Field(field.String, primary_key=True)
71+
value = field.Field(field.String)
72+
foreign_key_relationship = foreign_key_relationship.ForeignKeyRelationship(
73+
'SmallTestModel', {'referencing_key': 'key'})
74+
# single=True)
75+
#parents = relationship.Relationship('spanner_orm.tests.models.SmallTestModel',
76+
# {'parent_key': 'key'})
6477

6578
class InheritanceTestModel(SmallTestModel):
6679
"""Model class used for testing model inheritance."""

‎spanner_orm/tests/update_test.py‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,23 @@ def test_create_table_interleaved(self, get_model):
8686
'INTERLEAVE IN PARENT SmallTestModel ON DELETE CASCADE')
8787
self.assertEqual(test_update.ddl(), test_model_ddl)
8888

89+
@mock.patch('spanner_orm.admin.metadata.SpannerMetadata.model')
90+
def test_create_table_foreign_key(self, get_model):
91+
self.maxDiff = 1000
92+
93+
get_model.return_value = None
94+
new_model = models.ForeignKeyTestModel
95+
test_update = update.CreateTable(new_model)
96+
test_update.validate()
97+
98+
test_model_ddl = (
99+
'CREATE TABLE ForeignKeyTestModel ('
100+
'referencing_key STRING(MAX) NOT NULL, '
101+
'value STRING(MAX) NOT NULL, '
102+
'FOREIGN KEY (referencing_key) REFERENCES SmallTestModel (key)) '
103+
'PRIMARY KEY (referencing_key)')
104+
self.assertEqual(test_update.ddl(), test_model_ddl)
105+
89106
@mock.patch('spanner_orm.admin.metadata.SpannerMetadata.model')
90107
def test_create_table_error_on_existing_table(self, get_model):
91108
get_model.return_value = models.SmallTestModel

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL