diff --git a/spanner_orm/admin/column.py b/spanner_orm/admin/column.py index b559d4e..21036d2 100644 --- a/spanner_orm/admin/column.py +++ b/spanner_orm/admin/column.py @@ -33,9 +33,11 @@ class ColumnSchema(schema.InformationSchema): is_nullable = field.Field(field.String) spanner_type = field.Field(field.String) + @property def nullable(self) -> bool: return self.is_nullable == 'YES' + @property def field_type(self) -> Type[field.FieldType]: for field_type in field.ALL_TYPES: if self.spanner_type == field_type.ddl(): diff --git a/spanner_orm/admin/metadata.py b/spanner_orm/admin/metadata.py index 4156878..a1c4a3d 100644 --- a/spanner_orm/admin/metadata.py +++ b/spanner_orm/admin/metadata.py @@ -76,7 +76,7 @@ def tables(cls) -> Dict[str, Dict[str, Any]]: condition.equal_to('table_schema', '')) for column_row in columns: new_field = field.Field( - column_row.field_type(), nullable=column_row.nullable()) + column_row.field_type, nullable=column_row.nullable) new_field.name = column_row.column_name new_field.position = column_row.ordinal_position column_data[column_row.table_name][column_row.column_name] = new_field diff --git a/spanner_orm/admin/update.py b/spanner_orm/admin/update.py index 4277e10..0440931 100644 --- a/spanner_orm/admin/update.py +++ b/spanner_orm/admin/update.py @@ -50,8 +50,8 @@ def __init__(self, model_: Type[model.Model]): def ddl(self) -> str: fields = [ - '{} {}'.format(name, field.ddl()) - for name, field in self._model.fields.items() + '{} {}'.format(field.name, field.ddl()) + for field in self._model.fields.values() ] index_ddl = 'PRIMARY KEY ({})'.format(', '.join(self._model.primary_keys)) statement = 'CREATE TABLE {} ({}) {}'.format(self._model.table, @@ -154,9 +154,9 @@ def validate(self) -> None: model_ = metadata.SpannerMetadata.model(self._table) if not model_: raise error.SpannerError('Table {} does not exist'.format(self._table)) - if not self._field.nullable(): + if not self._field.nullable: raise error.SpannerError('Column {} is not nullable'.format(self._column)) - if self._field.primary_key(): + if self._field.primary_key: raise error.SpannerError('Column {} is a primary key'.format( self._column)) @@ -218,10 +218,10 @@ def validate(self) -> None: old_field = model_.fields[self._column] # Validate that the only alteration is to change column nullability - if self._field.field_type() != old_field.field_type(): + if self._field.field_type != old_field.field_type: raise error.SpannerError('Column {} is changing type'.format( self._column)) - if self._field.nullable() == old_field.nullable(): + if self._field.nullable == old_field.nullable: raise error.SpannerError('Column {} has no changes'.format(self._column)) diff --git a/spanner_orm/condition.py b/spanner_orm/condition.py index 0174930..e82ea14 100644 --- a/spanner_orm/condition.py +++ b/spanner_orm/condition.py @@ -151,8 +151,8 @@ def _validate(self, model_class: Type[Any]) -> None: self.destination_column, self.destination_model_class.table)) dest = self.destination_model_class.fields[self.destination_column] - if (origin.field_type() != dest.field_type() or - origin.nullable() != dest.nullable()): + if (origin.field_type != dest.field_type or + origin.nullable != dest.nullable): raise error.ValidationError('Types of {} and {} do not match'.format( origin.name, dest.name)) diff --git a/spanner_orm/field.py b/spanner_orm/field.py index 64f40c6..87dbf6d 100644 --- a/spanner_orm/field.py +++ b/spanner_orm/field.py @@ -16,7 +16,7 @@ import abc import datetime -from typing import Any, Type +from typing import Any, Type, Optional from spanner_orm import error @@ -48,8 +48,9 @@ class Field(object): def __init__(self, field_type: Type[FieldType], nullable: bool = False, - primary_key: bool = False): - self.name = None + primary_key: bool = False, + name: Optional[str] = None): + self._name = name self._type = field_type self._nullable = nullable self._primary_key = primary_key @@ -59,18 +60,30 @@ def ddl(self) -> str: return self._type.ddl() return '{field_type} NOT NULL'.format(field_type=self._type.ddl()) + @property def field_type(self) -> Type[FieldType]: return self._type def grpc_type(self) -> str: return self._type.grpc_type() + @property def nullable(self) -> bool: return self._nullable + @property def primary_key(self) -> bool: return self._primary_key + @property + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, value: str) -> None: + if not self._name: + self._name = value + def validate(self, value) -> None: if value is None: if not self._nullable: diff --git a/spanner_orm/index.py b/spanner_orm/index.py index 1a1ac35..1e785b4 100644 --- a/spanner_orm/index.py +++ b/spanner_orm/index.py @@ -28,15 +28,45 @@ def __init__(self, parent: Optional[str] = None, null_filtered: bool = False, unique: bool = False, - storing_columns: Optional[List[str]] = None): + storing_columns: Optional[List[str]] = None, + name: Optional[str] = None): if not columns: raise error.ValidationError('An index must have at least one column') - self.columns = columns - self.name = None - self.parent = parent - self.null_filtered = null_filtered - self.unique = unique - self.storing_columns = storing_columns or [] + self._columns = columns + self._name = name + self._parent = parent + self._null_filtered = null_filtered + self._unique = unique + self._storing_columns = storing_columns or [] + + @property + def columns(self) -> List[str]: + return self._columns + + @property + def name(self) -> Optional[str]: + return self._name + + @name.setter + def name(self, value: str) -> None: + if not self._name: + self._name = value + + @property + def parent(self) -> Optional[str]: + return self._parent + + @property + def null_filtered(self) -> bool: + return self._null_filtered + + @property + def unique(self) -> bool: + return self._unique + + @property + def storing_columns(self) -> List[str]: + return self._storing_columns @property def primary(self) -> bool: diff --git a/spanner_orm/metadata.py b/spanner_orm/metadata.py index 8c7c5e2..9506b41 100644 --- a/spanner_orm/metadata.py +++ b/spanner_orm/metadata.py @@ -71,7 +71,7 @@ def finalize(self) -> None: sorted_fields = list(sorted(self.fields.values(), key=lambda f: f.position)) if index.Index.PRIMARY_INDEX not in self.indexes: - primary_keys = [f.name for f in sorted_fields if f.primary_key()] + primary_keys = [f.name for f in sorted_fields if f.primary_key] primary_index = index.Index(primary_keys) primary_index.name = index.Index.PRIMARY_INDEX self.indexes[index.Index.PRIMARY_INDEX] = primary_index @@ -94,6 +94,8 @@ def add_metadata(self, metadata: 'ModelMetadata') -> None: def add_field(self, name: str, new_field: field.Field) -> None: new_field.name = name new_field.position = len(self.fields) + if new_field.name in self.fields: + raise error.SpannerError('Already contains a field named "{}"'.format(new_field.name)) self.fields[name] = new_field def add_relation(self, name: str, @@ -103,4 +105,6 @@ def add_relation(self, name: str, def add_index(self, name: str, new_index: index.Index) -> None: new_index.name = name + if new_index.name in self.indexes: + raise error.SpannerError('Already contains an index named "{}"'.format(new_index.name)) self.indexes[name] = new_index diff --git a/spanner_orm/relationship.py b/spanner_orm/relationship.py index d929b8f..6f8ff20 100644 --- a/spanner_orm/relationship.py +++ b/spanner_orm/relationship.py @@ -14,7 +14,7 @@ # limitations under the License. """Helps define a foreign key relationship between two models.""" -from typing import Any, Dict, List, Type, Union +from typing import Any, Dict, List, Type, Union, Optional import dataclasses from spanner_orm import error diff --git a/spanner_orm/tests/admin_test.py b/spanner_orm/tests/admin_test.py index 9700634..0ad8262 100644 --- a/spanner_orm/tests/admin_test.py +++ b/spanner_orm/tests/admin_test.py @@ -47,8 +47,8 @@ def make_test_columns(self, model): 'table_name': model.table, 'column_name': row.name, 'ordinal_position': iteration, - 'is_nullable': 'YES' if row.nullable() else 'NO', - 'spanner_type': row.field_type().ddl() + 'is_nullable': 'YES' if row.nullable else 'NO', + 'spanner_type': row.field_type.ddl() }) iteration += 1 return [column.ColumnSchema(row) for row in columns] @@ -109,10 +109,10 @@ def test_metadata(self, tables, columns, index_columns, indexes): self.assertEqual(meta.table, model.table) self.assertEqual(meta.columns, model.columns) for row in model.columns: - self.assertEqual(meta.fields[row].field_type(), - model.fields[row].field_type()) - self.assertEqual(meta.fields[row].nullable(), - model.fields[row].nullable()) + self.assertEqual(meta.fields[row].field_type, + model.fields[row].field_type) + self.assertEqual(meta.fields[row].nullable, + model.fields[row].nullable) self.assertEqual(meta.primary_keys, model.primary_keys) self.assertEqual( getattr(meta, index.Index.PRIMARY_INDEX).columns, model.primary_keys) @@ -163,13 +163,23 @@ def test_secondary_index(self, tables, columns, index_columns, indexes): self.assertEqual(getattr(meta, name).columns, index_cols) def test_model_creation_ddl(self): - expected_ddl = [ - 'CREATE TABLE IndexTestModel (key STRING(MAX) NOT NULL,' - ' value STRING(MAX) NOT NULL) PRIMARY KEY (key)', - 'CREATE INDEX value_index ON IndexTestModel (value)' - ] - ddl = update.model_creation_ddl(models.IndexTestModel) - self.assertEqual(ddl, expected_ddl) + expected_ddl = [ + 'CREATE TABLE IndexTestModel (key STRING(MAX) NOT NULL,' + ' value STRING(MAX) NOT NULL) PRIMARY KEY (key)', + 'CREATE INDEX value ON IndexTestModel (value)' + ] + ddl = update.model_creation_ddl(models.IndexTestModel) + self.assertEqual(ddl, expected_ddl) + self.assertCountEqual(models.IndexTestModel.meta.indexes.keys(), ['PRIMARY_KEY', 'value_idx']) + + def test_model_creation_ddl2(self): + expected_ddl = [ + 'CREATE TABLE FieldCustomNameTestModel (key2 STRING(MAX) NOT NULL)' + ' PRIMARY KEY (key2)' + ] + ddl = update.model_creation_ddl(models.FieldCustomNameTestModel) + self.assertEqual(ddl, expected_ddl) + self.assertCountEqual(models.FieldCustomNameTestModel.meta.indexes.keys(), ['PRIMARY_KEY']) if __name__ == '__main__': diff --git a/spanner_orm/tests/model_test.py b/spanner_orm/tests/model_test.py index 804368a..52a4d27 100644 --- a/spanner_orm/tests/model_test.py +++ b/spanner_orm/tests/model_test.py @@ -95,8 +95,8 @@ def test_object_changes(self): def test_field_exists_on_model_class(self): self.assertIsInstance(models.SmallTestModel.key, field.Field) - self.assertEqual(models.SmallTestModel.key.field_type(), field.String) - self.assertFalse(models.SmallTestModel.key.nullable()) + self.assertEqual(models.SmallTestModel.key.field_type, field.String) + self.assertFalse(models.SmallTestModel.key.nullable) self.assertEqual(models.SmallTestModel.key.name, 'key') def test_field_inheritance(self): diff --git a/spanner_orm/tests/models.py b/spanner_orm/tests/models.py index 2a8218a..41ec6bc 100644 --- a/spanner_orm/tests/models.py +++ b/spanner_orm/tests/models.py @@ -46,7 +46,13 @@ class IndexTestModel(model.Model): key = field.Field(field.String, primary_key=True) value = field.Field(field.String) - value_index = index.Index(['value']) + value_idx = index.Index(['value'], name='value') + + +class FieldCustomNameTestModel(model.Model): + __table__ = 'FieldCustomNameTestModel' + + key = field.Field(field.String, primary_key=True, name='key2') class RelationshipTestModel(model.Model):