singlestoredb 0.4.0__py3-none-any.whl → 1.0.4__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.
Potentially problematic release.
This version of singlestoredb might be problematic. Click here for more details.
- singlestoredb/__init__.py +33 -1
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/auth.py +5 -1
- singlestoredb/config.py +116 -14
- singlestoredb/connection.py +483 -516
- singlestoredb/converters.py +238 -135
- singlestoredb/exceptions.py +30 -2
- singlestoredb/functions/__init__.py +1 -0
- singlestoredb/functions/decorator.py +142 -0
- singlestoredb/functions/dtypes.py +1639 -0
- singlestoredb/functions/ext/__init__.py +2 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +661 -0
- singlestoredb/functions/ext/json.py +427 -0
- singlestoredb/functions/ext/mmap.py +306 -0
- singlestoredb/functions/ext/rowdat_1.py +744 -0
- singlestoredb/functions/signature.py +673 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +621 -0
- singlestoredb/fusion/handlers/stage.py +257 -0
- singlestoredb/fusion/handlers/utils.py +162 -0
- singlestoredb/fusion/handlers/workspace.py +412 -0
- singlestoredb/fusion/registry.py +164 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/{http.py → http/connection.py} +555 -154
- singlestoredb/management/__init__.py +3 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +14 -6
- singlestoredb/management/manager.py +100 -38
- singlestoredb/management/organization.py +188 -0
- singlestoredb/management/region.py +5 -5
- singlestoredb/management/utils.py +281 -2
- singlestoredb/management/workspace.py +1344 -49
- singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
- singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
- singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
- singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
- singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
- singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
- singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
- singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
- singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
- singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
- singlestoredb/pytest.py +283 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +385 -0
- singlestoredb/tests/test.sql +210 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +482 -115
- singlestoredb/tests/test_config.py +13 -13
- singlestoredb/tests/test_connection.py +241 -305
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_ext_func.py +1193 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +465 -0
- singlestoredb/tests/test_http.py +32 -26
- singlestoredb/tests/test_management.py +588 -8
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +11 -12
- singlestoredb/tests/test_udf.py +687 -0
- singlestoredb/tests/utils.py +3 -2
- singlestoredb/utils/config.py +58 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +4 -1
- singlestoredb-1.0.4.dist-info/METADATA +139 -0
- singlestoredb-1.0.4.dist-info/RECORD +112 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
- singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
- singlestoredb/clients/pymysqlsv/converters.py +0 -365
- singlestoredb/clients/pymysqlsv/err.py +0 -144
- singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
- singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
- singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
- singlestoredb/drivers/__init__.py +0 -45
- singlestoredb/drivers/base.py +0 -198
- singlestoredb/drivers/cymysql.py +0 -38
- singlestoredb/drivers/http.py +0 -47
- singlestoredb/drivers/mariadb.py +0 -40
- singlestoredb/drivers/mysqlconnector.py +0 -49
- singlestoredb/drivers/mysqldb.py +0 -60
- singlestoredb/drivers/pymysql.py +0 -37
- singlestoredb/drivers/pymysqlsv.py +0 -35
- singlestoredb/drivers/pyodbc.py +0 -65
- singlestoredb-0.4.0.dist-info/METADATA +0 -111
- singlestoredb-0.4.0.dist-info/RECORD +0 -86
- /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
- /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
- {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import datetime
|
|
3
|
+
import inspect
|
|
4
|
+
import numbers
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import string
|
|
8
|
+
import typing
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing import Callable
|
|
11
|
+
from typing import Dict
|
|
12
|
+
from typing import List
|
|
13
|
+
from typing import Optional
|
|
14
|
+
from typing import Sequence
|
|
15
|
+
from typing import Tuple
|
|
16
|
+
from typing import TypeVar
|
|
17
|
+
from typing import Union
|
|
18
|
+
|
|
19
|
+
try:
|
|
20
|
+
import numpy as np
|
|
21
|
+
has_numpy = True
|
|
22
|
+
except ImportError:
|
|
23
|
+
has_numpy = False
|
|
24
|
+
|
|
25
|
+
from . import dtypes as dt
|
|
26
|
+
from ..mysql.converters import escape_item # type: ignore
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
array_types: Tuple[Any, ...]
|
|
30
|
+
|
|
31
|
+
if has_numpy:
|
|
32
|
+
array_types = (Sequence, np.ndarray)
|
|
33
|
+
numpy_type_map = {
|
|
34
|
+
np.integer: 'int64',
|
|
35
|
+
np.int_: 'int64',
|
|
36
|
+
np.int64: 'int64',
|
|
37
|
+
np.int32: 'int32',
|
|
38
|
+
np.int16: 'int16',
|
|
39
|
+
np.int8: 'int8',
|
|
40
|
+
np.uint: 'uint64',
|
|
41
|
+
np.unsignedinteger: 'uint64',
|
|
42
|
+
np.uint64: 'uint64',
|
|
43
|
+
np.uint32: 'uint32',
|
|
44
|
+
np.uint16: 'uint16',
|
|
45
|
+
np.uint8: 'uint8',
|
|
46
|
+
np.longlong: 'uint64',
|
|
47
|
+
np.ulonglong: 'uint64',
|
|
48
|
+
np.unicode_: 'str',
|
|
49
|
+
np.str_: 'str',
|
|
50
|
+
np.bytes_: 'bytes',
|
|
51
|
+
np.float_: 'float64',
|
|
52
|
+
np.float64: 'float64',
|
|
53
|
+
np.float32: 'float32',
|
|
54
|
+
np.float16: 'float16',
|
|
55
|
+
np.double: 'float64',
|
|
56
|
+
}
|
|
57
|
+
else:
|
|
58
|
+
array_types = (Sequence,)
|
|
59
|
+
numpy_type_map = {}
|
|
60
|
+
|
|
61
|
+
float_type_map = {
|
|
62
|
+
'float': 'float64',
|
|
63
|
+
'float_': 'float64',
|
|
64
|
+
'float64': 'float64',
|
|
65
|
+
'f8': 'float64',
|
|
66
|
+
'double': 'float64',
|
|
67
|
+
'float32': 'float32',
|
|
68
|
+
'f4': 'float32',
|
|
69
|
+
'float16': 'float16',
|
|
70
|
+
'f2': 'float16',
|
|
71
|
+
'float8': 'float8',
|
|
72
|
+
'f1': 'float8',
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
int_type_map = {
|
|
76
|
+
'int': 'int64',
|
|
77
|
+
'integer': 'int64',
|
|
78
|
+
'int_': 'int64',
|
|
79
|
+
'int64': 'int64',
|
|
80
|
+
'i8': 'int64',
|
|
81
|
+
'int32': 'int32',
|
|
82
|
+
'i4': 'int32',
|
|
83
|
+
'int16': 'int16',
|
|
84
|
+
'i2': 'int16',
|
|
85
|
+
'int8': 'int8',
|
|
86
|
+
'i1': 'int8',
|
|
87
|
+
'uint': 'uint64',
|
|
88
|
+
'uinteger': 'uint64',
|
|
89
|
+
'uint_': 'uint64',
|
|
90
|
+
'uint64': 'uint64',
|
|
91
|
+
'u8': 'uint64',
|
|
92
|
+
'uint32': 'uint32',
|
|
93
|
+
'u4': 'uint32',
|
|
94
|
+
'uint16': 'uint16',
|
|
95
|
+
'u2': 'uint16',
|
|
96
|
+
'uint8': 'uint8',
|
|
97
|
+
'u1': 'uint8',
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
sql_type_map = {
|
|
101
|
+
'bool': 'BOOL',
|
|
102
|
+
'int8': 'TINYINT',
|
|
103
|
+
'int16': 'SMALLINT',
|
|
104
|
+
'int32': 'INT',
|
|
105
|
+
'int64': 'BIGINT',
|
|
106
|
+
'uint8': 'TINYINT UNSIGNED',
|
|
107
|
+
'uint16': 'SMALLINT UNSIGNED',
|
|
108
|
+
'uint32': 'INT UNSIGNED',
|
|
109
|
+
'uint64': 'BIGINT UNSIGNED',
|
|
110
|
+
'float32': 'FLOAT',
|
|
111
|
+
'float64': 'DOUBLE',
|
|
112
|
+
'str': 'TEXT',
|
|
113
|
+
'bytes': 'BLOB',
|
|
114
|
+
'null': 'NULL',
|
|
115
|
+
'datetime': 'DATETIME',
|
|
116
|
+
'datetime6': 'DATETIME(6)',
|
|
117
|
+
'date': 'DATE',
|
|
118
|
+
'time': 'TIME',
|
|
119
|
+
'time6': 'TIME(6)',
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
sql_to_type_map = {
|
|
123
|
+
'BOOL': 'bool',
|
|
124
|
+
'TINYINT': 'int8',
|
|
125
|
+
'TINYINT UNSIGNED': 'uint8',
|
|
126
|
+
'SMALLINT': 'int16',
|
|
127
|
+
'SMALLINT UNSIGNED': 'int16',
|
|
128
|
+
'MEDIUMINT': 'int32',
|
|
129
|
+
'MEDIUMINT UNSIGNED': 'int32',
|
|
130
|
+
'INT24': 'int32',
|
|
131
|
+
'INT24 UNSIGNED': 'int32',
|
|
132
|
+
'INT': 'int32',
|
|
133
|
+
'INT UNSIGNED': 'int32',
|
|
134
|
+
'INTEGER': 'int32',
|
|
135
|
+
'INTEGER UNSIGNED': 'int32',
|
|
136
|
+
'BIGINT': 'int64',
|
|
137
|
+
'BIGINT UNSIGNED': 'int64',
|
|
138
|
+
'FLOAT': 'float32',
|
|
139
|
+
'DOUBLE': 'float64',
|
|
140
|
+
'REAL': 'float64',
|
|
141
|
+
'DATE': 'date',
|
|
142
|
+
'TIME': 'time',
|
|
143
|
+
'TIME(6)': 'time6',
|
|
144
|
+
'DATETIME': 'datetime',
|
|
145
|
+
'DATETIME(6)': 'datetime',
|
|
146
|
+
'TIMESTAMP': 'datetime',
|
|
147
|
+
'TIMESTAMP(6)': 'datetime',
|
|
148
|
+
'YEAR': 'uint64',
|
|
149
|
+
'CHAR': 'str',
|
|
150
|
+
'VARCHAR': 'str',
|
|
151
|
+
'TEXT': 'str',
|
|
152
|
+
'TINYTEXT': 'str',
|
|
153
|
+
'MEDIUMTEXT': 'str',
|
|
154
|
+
'LONGTEXT': 'str',
|
|
155
|
+
'BINARY': 'bytes',
|
|
156
|
+
'VARBINARY': 'bytes',
|
|
157
|
+
'BLOB': 'bytes',
|
|
158
|
+
'TINYBLOB': 'bytes',
|
|
159
|
+
'MEDIUMBLOB': 'bytes',
|
|
160
|
+
'LONGBLOB': 'bytes',
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
class Collection:
|
|
165
|
+
"""Base class for collection data types."""
|
|
166
|
+
|
|
167
|
+
def __init__(self, *item_dtypes: Union[List[type], type]):
|
|
168
|
+
self.item_dtypes = item_dtypes
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
class TupleCollection(Collection):
|
|
172
|
+
pass
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
class ArrayCollection(Collection):
|
|
176
|
+
pass
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def escape_name(name: str) -> str:
|
|
180
|
+
"""Escape a function parameter name."""
|
|
181
|
+
if '`' in name:
|
|
182
|
+
name = name.replace('`', '``')
|
|
183
|
+
return f'`{name}`'
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def simplify_dtype(dtype: Any) -> List[Any]:
|
|
187
|
+
"""
|
|
188
|
+
Expand a type annotation to a flattened list of atomic types.
|
|
189
|
+
|
|
190
|
+
Parameters
|
|
191
|
+
----------
|
|
192
|
+
dtype : Any
|
|
193
|
+
Python type annotation
|
|
194
|
+
|
|
195
|
+
Returns
|
|
196
|
+
-------
|
|
197
|
+
List[Any] -- list of dtype strings, TupleCollections, and ArrayCollections
|
|
198
|
+
|
|
199
|
+
"""
|
|
200
|
+
origin = typing.get_origin(dtype)
|
|
201
|
+
atype = type(dtype)
|
|
202
|
+
args = []
|
|
203
|
+
|
|
204
|
+
# Flatten Unions
|
|
205
|
+
if origin is Union:
|
|
206
|
+
for x in typing.get_args(dtype):
|
|
207
|
+
args.extend(simplify_dtype(x))
|
|
208
|
+
|
|
209
|
+
# Expand custom types to individual types (does not support bounds)
|
|
210
|
+
elif atype is TypeVar:
|
|
211
|
+
for x in dtype.__constraints__:
|
|
212
|
+
args.extend(simplify_dtype(x))
|
|
213
|
+
if dtype.__bound__:
|
|
214
|
+
args.extend(simplify_dtype(dtype.__bound__))
|
|
215
|
+
|
|
216
|
+
# Sequence types
|
|
217
|
+
elif origin is not None and issubclass(origin, Sequence):
|
|
218
|
+
item_args: List[Union[List[type], type]] = []
|
|
219
|
+
for x in typing.get_args(dtype):
|
|
220
|
+
item_dtype = simplify_dtype(x)
|
|
221
|
+
if len(item_dtype) == 1:
|
|
222
|
+
item_args.append(item_dtype[0])
|
|
223
|
+
else:
|
|
224
|
+
item_args.append(item_dtype)
|
|
225
|
+
if origin is tuple or origin is Tuple:
|
|
226
|
+
args.append(TupleCollection(*item_args))
|
|
227
|
+
elif len(item_args) > 1:
|
|
228
|
+
raise TypeError('sequence types may only contain one item data type')
|
|
229
|
+
else:
|
|
230
|
+
args.append(ArrayCollection(*item_args))
|
|
231
|
+
|
|
232
|
+
# Not a Union or TypeVar
|
|
233
|
+
else:
|
|
234
|
+
args.append(dtype)
|
|
235
|
+
|
|
236
|
+
return args
|
|
237
|
+
|
|
238
|
+
|
|
239
|
+
def classify_dtype(dtype: Any) -> str:
|
|
240
|
+
"""Classify the type annotation into a type name."""
|
|
241
|
+
if isinstance(dtype, list):
|
|
242
|
+
return '|'.join(classify_dtype(x) for x in dtype)
|
|
243
|
+
|
|
244
|
+
# Specific types
|
|
245
|
+
if dtype is None or dtype is type(None): # noqa: E721
|
|
246
|
+
return 'null'
|
|
247
|
+
if dtype is int:
|
|
248
|
+
return 'int64'
|
|
249
|
+
if dtype is float:
|
|
250
|
+
return 'float64'
|
|
251
|
+
if dtype is bool:
|
|
252
|
+
return 'bool'
|
|
253
|
+
|
|
254
|
+
if not inspect.isclass(dtype):
|
|
255
|
+
# Check for compound types
|
|
256
|
+
origin = typing.get_origin(dtype)
|
|
257
|
+
if origin is not None:
|
|
258
|
+
# Tuple type
|
|
259
|
+
if origin is Tuple:
|
|
260
|
+
args = typing.get_args(dtype)
|
|
261
|
+
item_dtypes = ','.join(classify_dtype(x) for x in args)
|
|
262
|
+
return f'tuple:{item_dtypes}'
|
|
263
|
+
|
|
264
|
+
# Array types
|
|
265
|
+
elif issubclass(origin, array_types):
|
|
266
|
+
args = typing.get_args(dtype)
|
|
267
|
+
item_dtype = classify_dtype(args[0])
|
|
268
|
+
return f'array[{item_dtype}]'
|
|
269
|
+
|
|
270
|
+
raise TypeError(f'unsupported type annotation: {dtype}')
|
|
271
|
+
|
|
272
|
+
if isinstance(dtype, ArrayCollection):
|
|
273
|
+
item_dtypes = ','.join(classify_dtype(x) for x in dtype.item_dtypes)
|
|
274
|
+
return f'array[{item_dtypes}]'
|
|
275
|
+
|
|
276
|
+
if isinstance(dtype, TupleCollection):
|
|
277
|
+
item_dtypes = ','.join(classify_dtype(x) for x in dtype.item_dtypes)
|
|
278
|
+
return f'tuple[{item_dtypes}]'
|
|
279
|
+
|
|
280
|
+
# Check numpy types if it's available
|
|
281
|
+
if dtype in numpy_type_map:
|
|
282
|
+
return numpy_type_map[dtype]
|
|
283
|
+
|
|
284
|
+
# Broad numeric types
|
|
285
|
+
if issubclass(dtype, int):
|
|
286
|
+
return 'int64'
|
|
287
|
+
if issubclass(dtype, float):
|
|
288
|
+
return 'float64'
|
|
289
|
+
|
|
290
|
+
# Strings / Bytes
|
|
291
|
+
if issubclass(dtype, str):
|
|
292
|
+
return 'str'
|
|
293
|
+
if issubclass(dtype, (bytes, bytearray)):
|
|
294
|
+
return 'bytes'
|
|
295
|
+
|
|
296
|
+
# Date / Times
|
|
297
|
+
if issubclass(dtype, datetime.datetime):
|
|
298
|
+
return 'datetime'
|
|
299
|
+
if issubclass(dtype, datetime.date):
|
|
300
|
+
return 'date'
|
|
301
|
+
if issubclass(dtype, datetime.timedelta):
|
|
302
|
+
return 'time'
|
|
303
|
+
|
|
304
|
+
# Last resort, guess it by the name...
|
|
305
|
+
name = dtype.__name__.lower()
|
|
306
|
+
is_float = issubclass(dtype, numbers.Real)
|
|
307
|
+
is_int = issubclass(dtype, numbers.Integral)
|
|
308
|
+
if is_float:
|
|
309
|
+
return float_type_map.get(name, 'float64')
|
|
310
|
+
if is_int:
|
|
311
|
+
return int_type_map.get(name, 'int64')
|
|
312
|
+
|
|
313
|
+
raise TypeError(f'unsupported type annotation: {dtype}')
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def collapse_dtypes(dtypes: Union[str, List[str]]) -> str:
|
|
317
|
+
"""
|
|
318
|
+
Collapse a dtype possibly containing multiple data types to one type.
|
|
319
|
+
|
|
320
|
+
Parameters
|
|
321
|
+
----------
|
|
322
|
+
dtypes : str or list[str]
|
|
323
|
+
The data types to collapse
|
|
324
|
+
|
|
325
|
+
Returns
|
|
326
|
+
-------
|
|
327
|
+
str
|
|
328
|
+
|
|
329
|
+
"""
|
|
330
|
+
if not isinstance(dtypes, list):
|
|
331
|
+
return dtypes
|
|
332
|
+
|
|
333
|
+
orig_dtypes = dtypes
|
|
334
|
+
dtypes = list(set(dtypes))
|
|
335
|
+
|
|
336
|
+
is_nullable = 'null' in dtypes
|
|
337
|
+
|
|
338
|
+
dtypes = [x for x in dtypes if x != 'null']
|
|
339
|
+
|
|
340
|
+
if 'uint64' in dtypes:
|
|
341
|
+
dtypes = [x for x in dtypes if x not in ('uint8', 'uint16', 'uint32')]
|
|
342
|
+
if 'uint32' in dtypes:
|
|
343
|
+
dtypes = [x for x in dtypes if x not in ('uint8', 'uint16')]
|
|
344
|
+
if 'uint16' in dtypes:
|
|
345
|
+
dtypes = [x for x in dtypes if x not in ('uint8',)]
|
|
346
|
+
|
|
347
|
+
if 'int64' in dtypes:
|
|
348
|
+
dtypes = [
|
|
349
|
+
x for x in dtypes if x not in (
|
|
350
|
+
'int8', 'int16', 'int32',
|
|
351
|
+
'uint8', 'uint16', 'uint32',
|
|
352
|
+
)
|
|
353
|
+
]
|
|
354
|
+
if 'int32' in dtypes:
|
|
355
|
+
dtypes = [
|
|
356
|
+
x for x in dtypes if x not in (
|
|
357
|
+
'int8', 'int16',
|
|
358
|
+
'uint8', 'uint16',
|
|
359
|
+
)
|
|
360
|
+
]
|
|
361
|
+
if 'int16' in dtypes:
|
|
362
|
+
dtypes = [x for x in dtypes if x not in ('int8', 'uint8')]
|
|
363
|
+
|
|
364
|
+
if 'float64' in dtypes:
|
|
365
|
+
dtypes = [
|
|
366
|
+
x for x in dtypes if x not in (
|
|
367
|
+
'float32',
|
|
368
|
+
'int8', 'int16', 'int32', 'int64',
|
|
369
|
+
'uint8', 'uint16', 'uint32', 'uint64',
|
|
370
|
+
)
|
|
371
|
+
]
|
|
372
|
+
if 'float32' in dtypes:
|
|
373
|
+
dtypes = [
|
|
374
|
+
x for x in dtypes if x not in (
|
|
375
|
+
'int8', 'int16', 'int32',
|
|
376
|
+
'uint8', 'uint16', 'uint32',
|
|
377
|
+
)
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
for i, item in enumerate(dtypes):
|
|
381
|
+
|
|
382
|
+
if item.startswith('array[') and '|' in item:
|
|
383
|
+
_, item_spec = item.split('[', 1)
|
|
384
|
+
item_spec = item_spec[:-1]
|
|
385
|
+
item = collapse_dtypes(item_spec.split('|'))
|
|
386
|
+
dtypes[i] = f'array[{item}]'
|
|
387
|
+
|
|
388
|
+
elif item.startswith('tuple[') and '|' in item:
|
|
389
|
+
_, item_spec = item.split('[', 1)
|
|
390
|
+
item_spec = item_spec[:-1]
|
|
391
|
+
sub_dtypes = []
|
|
392
|
+
for subitem_spec in item_spec.split(','):
|
|
393
|
+
item = collapse_dtypes(subitem_spec.split('|'))
|
|
394
|
+
sub_dtypes.append(item)
|
|
395
|
+
dtypes[i] = f'tuple[{",".join(sub_dtypes)}]'
|
|
396
|
+
|
|
397
|
+
if len(dtypes) > 1:
|
|
398
|
+
raise TypeError(
|
|
399
|
+
'types can not be collapsed to a single type: '
|
|
400
|
+
f'{", ".join(orig_dtypes)}',
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
if not dtypes:
|
|
404
|
+
return 'null'
|
|
405
|
+
|
|
406
|
+
return dtypes[0] + ('?' if is_nullable else '')
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def get_signature(func: Callable[..., Any], name: Optional[str] = None) -> Dict[str, Any]:
|
|
410
|
+
'''
|
|
411
|
+
Print the UDF signature of the Python callable.
|
|
412
|
+
|
|
413
|
+
Parameters
|
|
414
|
+
----------
|
|
415
|
+
func : Callable
|
|
416
|
+
The function to extract the signature of
|
|
417
|
+
name : str, optional
|
|
418
|
+
Name override for function
|
|
419
|
+
|
|
420
|
+
Returns
|
|
421
|
+
-------
|
|
422
|
+
Dict[str, Any]
|
|
423
|
+
|
|
424
|
+
'''
|
|
425
|
+
signature = inspect.signature(func)
|
|
426
|
+
args: List[Dict[str, Any]] = []
|
|
427
|
+
attrs = getattr(func, '_singlestoredb_attrs', {})
|
|
428
|
+
name = attrs.get('name', name if name else func.__name__)
|
|
429
|
+
out: Dict[str, Any] = dict(name=name, args=args)
|
|
430
|
+
|
|
431
|
+
arg_names = [x for x in signature.parameters]
|
|
432
|
+
defaults = [
|
|
433
|
+
x.default if x.default is not inspect.Parameter.empty else None
|
|
434
|
+
for x in signature.parameters.values()
|
|
435
|
+
]
|
|
436
|
+
annotations = {
|
|
437
|
+
k: x.annotation for k, x in signature.parameters.items()
|
|
438
|
+
if x.annotation is not inspect.Parameter.empty
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
for p in signature.parameters.values():
|
|
442
|
+
if p.kind == inspect.Parameter.VAR_POSITIONAL:
|
|
443
|
+
raise TypeError('variable positional arguments are not supported')
|
|
444
|
+
elif p.kind == inspect.Parameter.VAR_KEYWORD:
|
|
445
|
+
raise TypeError('variable keyword arguments are not supported')
|
|
446
|
+
|
|
447
|
+
args_overrides = attrs.get('args', None)
|
|
448
|
+
returns_overrides = attrs.get('returns', None)
|
|
449
|
+
|
|
450
|
+
spec_diff = set(arg_names).difference(set(annotations.keys()))
|
|
451
|
+
|
|
452
|
+
# Make sure all arguments are annotated
|
|
453
|
+
if spec_diff and args_overrides is None:
|
|
454
|
+
raise TypeError(
|
|
455
|
+
'missing annotations for {} in {}'
|
|
456
|
+
.format(', '.join(spec_diff), name),
|
|
457
|
+
)
|
|
458
|
+
elif isinstance(args_overrides, dict):
|
|
459
|
+
for s in spec_diff:
|
|
460
|
+
if s not in args_overrides:
|
|
461
|
+
raise TypeError(
|
|
462
|
+
'missing annotations for {} in {}'
|
|
463
|
+
.format(', '.join(spec_diff), name),
|
|
464
|
+
)
|
|
465
|
+
elif isinstance(args_overrides, list):
|
|
466
|
+
if len(arg_names) != len(args_overrides):
|
|
467
|
+
raise TypeError(
|
|
468
|
+
'number of annotations does not match in {}: {}'
|
|
469
|
+
.format(name, ', '.join(spec_diff)),
|
|
470
|
+
)
|
|
471
|
+
|
|
472
|
+
for i, arg in enumerate(arg_names):
|
|
473
|
+
if isinstance(args_overrides, list):
|
|
474
|
+
sql = args_overrides[i]
|
|
475
|
+
arg_type = sql_to_dtype(sql)
|
|
476
|
+
elif isinstance(args_overrides, dict) and arg in args_overrides:
|
|
477
|
+
sql = args_overrides[arg]
|
|
478
|
+
arg_type = sql_to_dtype(sql)
|
|
479
|
+
elif isinstance(args_overrides, str):
|
|
480
|
+
sql = args_overrides
|
|
481
|
+
arg_type = sql_to_dtype(sql)
|
|
482
|
+
elif args_overrides is not None \
|
|
483
|
+
and not isinstance(args_overrides, (list, dict, str)):
|
|
484
|
+
raise TypeError(f'unrecognized type for arguments: {args_overrides}')
|
|
485
|
+
else:
|
|
486
|
+
arg_type = collapse_dtypes([
|
|
487
|
+
classify_dtype(x) for x in simplify_dtype(annotations[arg])
|
|
488
|
+
])
|
|
489
|
+
sql = dtype_to_sql(arg_type)
|
|
490
|
+
args.append(dict(name=arg, dtype=arg_type, sql=sql, default=defaults[i]))
|
|
491
|
+
|
|
492
|
+
if returns_overrides is None \
|
|
493
|
+
and signature.return_annotation is inspect.Signature.empty:
|
|
494
|
+
raise TypeError(f'no return value annotation in function {name}')
|
|
495
|
+
|
|
496
|
+
if isinstance(returns_overrides, str):
|
|
497
|
+
sql = returns_overrides
|
|
498
|
+
out_type = sql_to_dtype(sql)
|
|
499
|
+
elif returns_overrides is not None and not isinstance(returns_overrides, str):
|
|
500
|
+
raise TypeError(f'unrecognized type for return value: {returns_overrides}')
|
|
501
|
+
else:
|
|
502
|
+
out_type = collapse_dtypes([
|
|
503
|
+
classify_dtype(x) for x in simplify_dtype(signature.return_annotation)
|
|
504
|
+
])
|
|
505
|
+
sql = dtype_to_sql(out_type)
|
|
506
|
+
out['returns'] = dict(dtype=out_type, sql=sql, default=None)
|
|
507
|
+
|
|
508
|
+
copied_keys = ['database', 'environment', 'packages', 'resources', 'replace']
|
|
509
|
+
for key in copied_keys:
|
|
510
|
+
if attrs.get(key):
|
|
511
|
+
out[key] = attrs[key]
|
|
512
|
+
|
|
513
|
+
out['endpoint'] = '/invoke'
|
|
514
|
+
out['doc'] = func.__doc__
|
|
515
|
+
|
|
516
|
+
return out
|
|
517
|
+
|
|
518
|
+
|
|
519
|
+
def sql_to_dtype(sql: str) -> str:
|
|
520
|
+
"""
|
|
521
|
+
Convert a SQL type into a normalized data type identifier.
|
|
522
|
+
|
|
523
|
+
Parameters
|
|
524
|
+
----------
|
|
525
|
+
sql : str
|
|
526
|
+
SQL data type specification
|
|
527
|
+
|
|
528
|
+
Returns
|
|
529
|
+
-------
|
|
530
|
+
str
|
|
531
|
+
|
|
532
|
+
"""
|
|
533
|
+
sql = re.sub(r'\s+', r' ', sql.upper().strip())
|
|
534
|
+
|
|
535
|
+
m = re.match(r'(\w+)(\([^\)]+\))?', sql)
|
|
536
|
+
if not m:
|
|
537
|
+
raise TypeError(f'unrecognized data type: {sql}')
|
|
538
|
+
|
|
539
|
+
sql_type = m.group(1)
|
|
540
|
+
type_attrs = re.split(r'\s*,\s*', m.group(2) or '')
|
|
541
|
+
|
|
542
|
+
if sql_type in ('DATETIME', 'TIME', 'TIMESTAMP') and \
|
|
543
|
+
type_attrs and type_attrs[0] == '6':
|
|
544
|
+
sql_type += '(6)'
|
|
545
|
+
|
|
546
|
+
elif ' UNSIGNED' in sql:
|
|
547
|
+
sql_type += ' UNSIGNED'
|
|
548
|
+
|
|
549
|
+
try:
|
|
550
|
+
dtype = sql_to_type_map[sql_type]
|
|
551
|
+
except KeyError:
|
|
552
|
+
raise TypeError(f'unrecognized data type: {sql_type}')
|
|
553
|
+
|
|
554
|
+
if ' NOT NULL' not in sql:
|
|
555
|
+
dtype += '?'
|
|
556
|
+
|
|
557
|
+
return dtype
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def dtype_to_sql(dtype: str, default: Any = None) -> str:
|
|
561
|
+
"""
|
|
562
|
+
Convert a collapsed dtype string to a SQL type.
|
|
563
|
+
|
|
564
|
+
Parameters
|
|
565
|
+
----------
|
|
566
|
+
dtype : str
|
|
567
|
+
Simplified data type string
|
|
568
|
+
default : Any, optional
|
|
569
|
+
Default value
|
|
570
|
+
|
|
571
|
+
Returns
|
|
572
|
+
-------
|
|
573
|
+
str
|
|
574
|
+
|
|
575
|
+
"""
|
|
576
|
+
nullable = ' NOT NULL'
|
|
577
|
+
if dtype.endswith('?'):
|
|
578
|
+
nullable = ' NULL'
|
|
579
|
+
dtype = dtype[:-1]
|
|
580
|
+
|
|
581
|
+
if dtype == 'null':
|
|
582
|
+
nullable = ''
|
|
583
|
+
|
|
584
|
+
default_clause = ''
|
|
585
|
+
if default is not None:
|
|
586
|
+
if default is dt.NULL:
|
|
587
|
+
default = None
|
|
588
|
+
default_clause = f' DEFAULT {escape_item(default, "utf8")}'
|
|
589
|
+
|
|
590
|
+
if dtype.startswith('array['):
|
|
591
|
+
_, dtypes = dtype.split('[', 1)
|
|
592
|
+
dtypes = dtypes[:-1]
|
|
593
|
+
item_dtype = dtype_to_sql(dtypes)
|
|
594
|
+
return f'ARRAY({item_dtype}){nullable}{default_clause}'
|
|
595
|
+
|
|
596
|
+
if dtype.startswith('tuple['):
|
|
597
|
+
_, dtypes = dtype.split('[', 1)
|
|
598
|
+
dtypes = dtypes[:-1]
|
|
599
|
+
item_dtypes = []
|
|
600
|
+
for i, item in enumerate(dtypes.split(',')):
|
|
601
|
+
name = string.ascii_letters[i]
|
|
602
|
+
if '=' in item:
|
|
603
|
+
name, item = item.split('=', 1)
|
|
604
|
+
item_dtypes.append(name + ' ' + dtype_to_sql(item))
|
|
605
|
+
return f'RECORD({", ".join(item_dtypes)}){nullable}{default_clause}'
|
|
606
|
+
|
|
607
|
+
return f'{sql_type_map[dtype]}{nullable}{default_clause}'
|
|
608
|
+
|
|
609
|
+
|
|
610
|
+
def signature_to_sql(
|
|
611
|
+
signature: Dict[str, Any],
|
|
612
|
+
url: Optional[str] = None,
|
|
613
|
+
data_format: str = 'rowdat_1',
|
|
614
|
+
app_mode: str = 'remote',
|
|
615
|
+
link: Optional[str] = None,
|
|
616
|
+
replace: bool = False,
|
|
617
|
+
) -> str:
|
|
618
|
+
'''
|
|
619
|
+
Convert a dictionary function signature into SQL.
|
|
620
|
+
|
|
621
|
+
Parameters
|
|
622
|
+
----------
|
|
623
|
+
signature : Dict[str, Any]
|
|
624
|
+
Function signature in the form of a dictionary as returned by
|
|
625
|
+
the `get_signature` function
|
|
626
|
+
|
|
627
|
+
Returns
|
|
628
|
+
-------
|
|
629
|
+
str : SQL formatted function signature
|
|
630
|
+
|
|
631
|
+
'''
|
|
632
|
+
args = []
|
|
633
|
+
for arg in signature['args']:
|
|
634
|
+
# Use default value from Python function if SQL doesn't set one
|
|
635
|
+
default = ''
|
|
636
|
+
if not re.search(r'\s+default\s+\S', arg['sql'], flags=re.I):
|
|
637
|
+
default = ''
|
|
638
|
+
if arg.get('default', None) is not None:
|
|
639
|
+
default = f' DEFAULT {escape_item(arg["default"], "utf8")}'
|
|
640
|
+
args.append(escape_name(arg['name']) + ' ' + arg['sql'] + default)
|
|
641
|
+
|
|
642
|
+
returns = ''
|
|
643
|
+
if signature.get('returns'):
|
|
644
|
+
res = signature['returns']['sql']
|
|
645
|
+
returns = f' RETURNS {res}'
|
|
646
|
+
|
|
647
|
+
host = os.environ.get('SINGLESTOREDB_EXT_HOST', '127.0.0.1')
|
|
648
|
+
port = os.environ.get('SINGLESTOREDB_EXT_PORT', '8000')
|
|
649
|
+
|
|
650
|
+
if app_mode.lower() == 'remote':
|
|
651
|
+
url = url or f'https://{host}:{port}/invoke'
|
|
652
|
+
elif url is None:
|
|
653
|
+
raise ValueError('url can not be `None`')
|
|
654
|
+
|
|
655
|
+
database = ''
|
|
656
|
+
if signature.get('database'):
|
|
657
|
+
database = escape_name(signature['database']) + '.'
|
|
658
|
+
|
|
659
|
+
or_replace = 'OR REPLACE ' if (bool(signature.get('replace')) or replace) else ''
|
|
660
|
+
|
|
661
|
+
link_str = ''
|
|
662
|
+
if link:
|
|
663
|
+
if not re.match(r'^[\w_]+$', link):
|
|
664
|
+
raise ValueError(f'invalid LINK name: {link}')
|
|
665
|
+
link_str = f' LINK {link}'
|
|
666
|
+
|
|
667
|
+
return (
|
|
668
|
+
f'CREATE {or_replace}EXTERNAL FUNCTION ' +
|
|
669
|
+
f'{database}{escape_name(signature["name"])}' +
|
|
670
|
+
'(' + ', '.join(args) + ')' + returns +
|
|
671
|
+
f' AS {app_mode.upper()} SERVICE "{url}" FORMAT {data_format.upper()}'
|
|
672
|
+
f'{link_str};'
|
|
673
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import importlib
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
from .registry import execute
|
|
6
|
+
from .registry import get_handler
|
|
7
|
+
|
|
8
|
+
# Load all files in handlers directory
|
|
9
|
+
for f in os.listdir(os.path.join(os.path.dirname(__file__), 'handlers')):
|
|
10
|
+
if f.endswith('.py') and not f.startswith('_'):
|
|
11
|
+
importlib.import_module(f'singlestoredb.fusion.handlers.{f[:-3]}')
|