matrixone-python-sdk 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- matrixone/__init__.py +155 -0
- matrixone/account.py +723 -0
- matrixone/async_client.py +3913 -0
- matrixone/async_metadata_manager.py +311 -0
- matrixone/async_orm.py +123 -0
- matrixone/async_vector_index_manager.py +633 -0
- matrixone/base_client.py +208 -0
- matrixone/client.py +4672 -0
- matrixone/config.py +452 -0
- matrixone/connection_hooks.py +286 -0
- matrixone/exceptions.py +89 -0
- matrixone/logger.py +782 -0
- matrixone/metadata.py +820 -0
- matrixone/moctl.py +219 -0
- matrixone/orm.py +2277 -0
- matrixone/pitr.py +646 -0
- matrixone/pubsub.py +771 -0
- matrixone/restore.py +411 -0
- matrixone/search_vector_index.py +1176 -0
- matrixone/snapshot.py +550 -0
- matrixone/sql_builder.py +844 -0
- matrixone/sqlalchemy_ext/__init__.py +161 -0
- matrixone/sqlalchemy_ext/adapters.py +163 -0
- matrixone/sqlalchemy_ext/dialect.py +534 -0
- matrixone/sqlalchemy_ext/fulltext_index.py +895 -0
- matrixone/sqlalchemy_ext/fulltext_search.py +1686 -0
- matrixone/sqlalchemy_ext/hnsw_config.py +194 -0
- matrixone/sqlalchemy_ext/ivf_config.py +252 -0
- matrixone/sqlalchemy_ext/table_builder.py +351 -0
- matrixone/sqlalchemy_ext/vector_index.py +1721 -0
- matrixone/sqlalchemy_ext/vector_type.py +948 -0
- matrixone/version.py +580 -0
- matrixone_python_sdk-0.1.0.dist-info/METADATA +706 -0
- matrixone_python_sdk-0.1.0.dist-info/RECORD +122 -0
- matrixone_python_sdk-0.1.0.dist-info/WHEEL +5 -0
- matrixone_python_sdk-0.1.0.dist-info/entry_points.txt +5 -0
- matrixone_python_sdk-0.1.0.dist-info/licenses/LICENSE +200 -0
- matrixone_python_sdk-0.1.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +19 -0
- tests/offline/__init__.py +20 -0
- tests/offline/conftest.py +77 -0
- tests/offline/test_account.py +703 -0
- tests/offline/test_async_client_query_comprehensive.py +1218 -0
- tests/offline/test_basic.py +54 -0
- tests/offline/test_case_sensitivity.py +227 -0
- tests/offline/test_connection_hooks_offline.py +287 -0
- tests/offline/test_dialect_schema_handling.py +609 -0
- tests/offline/test_explain_methods.py +346 -0
- tests/offline/test_filter_logical_in.py +237 -0
- tests/offline/test_fulltext_search_comprehensive.py +795 -0
- tests/offline/test_ivf_config.py +249 -0
- tests/offline/test_join_methods.py +281 -0
- tests/offline/test_join_sqlalchemy_compatibility.py +276 -0
- tests/offline/test_logical_in_method.py +237 -0
- tests/offline/test_matrixone_version_parsing.py +264 -0
- tests/offline/test_metadata_offline.py +557 -0
- tests/offline/test_moctl.py +300 -0
- tests/offline/test_moctl_simple.py +251 -0
- tests/offline/test_model_support_offline.py +359 -0
- tests/offline/test_model_support_simple.py +225 -0
- tests/offline/test_pinecone_filter_offline.py +377 -0
- tests/offline/test_pitr.py +585 -0
- tests/offline/test_pubsub.py +712 -0
- tests/offline/test_query_update.py +283 -0
- tests/offline/test_restore.py +445 -0
- tests/offline/test_snapshot_comprehensive.py +384 -0
- tests/offline/test_sql_escaping_edge_cases.py +551 -0
- tests/offline/test_sqlalchemy_integration.py +382 -0
- tests/offline/test_sqlalchemy_vector_integration.py +434 -0
- tests/offline/test_table_builder.py +198 -0
- tests/offline/test_unified_filter.py +398 -0
- tests/offline/test_unified_transaction.py +495 -0
- tests/offline/test_vector_index.py +238 -0
- tests/offline/test_vector_operations.py +688 -0
- tests/offline/test_vector_type.py +174 -0
- tests/offline/test_version_core.py +328 -0
- tests/offline/test_version_management.py +372 -0
- tests/offline/test_version_standalone.py +652 -0
- tests/online/__init__.py +20 -0
- tests/online/conftest.py +216 -0
- tests/online/test_account_management.py +194 -0
- tests/online/test_advanced_features.py +344 -0
- tests/online/test_async_client_interfaces.py +330 -0
- tests/online/test_async_client_online.py +285 -0
- tests/online/test_async_model_insert_online.py +293 -0
- tests/online/test_async_orm_online.py +300 -0
- tests/online/test_async_simple_query_online.py +802 -0
- tests/online/test_async_transaction_simple_query.py +300 -0
- tests/online/test_basic_connection.py +130 -0
- tests/online/test_client_online.py +238 -0
- tests/online/test_config.py +90 -0
- tests/online/test_config_validation.py +123 -0
- tests/online/test_connection_hooks_new_online.py +217 -0
- tests/online/test_dialect_schema_handling_online.py +331 -0
- tests/online/test_filter_logical_in_online.py +374 -0
- tests/online/test_fulltext_comprehensive.py +1773 -0
- tests/online/test_fulltext_label_online.py +433 -0
- tests/online/test_fulltext_search_online.py +842 -0
- tests/online/test_ivf_stats_online.py +506 -0
- tests/online/test_logger_integration.py +311 -0
- tests/online/test_matrixone_query_orm.py +540 -0
- tests/online/test_metadata_online.py +579 -0
- tests/online/test_model_insert_online.py +255 -0
- tests/online/test_mysql_driver_validation.py +213 -0
- tests/online/test_orm_advanced_features.py +2022 -0
- tests/online/test_orm_cte_integration.py +269 -0
- tests/online/test_orm_online.py +270 -0
- tests/online/test_pinecone_filter.py +708 -0
- tests/online/test_pubsub_operations.py +352 -0
- tests/online/test_query_methods.py +225 -0
- tests/online/test_query_update_online.py +433 -0
- tests/online/test_search_vector_index.py +557 -0
- tests/online/test_simple_fulltext_online.py +915 -0
- tests/online/test_snapshot_comprehensive.py +998 -0
- tests/online/test_sqlalchemy_engine_integration.py +336 -0
- tests/online/test_sqlalchemy_integration.py +425 -0
- tests/online/test_transaction_contexts.py +1219 -0
- tests/online/test_transaction_insert_methods.py +356 -0
- tests/online/test_transaction_query_methods.py +288 -0
- tests/online/test_unified_filter_online.py +529 -0
- tests/online/test_vector_comprehensive.py +706 -0
- tests/online/test_version_management.py +291 -0
@@ -0,0 +1,351 @@
|
|
1
|
+
# Copyright 2021 - 2022 Matrix Origin
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
|
15
|
+
"""
|
16
|
+
Table builder utilities for MatrixOne vector tables.
|
17
|
+
"""
|
18
|
+
|
19
|
+
from typing import List, Optional, Union
|
20
|
+
|
21
|
+
from sqlalchemy import (
|
22
|
+
TIMESTAMP,
|
23
|
+
BigInteger,
|
24
|
+
Boolean,
|
25
|
+
Column,
|
26
|
+
Date,
|
27
|
+
DateTime,
|
28
|
+
ForeignKeyConstraint,
|
29
|
+
Index,
|
30
|
+
Integer,
|
31
|
+
MetaData,
|
32
|
+
Numeric,
|
33
|
+
PrimaryKeyConstraint,
|
34
|
+
SmallInteger,
|
35
|
+
String,
|
36
|
+
Table,
|
37
|
+
Text,
|
38
|
+
Time,
|
39
|
+
)
|
40
|
+
from sqlalchemy.dialects.mysql import BLOB, JSON, LONGBLOB, MEDIUMBLOB, TINYBLOB, TINYINT, VARBINARY
|
41
|
+
|
42
|
+
from .vector_type import Vectorf32, Vectorf64, VectorPrecision, VectorType
|
43
|
+
|
44
|
+
|
45
|
+
class VectorTableBuilder:
|
46
|
+
"""
|
47
|
+
Builder class for creating MatrixOne vector tables with SQLAlchemy.
|
48
|
+
|
49
|
+
This class provides a fluent interface for building vector tables with
|
50
|
+
proper column definitions, indexes, and constraints. It's designed to
|
51
|
+
work seamlessly with MatrixOne's vector capabilities and SQLAlchemy.
|
52
|
+
|
53
|
+
Key Features:
|
54
|
+
|
55
|
+
- Fluent method chaining for table definition
|
56
|
+
- Support for all MatrixOne column types including vectors
|
57
|
+
- Automatic vector index creation
|
58
|
+
- Constraint and foreign key support
|
59
|
+
- Integration with SQLAlchemy metadata
|
60
|
+
|
61
|
+
Supported Column Types:
|
62
|
+
- Standard types: Integer, String, Text, DateTime, etc.
|
63
|
+
- Vector types: Vectorf32, Vectorf64 with configurable dimensions
|
64
|
+
- MatrixOne-specific types: JSON, BLOB variants
|
65
|
+
|
66
|
+
Usage Examples
|
67
|
+
|
68
|
+
.. code-block:: python
|
69
|
+
|
70
|
+
# Create a simple vector table
|
71
|
+
builder = VectorTableBuilder('documents')
|
72
|
+
table = (builder
|
73
|
+
.add_int_column('id', primary_key=True)
|
74
|
+
.add_string_column('title', length=255)
|
75
|
+
.add_text_column('content')
|
76
|
+
.add_vector_column('embedding', Vectorf32(384))
|
77
|
+
.build())
|
78
|
+
|
79
|
+
# Create a complex table with indexes
|
80
|
+
builder = VectorTableBuilder('products')
|
81
|
+
table = (builder
|
82
|
+
.add_bigint_column('id', primary_key=True)
|
83
|
+
.add_string_column('name', length=100)
|
84
|
+
.add_numeric_column('price', precision=10, scale=2)
|
85
|
+
.add_vector_column('features', Vectorf64(512))
|
86
|
+
.add_vector_index('idx_features', 'features', 'ivfflat', lists=100)
|
87
|
+
.build())
|
88
|
+
|
89
|
+
Note: This builder is primarily used internally by the Client's table
|
90
|
+
creation methods, but can be used directly for advanced use cases.
|
91
|
+
"""
|
92
|
+
|
93
|
+
def __init__(self, table_name: str, metadata: MetaData = None):
|
94
|
+
"""
|
95
|
+
Initialize the table builder.
|
96
|
+
|
97
|
+
Args:
|
98
|
+
|
99
|
+
table_name: Name of the table to create
|
100
|
+
metadata: SQLAlchemy metadata object
|
101
|
+
"""
|
102
|
+
self.table_name = table_name
|
103
|
+
self.metadata = metadata or MetaData()
|
104
|
+
self.columns = []
|
105
|
+
self.indexes = []
|
106
|
+
self.constraints = []
|
107
|
+
|
108
|
+
def add_column(self, name: str, type_, **kwargs):
|
109
|
+
"""Add a column to the table."""
|
110
|
+
column = Column(name, type_, **kwargs)
|
111
|
+
self.columns.append(column)
|
112
|
+
return self
|
113
|
+
|
114
|
+
def add_int_column(self, name: str, primary_key: bool = False, **kwargs):
|
115
|
+
"""Add an integer column."""
|
116
|
+
if primary_key:
|
117
|
+
kwargs["primary_key"] = True
|
118
|
+
return self.add_column(name, Integer, **kwargs)
|
119
|
+
|
120
|
+
def add_bigint_column(self, name: str, primary_key: bool = False, **kwargs):
|
121
|
+
"""Add a bigint column."""
|
122
|
+
if primary_key:
|
123
|
+
kwargs["primary_key"] = True
|
124
|
+
return self.add_column(name, BigInteger, **kwargs)
|
125
|
+
|
126
|
+
def add_string_column(self, name: str, length: int = 255, **kwargs):
|
127
|
+
"""Add a string column."""
|
128
|
+
return self.add_column(name, String(length), **kwargs)
|
129
|
+
|
130
|
+
def add_text_column(self, name: str, **kwargs):
|
131
|
+
"""Add a text column."""
|
132
|
+
return self.add_column(name, Text, **kwargs)
|
133
|
+
|
134
|
+
def add_json_column(self, name: str, **kwargs):
|
135
|
+
"""Add a JSON column."""
|
136
|
+
return self.add_column(name, JSON, **kwargs)
|
137
|
+
|
138
|
+
def add_vector_column(self, name: str, dimension: int, precision: str = VectorPrecision.F32, **kwargs):
|
139
|
+
"""Add a vector column."""
|
140
|
+
if precision == VectorPrecision.F32:
|
141
|
+
vector_type = Vectorf32(dimension=dimension)
|
142
|
+
elif precision == VectorPrecision.F64:
|
143
|
+
vector_type = Vectorf64(dimension=dimension)
|
144
|
+
else:
|
145
|
+
vector_type = VectorType(dimension=dimension, precision=precision)
|
146
|
+
|
147
|
+
return self.add_column(name, vector_type, **kwargs)
|
148
|
+
|
149
|
+
def add_vecf32_column(self, name: str, dimension: int, **kwargs):
|
150
|
+
"""Add a vecf32 column."""
|
151
|
+
return self.add_vector_column(name, dimension, VectorPrecision.F32, **kwargs)
|
152
|
+
|
153
|
+
def add_vecf64_column(self, name: str, dimension: int, **kwargs):
|
154
|
+
"""Add a vecf64 column."""
|
155
|
+
return self.add_vector_column(name, dimension, VectorPrecision.F64, **kwargs)
|
156
|
+
|
157
|
+
def add_smallint_column(self, name: str, primary_key: bool = False, **kwargs):
|
158
|
+
"""Add a smallint column."""
|
159
|
+
if primary_key:
|
160
|
+
kwargs["primary_key"] = True
|
161
|
+
return self.add_column(name, SmallInteger, **kwargs)
|
162
|
+
|
163
|
+
def add_tinyint_column(self, name: str, primary_key: bool = False, **kwargs):
|
164
|
+
"""Add a tinyint column."""
|
165
|
+
if primary_key:
|
166
|
+
kwargs["primary_key"] = True
|
167
|
+
return self.add_column(name, TINYINT, **kwargs)
|
168
|
+
|
169
|
+
def add_numeric_column(
|
170
|
+
self, name: str, column_type: str, precision: Optional[int] = None, scale: Optional[int] = None, **kwargs
|
171
|
+
):
|
172
|
+
"""Add a numeric column (float, double, decimal, numeric)."""
|
173
|
+
if column_type in ("float", "double"):
|
174
|
+
from sqlalchemy import Float
|
175
|
+
|
176
|
+
return self.add_column(name, Float(precision=precision), **kwargs)
|
177
|
+
elif column_type in ("decimal", "numeric"):
|
178
|
+
return self.add_column(name, Numeric(precision=precision, scale=scale), **kwargs)
|
179
|
+
else:
|
180
|
+
raise ValueError(f"Unsupported numeric type: {column_type}")
|
181
|
+
|
182
|
+
def add_datetime_column(self, name: str, column_type: str, **kwargs):
|
183
|
+
"""Add a datetime column (date, datetime, timestamp, time, year)."""
|
184
|
+
if column_type == "date":
|
185
|
+
return self.add_column(name, Date, **kwargs)
|
186
|
+
elif column_type == "datetime":
|
187
|
+
return self.add_column(name, DateTime, **kwargs)
|
188
|
+
elif column_type == "timestamp":
|
189
|
+
return self.add_column(name, TIMESTAMP, **kwargs)
|
190
|
+
elif column_type == "time":
|
191
|
+
return self.add_column(name, Time, **kwargs)
|
192
|
+
elif column_type == "year":
|
193
|
+
# MySQL YEAR type
|
194
|
+
from sqlalchemy.dialects.mysql import YEAR
|
195
|
+
|
196
|
+
return self.add_column(name, YEAR, **kwargs)
|
197
|
+
else:
|
198
|
+
raise ValueError(f"Unsupported datetime type: {column_type}")
|
199
|
+
|
200
|
+
def add_boolean_column(self, name: str, **kwargs):
|
201
|
+
"""Add a boolean column."""
|
202
|
+
return self.add_column(name, Boolean, **kwargs)
|
203
|
+
|
204
|
+
def add_binary_column(self, name: str, column_type: str, **kwargs):
|
205
|
+
"""Add a binary column (blob, longblob, mediumblob, tinyblob, binary, varbinary)."""
|
206
|
+
if column_type == "blob":
|
207
|
+
return self.add_column(name, BLOB, **kwargs)
|
208
|
+
elif column_type == "longblob":
|
209
|
+
return self.add_column(name, LONGBLOB, **kwargs)
|
210
|
+
elif column_type == "mediumblob":
|
211
|
+
return self.add_column(name, MEDIUMBLOB, **kwargs)
|
212
|
+
elif column_type == "tinyblob":
|
213
|
+
return self.add_column(name, TINYBLOB, **kwargs)
|
214
|
+
elif column_type == "binary":
|
215
|
+
from sqlalchemy import Binary
|
216
|
+
|
217
|
+
return self.add_column(name, Binary, **kwargs)
|
218
|
+
elif column_type == "varbinary":
|
219
|
+
return self.add_column(name, VARBINARY, **kwargs)
|
220
|
+
else:
|
221
|
+
raise ValueError(f"Unsupported binary type: {column_type}")
|
222
|
+
|
223
|
+
def add_enum_column(self, name: str, column_type: str, values: list, **kwargs):
|
224
|
+
"""Add an enum or set column."""
|
225
|
+
if column_type == "enum":
|
226
|
+
from sqlalchemy import Enum
|
227
|
+
|
228
|
+
return self.add_column(name, Enum(*values), **kwargs)
|
229
|
+
elif column_type == "set":
|
230
|
+
from sqlalchemy.dialects.mysql import SET
|
231
|
+
|
232
|
+
return self.add_column(name, SET(*values), **kwargs)
|
233
|
+
else:
|
234
|
+
raise ValueError(f"Unsupported enum type: {column_type}")
|
235
|
+
|
236
|
+
def add_index(self, columns: Union[str, List[str]], name: Optional[str] = None, **kwargs):
|
237
|
+
"""Add an index to the table."""
|
238
|
+
if isinstance(columns, str):
|
239
|
+
columns = [columns]
|
240
|
+
|
241
|
+
index_name = name or f"idx_{self.table_name}_{'_'.join(columns)}"
|
242
|
+
index = Index(index_name, *columns, **kwargs)
|
243
|
+
self.indexes.append(index)
|
244
|
+
return self
|
245
|
+
|
246
|
+
def add_primary_key(self, columns: Union[str, List[str]]):
|
247
|
+
"""Add a primary key constraint."""
|
248
|
+
if isinstance(columns, str):
|
249
|
+
columns = [columns]
|
250
|
+
constraint = PrimaryKeyConstraint(*columns)
|
251
|
+
self.constraints.append(constraint)
|
252
|
+
return self
|
253
|
+
|
254
|
+
def add_foreign_key(
|
255
|
+
self,
|
256
|
+
columns: Union[str, List[str]],
|
257
|
+
ref_table: str,
|
258
|
+
ref_columns: Union[str, List[str]],
|
259
|
+
name: Optional[str] = None,
|
260
|
+
):
|
261
|
+
"""Add a foreign key constraint."""
|
262
|
+
if isinstance(columns, str):
|
263
|
+
columns = [columns]
|
264
|
+
if isinstance(ref_columns, str):
|
265
|
+
ref_columns = [ref_columns]
|
266
|
+
|
267
|
+
constraint_name = name or f"fk_{self.table_name}_{'_'.join(columns)}"
|
268
|
+
constraint = ForeignKeyConstraint(columns, [f"{ref_table}.{col}" for col in ref_columns], name=constraint_name)
|
269
|
+
self.constraints.append(constraint)
|
270
|
+
return self
|
271
|
+
|
272
|
+
def build(self) -> Table:
|
273
|
+
"""Build and return the SQLAlchemy Table object."""
|
274
|
+
return Table(self.table_name, self.metadata, *self.columns, *self.indexes, *self.constraints)
|
275
|
+
|
276
|
+
|
277
|
+
def create_vector_table(table_name: str, metadata: MetaData = None) -> VectorTableBuilder:
|
278
|
+
"""
|
279
|
+
Create a new vector table builder.
|
280
|
+
|
281
|
+
Args:
|
282
|
+
|
283
|
+
table_name: Name of the table
|
284
|
+
metadata: SQLAlchemy metadata object
|
285
|
+
|
286
|
+
Returns:
|
287
|
+
|
288
|
+
VectorTableBuilder instance
|
289
|
+
"""
|
290
|
+
return VectorTableBuilder(table_name, metadata)
|
291
|
+
|
292
|
+
|
293
|
+
# Convenience functions for common table patterns
|
294
|
+
def create_vector_index_table(table_name: str, metadata: MetaData = None) -> VectorTableBuilder:
|
295
|
+
"""
|
296
|
+
Create a table builder for vector index tables.
|
297
|
+
|
298
|
+
Example: create table vector_index_07(a int primary key, b vecf32(128), c int, key c_k(c))
|
299
|
+
"""
|
300
|
+
builder = VectorTableBuilder(table_name, metadata)
|
301
|
+
|
302
|
+
# Add common columns
|
303
|
+
builder.add_int_column("a", primary_key=True)
|
304
|
+
builder.add_vecf32_column("b", dimension=128)
|
305
|
+
builder.add_int_column("c")
|
306
|
+
|
307
|
+
# Add index on column c
|
308
|
+
builder.add_index("c", name="c_k")
|
309
|
+
|
310
|
+
return builder
|
311
|
+
|
312
|
+
|
313
|
+
def create_document_vector_table(table_name: str, metadata: MetaData = None, vector_dim: int = 384) -> VectorTableBuilder:
|
314
|
+
"""
|
315
|
+
Create a table builder for document vector storage.
|
316
|
+
"""
|
317
|
+
builder = VectorTableBuilder(table_name, metadata)
|
318
|
+
|
319
|
+
builder.add_int_column("id", primary_key=True, autoincrement=True)
|
320
|
+
builder.add_string_column("document_id", length=100, nullable=False, unique=True)
|
321
|
+
builder.add_string_column("title", length=255)
|
322
|
+
builder.add_text_column("content")
|
323
|
+
builder.add_vecf32_column("embedding", dimension=vector_dim, nullable=False)
|
324
|
+
builder.add_json_column("metadata")
|
325
|
+
|
326
|
+
# Add indexes
|
327
|
+
builder.add_index("document_id")
|
328
|
+
builder.add_index("title")
|
329
|
+
|
330
|
+
return builder
|
331
|
+
|
332
|
+
|
333
|
+
def create_product_vector_table(table_name: str, metadata: MetaData = None, vector_dim: int = 512) -> VectorTableBuilder:
|
334
|
+
"""
|
335
|
+
Create a table builder for product vector storage.
|
336
|
+
"""
|
337
|
+
builder = VectorTableBuilder(table_name, metadata)
|
338
|
+
|
339
|
+
builder.add_int_column("id", primary_key=True, autoincrement=True)
|
340
|
+
builder.add_string_column("product_id", length=50, nullable=False, unique=True)
|
341
|
+
builder.add_string_column("name", length=200)
|
342
|
+
builder.add_text_column("description")
|
343
|
+
builder.add_vecf32_column("embedding", dimension=vector_dim, nullable=False)
|
344
|
+
builder.add_string_column("category", length=100)
|
345
|
+
builder.add_string_column("price", length=20)
|
346
|
+
|
347
|
+
# Add indexes
|
348
|
+
builder.add_index("product_id")
|
349
|
+
builder.add_index("category")
|
350
|
+
|
351
|
+
return builder
|