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

Add a generic Array type and deprecate StringArray · 7dracoder/python-spanner-orm@924e01b · GitHub

Commit 924e01b

Browse files
committed
Add a generic Array type and deprecate StringArray
1 parent 96dacf9 commit 924e01b

3 files changed

Lines changed: 80 additions & 26 deletions

File tree

‎spanner_orm/__init__.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
StringArray = field.StringArray
6969
Timestamp = field.Timestamp
7070
BytesBase64 = field.BytesBase64
71+
Array = field.Array
7172

7273
ArbitraryCondition = condition.ArbitraryCondition
7374
Column = condition.Column

‎spanner_orm/field.py‎

Lines changed: 51 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
import base64
1818
import binascii
1919
import datetime
20+
import re
2021
from typing import Any, Optional, Type, Union
2122
import warnings
2223

@@ -195,29 +196,6 @@ def validate_type(self, value: Any) -> None:
195196
raise error.ValidationError(f'{value!r} is not of type str')
196197

197198

198-
class StringArray(FieldType):
199-
"""Represents an array of strings type."""
200-
201-
def ddl(self) -> str:
202-
"""See base class."""
203-
del self # Unused.
204-
return 'ARRAY<STRING(MAX)>'
205-
206-
def grpc_type(self) -> spanner_v1.Type:
207-
"""See base class."""
208-
del self # Unused.
209-
return spanner.param_types.Array(spanner.param_types.STRING)
210-
211-
def validate_type(self, value: Any) -> None:
212-
"""See base class."""
213-
del self # Unused.
214-
if not isinstance(value, list):
215-
raise error.ValidationError(f'{value!r} is not of type list')
216-
for item in value:
217-
if not isinstance(item, str):
218-
raise error.ValidationError(f'{item!r} is not of type str')
219-
220-
221199
class Timestamp(FieldType):
222200
"""Represents a timestamp type."""
223201

@@ -263,6 +241,54 @@ def validate_type(self, value: Any) -> None:
263241
raise error.ValidationError(f'{value!r} must be base64-encoded bytes.')
264242

265243

244+
class Array(FieldType):
245+
"""Represents an array type."""
246+
247+
def __init__(self, element_type: FieldType):
248+
"""Initializer.
249+
250+
Args:
251+
element_type: Type of the values in the array. Can't be an Array type
252+
itself.
253+
"""
254+
if isinstance(element_type, Array):
255+
# https://cloud.google.com/spanner/docs/reference/standard-sql/data-types#array_type
256+
raise error.SpannerError(
257+
'Cloud Spanner does not support arrays of arrays.')
258+
self._element_type = element_type
259+
260+
def ddl(self) -> str:
261+
"""See base class."""
262+
return f'ARRAY<{self._element_type.ddl()}>'
263+
264+
def grpc_type(self) -> spanner_v1.Type:
265+
"""See base class."""
266+
return spanner.param_types.Array(self._element_type.grpc_type())
267+
268+
def validate_type(self, value: Any) -> None:
269+
"""See base class."""
270+
if not isinstance(value, list):
271+
raise error.ValidationError(f'{value!r} is not of type list')
272+
for element in value:
273+
self._element_type.validate_type(element)
274+
275+
def comparable_with(self, other: FieldType) -> bool:
276+
"""See base class."""
277+
# Running `select [1, 2] = [1, 2];` in Cloud Spanner gives this error: Query
278+
# failed: Equality is not defined for arguments of type ARRAY<INT64> at line
279+
# 3, column 8
280+
return False
281+
282+
283+
class StringArray(Array):
284+
"""Deprecated way to represent an array of strings type."""
285+
286+
def __init__(self):
287+
super().__init__(String())
288+
warnings.warn(
289+
DeprecationWarning('Use Array(String()) instead of StringArray().'))
290+
291+
266292
def field_type_from_ddl(ddl: str) -> FieldType:
267293
"""Returns the field type for the given DDL expression."""
268294
if ddl == 'BOOL':
@@ -273,11 +299,11 @@ def field_type_from_ddl(ddl: str) -> FieldType:
273299
return Float()
274300
elif ddl == 'STRING(MAX)':
275301
return String()
276-
elif ddl == 'ARRAY<STRING(MAX)>':
277-
return StringArray()
278302
elif ddl == 'TIMESTAMP':
279303
return Timestamp()
280304
elif ddl == 'BYTES(MAX)':
281305
return BytesBase64()
306+
elif (match := re.fullmatch(r'ARRAY<(.*)>', ddl)) is not None:
307+
return Array(field_type_from_ddl(match.group(1)))
282308
else:
283309
raise error.SpannerError(f'Invalid or unimplemented DDL type: {ddl!r}')

‎spanner_orm/tests/field_test.py‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ class FieldTest(parameterized.TestCase):
3636
(field.String(), 'STRING(MAX)'),
3737
(field.Timestamp(), 'TIMESTAMP'),
3838
(field.BytesBase64(), 'BYTES(MAX)'),
39+
(field.Array(field.Boolean()), 'ARRAY<BOOL>'),
40+
(field.Array(field.String()), 'ARRAY<STRING(MAX)>'),
3941
)
4042
def test_field_type_ddl(
4143
self,
@@ -51,6 +53,10 @@ def test_field_type_ddl(
5153
(field.String(), spanner.param_types.STRING),
5254
(field.Timestamp(), spanner.param_types.TIMESTAMP),
5355
(field.BytesBase64(), spanner.param_types.BYTES),
56+
(field.Array(field.Boolean()),
57+
spanner.param_types.Array(spanner.param_types.BOOL)),
58+
(field.Array(field.String()),
59+
spanner.param_types.Array(spanner.param_types.STRING)),
5460
)
5561
def test_field_type_grpc_type(
5662
self,
@@ -67,6 +73,7 @@ def test_field_type_grpc_type(
6773
(field.String(), 'foo'),
6874
(field.Timestamp(), datetime.datetime(2022, 9, 21)),
6975
(field.BytesBase64(), base64.b64encode(b'\x00')),
76+
(field.Array(field.Boolean()), [True]),
7077
)
7178
def test_field_type_validate_type_ok(
7279
self,
@@ -83,6 +90,8 @@ def test_field_type_validate_type_ok(
8390
(field.Timestamp(), datetime.date(2022, 9, 21)),
8491
(field.BytesBase64(), base64.b64encode(b'\x00').decode('utf-8')),
8592
(field.BytesBase64(), b'!'),
93+
(field.Array(field.Boolean()), {True}),
94+
(field.Array(field.Boolean()), [1]),
8695
)
8796
def test_field_type_validate_type_error(
8897
self,
@@ -95,6 +104,8 @@ def test_field_type_validate_type_error(
95104
@parameterized.parameters(
96105
(field.Boolean(), field.Boolean(), True),
97106
(field.Boolean(), field.String(), False),
107+
(field.Array(field.Integer()), field.Array(field.Integer()), False),
108+
(field.Array(field.Integer()), field.Integer(), False),
98109
)
99110
def test_field_type_comparable_with(
100111
self,
@@ -115,14 +126,30 @@ def test_field_field_type_is_class(self):
115126
self.assertIn('instance of FieldType', str(actual_warnings[0].message))
116127
self.assertIs(actual_warnings[0].category, DeprecationWarning)
117128

129+
def test_array_of_array_is_invalid(self):
130+
with self.assertRaisesRegex(error.SpannerError, 'arrays of arrays'):
131+
field.Array(field.Array(field.String()))
132+
133+
def test_string_array_is_deprecated_and_equivalent_to_array_of_string(self):
134+
with warnings.catch_warnings(record=True) as actual_warnings:
135+
string_array = field.StringArray()
136+
array_of_string = field.Array(field.String())
137+
self.assertLen(actual_warnings, 1)
138+
self.assertIn('Use Array(String()) instead',
139+
str(actual_warnings[0].message))
140+
self.assertIs(actual_warnings[0].category, DeprecationWarning)
141+
self.assertEqual(string_array.ddl(), array_of_string.ddl())
142+
self.assertEqual(string_array.grpc_type(), array_of_string.grpc_type())
143+
118144
@parameterized.parameters(
119145
'BOOL',
120146
'INT64',
121147
'FLOAT64',
122148
'STRING(MAX)',
123-
'ARRAY<STRING(MAX)>',
124149
'TIMESTAMP',
125150
'BYTES(MAX)',
151+
'ARRAY<INT64>',
152+
'ARRAY<STRING(MAX)>',
126153
)
127154
def test_ddl_to_field_type_to_ddl(self, ddl: str):
128155
self.assertEqual(field.field_type_from_ddl(ddl).ddl(), ddl)

0 commit comments

Comments
 (0)

Back | FazBrowse Home | New Git URL