Skip to content

Commit 253460c

Browse files
committed
Start populating model metadata from attributes
The popular python ORMs populate model metadata from class attributes instead of class methods. As part of doing this, the field types are now wrapped in a Field class, that handles the nullability checks. Still to do: relationships, indices
1 parent c003640 commit 253460c

11 files changed

Lines changed: 235 additions & 181 deletions

File tree

spanner_orm/__init__.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,8 @@
2828
ModelRelationship = relationship.ModelRelationship
2929

3030
Boolean = field.Boolean
31+
Field = field.Field
3132
Integer = field.Integer
32-
NullableBoolean = field.NullableBoolean
33-
NullableInteger = field.NullableInteger
34-
NullableString = field.NullableString
35-
NullableStringArray = field.NullableStringArray
36-
NullableTimestamp = field.NullableTimestamp
3733
String = field.String
3834
StringArray = field.StringArray
3935
Timestamp = field.Timestamp

spanner_orm/admin/metadata.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818

1919
from spanner_orm import condition
2020
from spanner_orm import error
21+
from spanner_orm import field
2122
from spanner_orm import model
2223
from spanner_orm import update
2324
from spanner_orm.admin import api
@@ -26,7 +27,7 @@
2627
from spanner_orm.schemas import index_column
2728

2829

29-
class DatabaseMetadata(object):
30+
class SpannerMetadata(object):
3031
"""Gathers information about a table from Spanner."""
3132

3233
@classmethod
@@ -97,7 +98,9 @@ def _tables(cls, transaction=None):
9798
transaction, condition.EqualityCondition('table_catalog', ''),
9899
condition.EqualityCondition('table_schema', ''))
99100
for schema in schemas:
100-
tables[schema.table_name][schema.column_name] = schema.type()
101+
new_field = field.Field(schema.field_type(), nullable=schema.nullable())
102+
new_field.name = schema.column_name
103+
tables[schema.table_name][schema.column_name] = new_field
101104
return tables
102105

103106
@classmethod

spanner_orm/condition.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -107,11 +107,12 @@ def _types(self):
107107

108108
def _validate(self, model):
109109
assert self.column in model.schema()
110-
origin_type = model.schema()[self.column]
110+
origin = model.schema()[self.column]
111111
assert self.destination_column in self.destination_model.schema()
112-
destination_type = self.destination_model.schema()[self.destination_column]
112+
dest = self.destination_model.schema()[self.destination_column]
113113

114-
assert origin_type.db_type() == destination_type.db_type()
114+
assert (origin.field_type() == dest.field_type() and
115+
origin.nullable() == dest.nullable())
115116

116117

117118
class IncludesCondition(Condition):

spanner_orm/field.py

Lines changed: 31 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,39 @@
2020
from google.cloud.spanner_v1.proto import type_pb2
2121

2222

23-
class FieldType(abc.ABC):
24-
"""Base class for column types for Spanner interactions."""
23+
class Field(object):
24+
"""Represents a column in a table as a field in a model."""
2525

26-
@classmethod
27-
def full_ddl(cls):
28-
if issubclass(cls, NullableType):
29-
return cls.ddl()
26+
def __init__(self, field_type, nullable=False):
27+
self._type = field_type
28+
self._nullable = nullable
29+
30+
def ddl(self):
31+
if self._nullable:
32+
return self._type.ddl()
33+
return '{field_type} NOT NULL'.format(field_type=self._type.ddl())
34+
35+
def field_type(self):
36+
return self._type
37+
38+
def grpc_type(self):
39+
return self._type.grpc_type()
40+
41+
def grpc_list_type(self):
42+
return self._type.grpc_list_type()
43+
44+
def nullable(self):
45+
return self._nullable
46+
47+
def validate(self, value):
48+
if value is None:
49+
assert self._nullable
3050
else:
31-
return '{} NOT NULL'.format(cls.ddl())
51+
self._type.validate_type(value)
3252

33-
@staticmethod
34-
@abc.abstractmethod
35-
def db_type():
36-
raise NotImplementedError
53+
54+
class FieldType(abc.ABC):
55+
"""Base class for column types for Spanner interactions."""
3756

3857
@staticmethod
3958
@abc.abstractmethod
@@ -50,30 +69,15 @@ def grpc_list_type(cls):
5069
return type_pb2.Type(
5170
code=type_pb2.ARRAY, array_element_type=cls.grpc_type())
5271

53-
@classmethod
54-
def validate(cls, value):
55-
if value is None:
56-
assert issubclass(cls, NullableType), 'Null value for non-nullable column'
57-
else:
58-
cls.validate_type(value)
59-
6072
@staticmethod
6173
@abc.abstractmethod
6274
def validate_type(value):
6375
raise NotImplementedError
6476

6577

66-
class NullableType(abc.ABC):
67-
pass
68-
69-
7078
class Boolean(FieldType):
7179
"""Represents a boolean type."""
7280

73-
@staticmethod
74-
def db_type():
75-
return Boolean
76-
7781
@staticmethod
7882
def ddl():
7983
return 'BOOL'
@@ -87,17 +91,9 @@ def validate_type(value):
8791
assert isinstance(value, bool), '{} is not of type bool'.format(value)
8892

8993

90-
class NullableBoolean(NullableType, Boolean):
91-
pass
92-
93-
9494
class Integer(FieldType):
9595
"""Represents an integer type."""
9696

97-
@staticmethod
98-
def db_type():
99-
return Integer
100-
10197
@staticmethod
10298
def ddl():
10399
return 'INT64'
@@ -111,17 +107,9 @@ def validate_type(value):
111107
assert isinstance(value, int), '{} is not of type int'.format(value)
112108

113109

114-
class NullableInteger(NullableType, Integer):
115-
pass
116-
117-
118110
class String(FieldType):
119111
"""Represents a string type."""
120112

121-
@staticmethod
122-
def db_type():
123-
return String
124-
125113
@staticmethod
126114
def ddl():
127115
return 'STRING(MAX)'
@@ -135,17 +123,9 @@ def validate_type(value):
135123
assert isinstance(value, str), '{} is not of type str'.format(value)
136124

137125

138-
class NullableString(NullableType, String):
139-
pass
140-
141-
142126
class StringArray(FieldType):
143127
"""Represents an array of strings type."""
144128

145-
@staticmethod
146-
def db_type():
147-
return StringArray
148-
149129
@staticmethod
150130
def ddl():
151131
return 'ARRAY<STRING(MAX)>'
@@ -161,17 +141,9 @@ def validate_type(value):
161141
assert isinstance(item, str), '{} is not of type str'.format(item)
162142

163143

164-
class NullableStringArray(NullableType, StringArray):
165-
pass
166-
167-
168144
class Timestamp(FieldType):
169145
"""Represents a timestamp type."""
170146

171-
@staticmethod
172-
def db_type():
173-
return Timestamp
174-
175147
@staticmethod
176148
def ddl():
177149
return 'TIMESTAMP'
@@ -185,11 +157,4 @@ def validate_type(value):
185157
assert isinstance(value, datetime.datetime)
186158

187159

188-
class NullableTimestamp(NullableType, Timestamp):
189-
pass
190-
191-
192-
ALL_TYPES = [
193-
Boolean, NullableBoolean, Integer, NullableInteger, String, NullableString,
194-
StringArray, NullableStringArray, Timestamp, NullableTimestamp
195-
]
160+
ALL_TYPES = [Boolean, Integer, String, StringArray, Timestamp]

spanner_orm/model.py

Lines changed: 40 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,53 @@
1414
# limitations under the License.
1515
"""Holds table-specific information to make querying spanner eaiser."""
1616

17-
import abc
1817
import copy
1918

2019
from spanner_orm import api
2120
from spanner_orm import condition
2221
from spanner_orm import error
22+
from spanner_orm import field
2323
from spanner_orm import query
2424

2525
from google.cloud import spanner
2626

2727

28-
class Model(abc.ABC):
28+
class Meta(object):
29+
30+
def __init__(self, schema=None, table=None):
31+
self.schema = schema
32+
self.table = table
33+
34+
35+
class ModelBase(type):
36+
"""Populates Model metadata based on class attributes."""
37+
38+
def __new__(mcs, name, bases, attrs, **kwargs):
39+
parents = [base for base in bases if isinstance(base, ModelBase)]
40+
if not parents:
41+
return super().__new__(mcs, name, bases, attrs, **kwargs)
42+
43+
table = None
44+
schema = {}
45+
old_attrs = attrs.copy()
46+
for key, value in old_attrs.items():
47+
if isinstance(value, field.Field):
48+
value.name = key
49+
schema[key] = attrs.pop(key)
50+
elif key == '__table__':
51+
table = attrs.pop(key)
52+
53+
cls = super().__new__(mcs, name, bases, attrs, **kwargs)
54+
cls.meta = Meta(schema=schema, table=table)
55+
return cls
56+
57+
def __getattr__(cls, name):
58+
if name in cls.meta.schema:
59+
return cls.meta.schema[name]
60+
raise AttributeError(name)
61+
62+
63+
class Model(metaclass=ModelBase):
2964
"""Maps to a table in spanner and has basic functions for querying tables."""
3065

3166
@classmethod
@@ -38,7 +73,6 @@ def columns(cls):
3873
return set(cls.schema())
3974

4075
@staticmethod
41-
@abc.abstractmethod
4276
def primary_index_keys():
4377
raise NotImplementedError
4478

@@ -47,19 +81,17 @@ def relations(cls):
4781
return {}
4882

4983
@classmethod
50-
@abc.abstractmethod
5184
def schema(cls):
52-
raise NotImplementedError
85+
return cls.meta.schema
5386

5487
@classmethod
55-
@abc.abstractmethod
5688
def table(cls):
57-
raise NotImplementedError
89+
return cls.meta.table
5890

5991
@classmethod
6092
def create_table_ddl(cls):
6193
fields = [
62-
'{} {}'.format(name, field.full_ddl())
94+
'{} {}'.format(name, field.ddl())
6395
for name, field in cls.schema().items()
6496
]
6597
field_ddl = '({})'.format(', '.join(fields))

spanner_orm/schemas/column.py

Lines changed: 13 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -22,34 +22,26 @@
2222
class ColumnSchema(schema.Schema):
2323
"""Model for interacting with Spanner column schema table."""
2424

25+
__table__ = 'information_schema.columns'
26+
table_catalog = field.Field(field.String)
27+
table_schema = field.Field(field.String)
28+
table_name = field.Field(field.String)
29+
column_name = field.Field(field.String)
30+
ordinal_position = field.Field(field.Integer)
31+
is_nullable = field.Field(field.String)
32+
spanner_type = field.Field(field.String)
33+
2534
@staticmethod
2635
def primary_index_keys():
2736
return ['table_catalog', 'table_schema', 'table_name', 'column_name']
2837

29-
@classmethod
30-
def schema(cls):
31-
return {
32-
'table_catalog': field.String,
33-
'table_schema': field.String,
34-
'table_name': field.String,
35-
'column_name': field.String,
36-
'ordinal_position': field.Integer,
37-
'is_nullable': field.String,
38-
'spanner_type': field.String
39-
}
40-
41-
@classmethod
42-
def table(cls):
43-
return 'information_schema.columns'
44-
4538
def nullable(self):
4639
return self.is_nullable == 'YES'
4740

48-
def type(self):
49-
for db_type in field.ALL_TYPES:
50-
db_nullable = issubclass(db_type, field.NullableType)
51-
if self.spanner_type == db_type.ddl() and self.nullable() == db_nullable:
52-
return db_type
41+
def field_type(self):
42+
for field_type in field.ALL_TYPES:
43+
if self.spanner_type == field_type.ddl():
44+
return field_type
5345

5446
raise error.SpannerError('No corresponding Type for {}'.format(
5547
self.spanner_type))

spanner_orm/schemas/index.py

Lines changed: 11 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -21,24 +21,17 @@
2121
class IndexSchema(schema.Schema):
2222
"""Model for interacting with Spanner index schema table."""
2323

24+
__table__ = 'information_schema.indexes'
25+
table_catalog = field.Field(field.String)
26+
table_schema = field.Field(field.String)
27+
table_name = field.Field(field.String)
28+
index_name = field.Field(field.String)
29+
index_type = field.Field(field.String)
30+
parent_table_name = field.Field(field.String, nullable=True)
31+
is_unique = field.Field(field.Boolean)
32+
is_null_filtered = field.Field(field.Boolean)
33+
index_state = field.Field(field.String)
34+
2435
@staticmethod
2536
def primary_index_keys():
2637
return ['table_catalog', 'table_schema', 'table_name', 'index_name']
27-
28-
@classmethod
29-
def schema(cls):
30-
return {
31-
'table_catalog': field.String,
32-
'table_schema': field.String,
33-
'table_name': field.String,
34-
'index_name': field.String,
35-
'index_type': field.String,
36-
'parent_table_name': field.NullableString,
37-
'is_unique': field.Boolean,
38-
'is_null_filtered': field.Boolean,
39-
'index_state': field.String
40-
}
41-
42-
@classmethod
43-
def table(cls):
44-
return 'information_schema.indexes'

0 commit comments

Comments
 (0)