| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -49,13 +49,24 @@ def __init__(self, model_: Type[model.Model]): | |||
| 49 | 49 | self._model = model_ | |
| 50 | 50 | ||
| 51 | 51 | def ddl(self) -> str: | |
| 52 | - fields = [ | ||
| 52 | + key_fields = [ | ||
| 53 | 53 | '{} {}'.format(name, field.ddl()) | |
| 54 | 54 | for name, field in self._model.fields.items() | |
| 55 | 55 | ] | |
| 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 | + ) | ||
| 56 | 67 | index_ddl = 'PRIMARY KEY ({})'.format(', '.join(self._model.primary_keys)) | |
| 57 | 68 | statement = 'CREATE TABLE {} ({}) {}'.format(self._model.table, | |
| 58 | - ', '.join(fields), index_ddl) | ||
| 69 | + key_fields_ddl, index_ddl) | ||
| 59 | 70 | ||
| 60 | 71 | if self._model.interleaved: | |
| 61 | 72 | statement += ', INTERLEAVE IN PARENT {parent} ON DELETE CASCADE'.format( | |
| Original file line number | Diff line number | Diff 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 | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -32,6 +32,7 @@ | |||
| 32 | 32 | ||
| 33 | 33 | from spanner_orm import error | |
| 34 | 34 | from spanner_orm import field | |
| 35 | + from spanner_orm import foreign_key_relationship | ||
| 35 | 36 | from spanner_orm import index | |
| 36 | 37 | from spanner_orm import registry | |
| 37 | 38 | from spanner_orm import relationship | |
@@ -44,6 +45,7 @@ def __init__(self, | |||
| 44 | 45 | table: Optional[str] = None, | |
| 45 | 46 | fields: Optional[Dict[str, field.Field]] = None, | |
| 46 | 47 | relations: Optional[Dict[str, relationship.Relationship]] = None, | |
| 48 | + foreign_key_relations: Optional[Dict[str, foreign_key_relationship.ForeignKeyRelationship]] = None, | ||
| 47 | 49 | indexes: Optional[Dict[str, index.Index]] = None, | |
| 48 | 50 | interleaved: Optional[str] = None, | |
| 49 | 51 | model_class: Optional[Type[Any]] = None): | |
@@ -55,6 +57,7 @@ def __init__(self, | |||
| 55 | 57 | self.model_class = model_class | |
| 56 | 58 | self.primary_keys = [] | |
| 57 | 59 | self.relations = dict(relations or {}) | |
| 60 | + self.foreign_key_relations = dict(foreign_key_relations or {}) | ||
| 58 | 61 | self.table = table or '' | |
| 59 | 62 | ||
| 60 | 63 | def finalize(self) -> None: | |
@@ -101,6 +104,14 @@ def add_relation(self, name: str, | |||
| 101 | 104 | new_relation.name = name | |
| 102 | 105 | self.relations[name] = new_relation | |
| 103 | 106 | ||
| 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 | + | ||
| 104 | 115 | def add_index(self, name: str, new_index: index.Index) -> None: | |
| 105 | 116 | new_index.name = name | |
| 106 | 117 | self.indexes[name] = new_index | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -21,6 +21,7 @@ | |||
| 21 | 21 | from spanner_orm import api | |
| 22 | 22 | from spanner_orm import condition | |
| 23 | 23 | from spanner_orm import error | |
| 24 | + from spanner_orm import foreign_key_relationship | ||
| 24 | 25 | from spanner_orm import field | |
| 25 | 26 | from spanner_orm import index | |
| 26 | 27 | from spanner_orm import metadata | |
@@ -52,12 +53,19 @@ def __new__(mcs, name: str, bases: Any, attrs: Dict[str, Any], **kwargs: Any): | |||
| 52 | 53 | model_metadata.table = value | |
| 53 | 54 | elif key == '__interleaved__': | |
| 54 | 55 | model_metadata.interleaved = value | |
| 56 | + elif key == '__foreign_key__': | ||
| 57 | + model_metadata.foreign_key = value | ||
| 55 | 58 | if isinstance(value, field.Field): | |
| 56 | 59 | model_metadata.add_field(key, value) | |
| 57 | 60 | elif isinstance(value, index.Index): | |
| 58 | 61 | model_metadata.add_index(key, value) | |
| 59 | 62 | elif isinstance(value, relationship.Relationship): | |
| 60 | 63 | 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) | ||
| 61 | 69 | else: | |
| 62 | 70 | non_model_attrs[key] = value | |
| 63 | 71 | ||
@@ -112,6 +120,14 @@ def primary_keys(cls) -> List[str]: | |||
| 112 | 120 | def relations(cls) -> Dict[str, relationship.Relationship]: | |
| 113 | 121 | return cls.meta.relations | |
| 114 | 122 | ||
| 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 | + | ||
| 115 | 131 | @property | |
| 116 | 132 | def fields(cls) -> Dict[str, field.Field]: | |
| 117 | 133 | return cls.meta.fields | |
| Original file line number | Diff line number | Diff 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__) | ||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -15,6 +15,7 @@ | |||
| 15 | 15 | """Models used by unit tests.""" | |
| 16 | 16 | ||
| 17 | 17 | from spanner_orm import field | |
| 18 | + from spanner_orm import foreign_key_relationship | ||
| 18 | 19 | from spanner_orm import index | |
| 19 | 20 | from spanner_orm import model | |
| 20 | 21 | from spanner_orm import relationship | |
@@ -61,6 +62,18 @@ class RelationshipTestModel(model.Model): | |||
| 61 | 62 | parents = relationship.Relationship('spanner_orm.tests.models.SmallTestModel', | |
| 62 | 63 | {'parent_key': 'key'}) | |
| 63 | 64 | ||
| 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'}) | ||
| 64 | 77 | ||
| 65 | 78 | class InheritanceTestModel(SmallTestModel): | |
| 66 | 79 | """Model class used for testing model inheritance.""" | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -86,6 +86,23 @@ def test_create_table_interleaved(self, get_model): | |||
| 86 | 86 | 'INTERLEAVE IN PARENT SmallTestModel ON DELETE CASCADE') | |
| 87 | 87 | self.assertEqual(test_update.ddl(), test_model_ddl) | |
| 88 | 88 | ||
| 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 | + | ||
| 89 | 106 | @mock.patch('spanner_orm.admin.metadata.SpannerMetadata.model') | |
| 90 | 107 | def test_create_table_error_on_existing_table(self, get_model): | |
| 91 | 108 | get_model.return_value = models.SmallTestModel | |
| Back | FazBrowse Home | New Git URL |
0 commit comments