| FazBrowse GitHub Viewer | Trending | | Home |
| Tools: [Download Repo ZIP] [Original HTTPS Page] |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -68,6 +68,7 @@ | |||
| 68 | 68 | StringArray = field.StringArray | |
| 69 | 69 | Timestamp = field.Timestamp | |
| 70 | 70 | BytesBase64 = field.BytesBase64 | |
| 71 | + Array = field.Array | ||
| 71 | 72 | ||
| 72 | 73 | ArbitraryCondition = condition.ArbitraryCondition | |
| 73 | 74 | Column = condition.Column | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -17,6 +17,7 @@ | |||
| 17 | 17 | import base64 | |
| 18 | 18 | import binascii | |
| 19 | 19 | import datetime | |
| 20 | + import re | ||
| 20 | 21 | from typing import Any, Optional, Type, Union | |
| 21 | 22 | import warnings | |
| 22 | 23 | ||
@@ -195,29 +196,6 @@ def validate_type(self, value: Any) -> None: | |||
| 195 | 196 | raise error.ValidationError(f'{value!r} is not of type str') | |
| 196 | 197 | ||
| 197 | 198 | ||
| 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 | - | ||
| 221 | 199 | class Timestamp(FieldType): | |
| 222 | 200 | """Represents a timestamp type.""" | |
| 223 | 201 | ||
@@ -263,6 +241,54 @@ def validate_type(self, value: Any) -> None: | |||
| 263 | 241 | raise error.ValidationError(f'{value!r} must be base64-encoded bytes.') | |
| 264 | 242 | ||
| 265 | 243 | ||
| 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 | + | ||
| 266 | 292 | def field_type_from_ddl(ddl: str) -> FieldType: | |
| 267 | 293 | """Returns the field type for the given DDL expression.""" | |
| 268 | 294 | if ddl == 'BOOL': | |
@@ -273,11 +299,11 @@ def field_type_from_ddl(ddl: str) -> FieldType: | |||
| 273 | 299 | return Float() | |
| 274 | 300 | elif ddl == 'STRING(MAX)': | |
| 275 | 301 | return String() | |
| 276 | - elif ddl == 'ARRAY<STRING(MAX)>': | ||
| 277 | - return StringArray() | ||
| 278 | 302 | elif ddl == 'TIMESTAMP': | |
| 279 | 303 | return Timestamp() | |
| 280 | 304 | elif ddl == 'BYTES(MAX)': | |
| 281 | 305 | return BytesBase64() | |
| 306 | + elif (match := re.fullmatch(r'ARRAY<(.*)>', ddl)) is not None: | ||
| 307 | + return Array(field_type_from_ddl(match.group(1))) | ||
| 282 | 308 | else: | |
| 283 | 309 | raise error.SpannerError(f'Invalid or unimplemented DDL type: {ddl!r}') | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
@@ -36,6 +36,8 @@ class FieldTest(parameterized.TestCase): | |||
| 36 | 36 | (field.String(), 'STRING(MAX)'), | |
| 37 | 37 | (field.Timestamp(), 'TIMESTAMP'), | |
| 38 | 38 | (field.BytesBase64(), 'BYTES(MAX)'), | |
| 39 | + (field.Array(field.Boolean()), 'ARRAY<BOOL>'), | ||
| 40 | + (field.Array(field.String()), 'ARRAY<STRING(MAX)>'), | ||
| 39 | 41 | ) | |
| 40 | 42 | def test_field_type_ddl( | |
| 41 | 43 | self, | |
@@ -51,6 +53,10 @@ def test_field_type_ddl( | |||
| 51 | 53 | (field.String(), spanner.param_types.STRING), | |
| 52 | 54 | (field.Timestamp(), spanner.param_types.TIMESTAMP), | |
| 53 | 55 | (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)), | ||
| 54 | 60 | ) | |
| 55 | 61 | def test_field_type_grpc_type( | |
| 56 | 62 | self, | |
@@ -67,6 +73,7 @@ def test_field_type_grpc_type( | |||
| 67 | 73 | (field.String(), 'foo'), | |
| 68 | 74 | (field.Timestamp(), datetime.datetime(2022, 9, 21)), | |
| 69 | 75 | (field.BytesBase64(), base64.b64encode(b'\x00')), | |
| 76 | + (field.Array(field.Boolean()), [True]), | ||
| 70 | 77 | ) | |
| 71 | 78 | def test_field_type_validate_type_ok( | |
| 72 | 79 | self, | |
@@ -83,6 +90,8 @@ def test_field_type_validate_type_ok( | |||
| 83 | 90 | (field.Timestamp(), datetime.date(2022, 9, 21)), | |
| 84 | 91 | (field.BytesBase64(), base64.b64encode(b'\x00').decode('utf-8')), | |
| 85 | 92 | (field.BytesBase64(), b'!'), | |
| 93 | + (field.Array(field.Boolean()), {True}), | ||
| 94 | + (field.Array(field.Boolean()), [1]), | ||
| 86 | 95 | ) | |
| 87 | 96 | def test_field_type_validate_type_error( | |
| 88 | 97 | self, | |
@@ -95,6 +104,8 @@ def test_field_type_validate_type_error( | |||
| 95 | 104 | @parameterized.parameters( | |
| 96 | 105 | (field.Boolean(), field.Boolean(), True), | |
| 97 | 106 | (field.Boolean(), field.String(), False), | |
| 107 | + (field.Array(field.Integer()), field.Array(field.Integer()), False), | ||
| 108 | + (field.Array(field.Integer()), field.Integer(), False), | ||
| 98 | 109 | ) | |
| 99 | 110 | def test_field_type_comparable_with( | |
| 100 | 111 | self, | |
@@ -115,14 +126,30 @@ def test_field_field_type_is_class(self): | |||
| 115 | 126 | self.assertIn('instance of FieldType', str(actual_warnings[0].message)) | |
| 116 | 127 | self.assertIs(actual_warnings[0].category, DeprecationWarning) | |
| 117 | 128 | ||
| 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 | + | ||
| 118 | 144 | @parameterized.parameters( | |
| 119 | 145 | 'BOOL', | |
| 120 | 146 | 'INT64', | |
| 121 | 147 | 'FLOAT64', | |
| 122 | 148 | 'STRING(MAX)', | |
| 123 | - 'ARRAY<STRING(MAX)>', | ||
| 124 | 149 | 'TIMESTAMP', | |
| 125 | 150 | 'BYTES(MAX)', | |
| 151 | + 'ARRAY<INT64>', | ||
| 152 | + 'ARRAY<STRING(MAX)>', | ||
| 126 | 153 | ) | |
| 127 | 154 | def test_ddl_to_field_type_to_ddl(self, ddl: str): | |
| 128 | 155 | self.assertEqual(field.field_type_from_ddl(ddl).ddl(), ddl) | |
| Back | FazBrowse Home | New Git URL |
0 commit comments