singlestoredb 1.16.1__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.
- singlestoredb/__init__.py +75 -0
- singlestoredb/ai/__init__.py +2 -0
- singlestoredb/ai/chat.py +139 -0
- singlestoredb/ai/embeddings.py +128 -0
- singlestoredb/alchemy/__init__.py +90 -0
- singlestoredb/apps/__init__.py +3 -0
- singlestoredb/apps/_cloud_functions.py +90 -0
- singlestoredb/apps/_config.py +72 -0
- singlestoredb/apps/_connection_info.py +18 -0
- singlestoredb/apps/_dashboards.py +47 -0
- singlestoredb/apps/_process.py +32 -0
- singlestoredb/apps/_python_udfs.py +100 -0
- singlestoredb/apps/_stdout_supress.py +30 -0
- singlestoredb/apps/_uvicorn_util.py +36 -0
- singlestoredb/auth.py +245 -0
- singlestoredb/config.py +484 -0
- singlestoredb/connection.py +1487 -0
- singlestoredb/converters.py +950 -0
- singlestoredb/docstring/__init__.py +33 -0
- singlestoredb/docstring/attrdoc.py +126 -0
- singlestoredb/docstring/common.py +230 -0
- singlestoredb/docstring/epydoc.py +267 -0
- singlestoredb/docstring/google.py +412 -0
- singlestoredb/docstring/numpydoc.py +562 -0
- singlestoredb/docstring/parser.py +100 -0
- singlestoredb/docstring/py.typed +1 -0
- singlestoredb/docstring/rest.py +256 -0
- singlestoredb/docstring/tests/__init__.py +1 -0
- singlestoredb/docstring/tests/_pydoctor.py +21 -0
- singlestoredb/docstring/tests/test_epydoc.py +729 -0
- singlestoredb/docstring/tests/test_google.py +1007 -0
- singlestoredb/docstring/tests/test_numpydoc.py +1100 -0
- singlestoredb/docstring/tests/test_parse_from_object.py +109 -0
- singlestoredb/docstring/tests/test_parser.py +248 -0
- singlestoredb/docstring/tests/test_rest.py +547 -0
- singlestoredb/docstring/tests/test_util.py +70 -0
- singlestoredb/docstring/util.py +141 -0
- singlestoredb/exceptions.py +120 -0
- singlestoredb/functions/__init__.py +16 -0
- singlestoredb/functions/decorator.py +201 -0
- singlestoredb/functions/dtypes.py +1793 -0
- singlestoredb/functions/ext/__init__.py +1 -0
- singlestoredb/functions/ext/arrow.py +375 -0
- singlestoredb/functions/ext/asgi.py +2133 -0
- singlestoredb/functions/ext/json.py +420 -0
- singlestoredb/functions/ext/mmap.py +413 -0
- singlestoredb/functions/ext/rowdat_1.py +724 -0
- singlestoredb/functions/ext/timer.py +89 -0
- singlestoredb/functions/ext/utils.py +218 -0
- singlestoredb/functions/signature.py +1578 -0
- singlestoredb/functions/typing/__init__.py +41 -0
- singlestoredb/functions/typing/numpy.py +20 -0
- singlestoredb/functions/typing/pandas.py +2 -0
- singlestoredb/functions/typing/polars.py +2 -0
- singlestoredb/functions/typing/pyarrow.py +2 -0
- singlestoredb/functions/utils.py +421 -0
- singlestoredb/fusion/__init__.py +11 -0
- singlestoredb/fusion/graphql.py +213 -0
- singlestoredb/fusion/handler.py +916 -0
- singlestoredb/fusion/handlers/__init__.py +0 -0
- singlestoredb/fusion/handlers/export.py +525 -0
- singlestoredb/fusion/handlers/files.py +690 -0
- singlestoredb/fusion/handlers/job.py +660 -0
- singlestoredb/fusion/handlers/models.py +250 -0
- singlestoredb/fusion/handlers/stage.py +502 -0
- singlestoredb/fusion/handlers/utils.py +324 -0
- singlestoredb/fusion/handlers/workspace.py +956 -0
- singlestoredb/fusion/registry.py +249 -0
- singlestoredb/fusion/result.py +399 -0
- singlestoredb/http/__init__.py +27 -0
- singlestoredb/http/connection.py +1267 -0
- singlestoredb/magics/__init__.py +34 -0
- singlestoredb/magics/run_personal.py +137 -0
- singlestoredb/magics/run_shared.py +134 -0
- singlestoredb/management/__init__.py +9 -0
- singlestoredb/management/billing_usage.py +148 -0
- singlestoredb/management/cluster.py +462 -0
- singlestoredb/management/export.py +295 -0
- singlestoredb/management/files.py +1102 -0
- singlestoredb/management/inference_api.py +105 -0
- singlestoredb/management/job.py +887 -0
- singlestoredb/management/manager.py +373 -0
- singlestoredb/management/organization.py +226 -0
- singlestoredb/management/region.py +169 -0
- singlestoredb/management/utils.py +423 -0
- singlestoredb/management/workspace.py +1927 -0
- singlestoredb/mysql/__init__.py +177 -0
- singlestoredb/mysql/_auth.py +298 -0
- singlestoredb/mysql/charset.py +214 -0
- singlestoredb/mysql/connection.py +2032 -0
- singlestoredb/mysql/constants/CLIENT.py +38 -0
- singlestoredb/mysql/constants/COMMAND.py +32 -0
- singlestoredb/mysql/constants/CR.py +78 -0
- singlestoredb/mysql/constants/ER.py +474 -0
- singlestoredb/mysql/constants/EXTENDED_TYPE.py +3 -0
- singlestoredb/mysql/constants/FIELD_TYPE.py +48 -0
- singlestoredb/mysql/constants/FLAG.py +15 -0
- singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
- singlestoredb/mysql/constants/VECTOR_TYPE.py +6 -0
- singlestoredb/mysql/constants/__init__.py +0 -0
- singlestoredb/mysql/converters.py +271 -0
- singlestoredb/mysql/cursors.py +896 -0
- singlestoredb/mysql/err.py +92 -0
- singlestoredb/mysql/optionfile.py +20 -0
- singlestoredb/mysql/protocol.py +450 -0
- singlestoredb/mysql/tests/__init__.py +19 -0
- singlestoredb/mysql/tests/base.py +126 -0
- singlestoredb/mysql/tests/conftest.py +37 -0
- singlestoredb/mysql/tests/test_DictCursor.py +132 -0
- singlestoredb/mysql/tests/test_SSCursor.py +141 -0
- singlestoredb/mysql/tests/test_basic.py +452 -0
- singlestoredb/mysql/tests/test_connection.py +851 -0
- singlestoredb/mysql/tests/test_converters.py +58 -0
- singlestoredb/mysql/tests/test_cursor.py +141 -0
- singlestoredb/mysql/tests/test_err.py +16 -0
- singlestoredb/mysql/tests/test_issues.py +514 -0
- singlestoredb/mysql/tests/test_load_local.py +75 -0
- singlestoredb/mysql/tests/test_nextset.py +88 -0
- singlestoredb/mysql/tests/test_optionfile.py +27 -0
- singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
- singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
- singlestoredb/mysql/times.py +23 -0
- singlestoredb/notebook/__init__.py +16 -0
- singlestoredb/notebook/_objects.py +213 -0
- singlestoredb/notebook/_portal.py +352 -0
- singlestoredb/py.typed +0 -0
- singlestoredb/pytest.py +352 -0
- singlestoredb/server/__init__.py +0 -0
- singlestoredb/server/docker.py +452 -0
- singlestoredb/server/free_tier.py +267 -0
- singlestoredb/tests/__init__.py +0 -0
- singlestoredb/tests/alltypes.sql +307 -0
- singlestoredb/tests/alltypes_no_nulls.sql +208 -0
- singlestoredb/tests/empty.sql +0 -0
- singlestoredb/tests/ext_funcs/__init__.py +702 -0
- singlestoredb/tests/local_infile.csv +3 -0
- singlestoredb/tests/test.ipynb +18 -0
- singlestoredb/tests/test.sql +680 -0
- singlestoredb/tests/test2.ipynb +18 -0
- singlestoredb/tests/test2.sql +1 -0
- singlestoredb/tests/test_basics.py +1332 -0
- singlestoredb/tests/test_config.py +318 -0
- singlestoredb/tests/test_connection.py +3103 -0
- singlestoredb/tests/test_dbapi.py +27 -0
- singlestoredb/tests/test_exceptions.py +45 -0
- singlestoredb/tests/test_ext_func.py +1472 -0
- singlestoredb/tests/test_ext_func_data.py +1101 -0
- singlestoredb/tests/test_fusion.py +1527 -0
- singlestoredb/tests/test_http.py +288 -0
- singlestoredb/tests/test_management.py +1599 -0
- singlestoredb/tests/test_plugin.py +33 -0
- singlestoredb/tests/test_results.py +171 -0
- singlestoredb/tests/test_types.py +132 -0
- singlestoredb/tests/test_udf.py +737 -0
- singlestoredb/tests/test_udf_returns.py +459 -0
- singlestoredb/tests/test_vectorstore.py +51 -0
- singlestoredb/tests/test_xdict.py +333 -0
- singlestoredb/tests/utils.py +141 -0
- singlestoredb/types.py +373 -0
- singlestoredb/utils/__init__.py +0 -0
- singlestoredb/utils/config.py +950 -0
- singlestoredb/utils/convert_rows.py +69 -0
- singlestoredb/utils/debug.py +13 -0
- singlestoredb/utils/dtypes.py +205 -0
- singlestoredb/utils/events.py +65 -0
- singlestoredb/utils/mogrify.py +151 -0
- singlestoredb/utils/results.py +585 -0
- singlestoredb/utils/xdict.py +425 -0
- singlestoredb/vectorstore.py +192 -0
- singlestoredb/warnings.py +5 -0
- singlestoredb-1.16.1.dist-info/METADATA +165 -0
- singlestoredb-1.16.1.dist-info/RECORD +183 -0
- singlestoredb-1.16.1.dist-info/WHEEL +5 -0
- singlestoredb-1.16.1.dist-info/entry_points.txt +2 -0
- singlestoredb-1.16.1.dist-info/licenses/LICENSE +201 -0
- singlestoredb-1.16.1.dist-info/top_level.txt +3 -0
- sqlx/__init__.py +4 -0
- sqlx/magic.py +113 -0
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import struct
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..exceptions import DatabaseError # noqa: F401
|
|
5
|
+
from ..exceptions import DataError # noqa: F401
|
|
6
|
+
from ..exceptions import Error # noqa: F401
|
|
7
|
+
from ..exceptions import IntegrityError # noqa: F401
|
|
8
|
+
from ..exceptions import InterfaceError # noqa: F401
|
|
9
|
+
from ..exceptions import InternalError # noqa: F401
|
|
10
|
+
from ..exceptions import MySQLError # noqa: F401
|
|
11
|
+
from ..exceptions import NotSupportedError # noqa: F401
|
|
12
|
+
from ..exceptions import OperationalError # noqa: F401
|
|
13
|
+
from ..exceptions import ProgrammingError # noqa: F401
|
|
14
|
+
from ..exceptions import Warning # noqa: F401
|
|
15
|
+
from .constants import ER
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
error_map = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _map_error(exc: Any, *errors: int) -> None:
|
|
22
|
+
for error in errors:
|
|
23
|
+
error_map[error] = exc
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_map_error(
|
|
27
|
+
ProgrammingError,
|
|
28
|
+
ER.DB_CREATE_EXISTS,
|
|
29
|
+
ER.SYNTAX_ERROR,
|
|
30
|
+
ER.PARSE_ERROR,
|
|
31
|
+
ER.NO_SUCH_TABLE,
|
|
32
|
+
ER.WRONG_DB_NAME,
|
|
33
|
+
ER.WRONG_TABLE_NAME,
|
|
34
|
+
ER.FIELD_SPECIFIED_TWICE,
|
|
35
|
+
ER.INVALID_GROUP_FUNC_USE,
|
|
36
|
+
ER.UNSUPPORTED_EXTENSION,
|
|
37
|
+
ER.TABLE_MUST_HAVE_COLUMNS,
|
|
38
|
+
ER.CANT_DO_THIS_DURING_AN_TRANSACTION,
|
|
39
|
+
ER.WRONG_DB_NAME,
|
|
40
|
+
ER.WRONG_COLUMN_NAME,
|
|
41
|
+
)
|
|
42
|
+
_map_error(
|
|
43
|
+
DataError,
|
|
44
|
+
ER.WARN_DATA_TRUNCATED,
|
|
45
|
+
ER.WARN_NULL_TO_NOTNULL,
|
|
46
|
+
ER.WARN_DATA_OUT_OF_RANGE,
|
|
47
|
+
ER.NO_DEFAULT,
|
|
48
|
+
ER.PRIMARY_CANT_HAVE_NULL,
|
|
49
|
+
ER.DATA_TOO_LONG,
|
|
50
|
+
ER.DATETIME_FUNCTION_OVERFLOW,
|
|
51
|
+
ER.TRUNCATED_WRONG_VALUE_FOR_FIELD,
|
|
52
|
+
ER.ILLEGAL_VALUE_FOR_TYPE,
|
|
53
|
+
)
|
|
54
|
+
_map_error(
|
|
55
|
+
IntegrityError,
|
|
56
|
+
ER.DUP_ENTRY,
|
|
57
|
+
ER.NO_REFERENCED_ROW,
|
|
58
|
+
ER.NO_REFERENCED_ROW_2,
|
|
59
|
+
ER.ROW_IS_REFERENCED,
|
|
60
|
+
ER.ROW_IS_REFERENCED_2,
|
|
61
|
+
ER.CANNOT_ADD_FOREIGN,
|
|
62
|
+
ER.BAD_NULL_ERROR,
|
|
63
|
+
)
|
|
64
|
+
_map_error(
|
|
65
|
+
NotSupportedError,
|
|
66
|
+
ER.WARNING_NOT_COMPLETE_ROLLBACK,
|
|
67
|
+
ER.NOT_SUPPORTED_YET,
|
|
68
|
+
ER.FEATURE_DISABLED,
|
|
69
|
+
ER.UNKNOWN_STORAGE_ENGINE,
|
|
70
|
+
)
|
|
71
|
+
_map_error(
|
|
72
|
+
OperationalError,
|
|
73
|
+
ER.DBACCESS_DENIED_ERROR,
|
|
74
|
+
ER.ACCESS_DENIED_ERROR,
|
|
75
|
+
ER.CON_COUNT_ERROR,
|
|
76
|
+
ER.TABLEACCESS_DENIED_ERROR,
|
|
77
|
+
ER.COLUMNACCESS_DENIED_ERROR,
|
|
78
|
+
ER.CONSTRAINT_FAILED,
|
|
79
|
+
ER.LOCK_DEADLOCK,
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
del _map_error, ER
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def raise_mysql_exception(data: bytes) -> Exception:
|
|
87
|
+
errno = struct.unpack('<h', data[1:3])[0]
|
|
88
|
+
errval = data[9:].decode('utf-8', 'replace')
|
|
89
|
+
errorclass = error_map.get(errno)
|
|
90
|
+
if errorclass is None:
|
|
91
|
+
errorclass = InternalError if errno < 1000 else OperationalError
|
|
92
|
+
raise errorclass(errno, errval)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import configparser
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
class Parser(configparser.RawConfigParser):
|
|
6
|
+
|
|
7
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
8
|
+
kwargs['allow_no_value'] = True
|
|
9
|
+
configparser.RawConfigParser.__init__(self, **kwargs)
|
|
10
|
+
|
|
11
|
+
def __remove_quotes(self, value: str) -> str:
|
|
12
|
+
quotes = ["'", '"']
|
|
13
|
+
for quote in quotes:
|
|
14
|
+
if len(value) >= 2 and value[0] == value[-1] == quote:
|
|
15
|
+
return value[1:-1]
|
|
16
|
+
return value
|
|
17
|
+
|
|
18
|
+
def get(self, section: str, option: str) -> str: # type: ignore
|
|
19
|
+
value = configparser.RawConfigParser.get(self, section, option)
|
|
20
|
+
return self.__remove_quotes(value)
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
# Python implementation of low level MySQL client-server protocol
|
|
3
|
+
# http://dev.mysql.com/doc/internals/en/client-server-protocol.html
|
|
4
|
+
import struct
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
from . import err
|
|
8
|
+
from ..config import get_option
|
|
9
|
+
from ..utils.results import Description
|
|
10
|
+
from .charset import MBLENGTH
|
|
11
|
+
from .constants import EXTENDED_TYPE
|
|
12
|
+
from .constants import FIELD_TYPE
|
|
13
|
+
from .constants import SERVER_STATUS
|
|
14
|
+
from .constants import VECTOR_TYPE
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
DEBUG = get_option('debug.connection')
|
|
18
|
+
|
|
19
|
+
NULL_COLUMN = 251
|
|
20
|
+
UNSIGNED_CHAR_COLUMN = 251
|
|
21
|
+
UNSIGNED_SHORT_COLUMN = 252
|
|
22
|
+
UNSIGNED_INT24_COLUMN = 253
|
|
23
|
+
UNSIGNED_INT64_COLUMN = 254
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def dump_packet(data): # pragma: no cover
|
|
27
|
+
|
|
28
|
+
def printable(data):
|
|
29
|
+
if 32 <= data < 127:
|
|
30
|
+
return chr(data)
|
|
31
|
+
return '.'
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
print('packet length:', len(data))
|
|
35
|
+
for i in range(1, 7):
|
|
36
|
+
f = sys._getframe(i)
|
|
37
|
+
print('call[%d]: %s (line %d)' % (i, f.f_code.co_name, f.f_lineno))
|
|
38
|
+
print('-' * 66)
|
|
39
|
+
except ValueError:
|
|
40
|
+
pass
|
|
41
|
+
dump_data = [data[i: i + 16] for i in range(0, min(len(data), 256), 16)]
|
|
42
|
+
for d in dump_data:
|
|
43
|
+
print(
|
|
44
|
+
' '.join('{:02X}'.format(x) for x in d)
|
|
45
|
+
+ ' ' * (16 - len(d))
|
|
46
|
+
+ ' ' * 2
|
|
47
|
+
+ ''.join(printable(x) for x in d),
|
|
48
|
+
)
|
|
49
|
+
print('-' * 66)
|
|
50
|
+
print()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class MysqlPacket:
|
|
54
|
+
"""
|
|
55
|
+
Representation of a MySQL response packet.
|
|
56
|
+
|
|
57
|
+
Provides an interface for reading/parsing the packet results.
|
|
58
|
+
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
__slots__ = ('_position', '_data')
|
|
62
|
+
|
|
63
|
+
def __init__(self, data, encoding):
|
|
64
|
+
self._position = 0
|
|
65
|
+
self._data = data
|
|
66
|
+
|
|
67
|
+
def get_all_data(self):
|
|
68
|
+
return self._data
|
|
69
|
+
|
|
70
|
+
def read(self, size):
|
|
71
|
+
"""Read the first 'size' bytes in packet and advance cursor past them."""
|
|
72
|
+
result = self._data[self._position: (self._position + size)]
|
|
73
|
+
if len(result) != size:
|
|
74
|
+
error = (
|
|
75
|
+
'Result length not requested length:\n'
|
|
76
|
+
'Expected=%s. Actual=%s. Position: %s. Data Length: %s'
|
|
77
|
+
% (size, len(result), self._position, len(self._data))
|
|
78
|
+
)
|
|
79
|
+
if DEBUG:
|
|
80
|
+
print(error)
|
|
81
|
+
self.dump()
|
|
82
|
+
raise AssertionError(error)
|
|
83
|
+
self._position += size
|
|
84
|
+
return result
|
|
85
|
+
|
|
86
|
+
def read_all(self):
|
|
87
|
+
"""
|
|
88
|
+
Read all remaining data in the packet.
|
|
89
|
+
|
|
90
|
+
(Subsequent read() will return errors.)
|
|
91
|
+
|
|
92
|
+
"""
|
|
93
|
+
result = self._data[self._position:]
|
|
94
|
+
self._position = None # ensure no subsequent read()
|
|
95
|
+
return result
|
|
96
|
+
|
|
97
|
+
def advance(self, length):
|
|
98
|
+
"""Advance the cursor in data buffer ``length`` bytes."""
|
|
99
|
+
new_position = self._position + length
|
|
100
|
+
if new_position < 0 or new_position > len(self._data):
|
|
101
|
+
raise Exception(
|
|
102
|
+
'Invalid advance amount (%s) for cursor. '
|
|
103
|
+
'Position=%s' % (length, new_position),
|
|
104
|
+
)
|
|
105
|
+
self._position = new_position
|
|
106
|
+
|
|
107
|
+
def rewind(self, position=0):
|
|
108
|
+
"""Set the position of the data buffer cursor to 'position'."""
|
|
109
|
+
if position < 0 or position > len(self._data):
|
|
110
|
+
raise Exception('Invalid position to rewind cursor to: %s.' % position)
|
|
111
|
+
self._position = position
|
|
112
|
+
|
|
113
|
+
def get_bytes(self, position, length=1):
|
|
114
|
+
"""
|
|
115
|
+
Get 'length' bytes starting at 'position'.
|
|
116
|
+
|
|
117
|
+
Position is start of payload (first four packet header bytes are not
|
|
118
|
+
included) starting at index '0'.
|
|
119
|
+
|
|
120
|
+
No error checking is done. If requesting outside end of buffer
|
|
121
|
+
an empty string (or string shorter than 'length') may be returned!
|
|
122
|
+
|
|
123
|
+
"""
|
|
124
|
+
return self._data[position: (position + length)]
|
|
125
|
+
|
|
126
|
+
def read_uint8(self):
|
|
127
|
+
result = self._data[self._position]
|
|
128
|
+
self._position += 1
|
|
129
|
+
return result
|
|
130
|
+
|
|
131
|
+
def read_uint16(self):
|
|
132
|
+
result = struct.unpack_from('<H', self._data, self._position)[0]
|
|
133
|
+
self._position += 2
|
|
134
|
+
return result
|
|
135
|
+
|
|
136
|
+
def read_uint24(self):
|
|
137
|
+
low, high = struct.unpack_from('<HB', self._data, self._position)
|
|
138
|
+
self._position += 3
|
|
139
|
+
return low + (high << 16)
|
|
140
|
+
|
|
141
|
+
def read_uint32(self):
|
|
142
|
+
result = struct.unpack_from('<I', self._data, self._position)[0]
|
|
143
|
+
self._position += 4
|
|
144
|
+
return result
|
|
145
|
+
|
|
146
|
+
def read_uint64(self):
|
|
147
|
+
result = struct.unpack_from('<Q', self._data, self._position)[0]
|
|
148
|
+
self._position += 8
|
|
149
|
+
return result
|
|
150
|
+
|
|
151
|
+
def read_string(self):
|
|
152
|
+
end_pos = self._data.find(b'\0', self._position)
|
|
153
|
+
if end_pos < 0:
|
|
154
|
+
return None
|
|
155
|
+
result = self._data[self._position: end_pos]
|
|
156
|
+
self._position = end_pos + 1
|
|
157
|
+
return result
|
|
158
|
+
|
|
159
|
+
def read_length_encoded_integer(self):
|
|
160
|
+
"""
|
|
161
|
+
Read a 'Length Coded Binary' number from the data buffer.
|
|
162
|
+
|
|
163
|
+
Length coded numbers can be anywhere from 1 to 9 bytes depending
|
|
164
|
+
on the value of the first byte.
|
|
165
|
+
|
|
166
|
+
"""
|
|
167
|
+
c = self.read_uint8()
|
|
168
|
+
if c == NULL_COLUMN:
|
|
169
|
+
return None
|
|
170
|
+
if c < UNSIGNED_CHAR_COLUMN:
|
|
171
|
+
return c
|
|
172
|
+
elif c == UNSIGNED_SHORT_COLUMN:
|
|
173
|
+
return self.read_uint16()
|
|
174
|
+
elif c == UNSIGNED_INT24_COLUMN:
|
|
175
|
+
return self.read_uint24()
|
|
176
|
+
elif c == UNSIGNED_INT64_COLUMN:
|
|
177
|
+
return self.read_uint64()
|
|
178
|
+
|
|
179
|
+
def read_length_coded_string(self):
|
|
180
|
+
"""
|
|
181
|
+
Read a 'Length Coded String' from the data buffer.
|
|
182
|
+
|
|
183
|
+
A 'Length Coded String' consists first of a length coded
|
|
184
|
+
(unsigned, positive) integer represented in 1-9 bytes followed by
|
|
185
|
+
that many bytes of binary data. (For example "cat" would be "3cat".)
|
|
186
|
+
|
|
187
|
+
"""
|
|
188
|
+
length = self.read_length_encoded_integer()
|
|
189
|
+
if length is None:
|
|
190
|
+
return None
|
|
191
|
+
return self.read(length)
|
|
192
|
+
|
|
193
|
+
def read_struct(self, fmt):
|
|
194
|
+
s = struct.Struct(fmt)
|
|
195
|
+
result = s.unpack_from(self._data, self._position)
|
|
196
|
+
self._position += s.size
|
|
197
|
+
return result
|
|
198
|
+
|
|
199
|
+
def is_ok_packet(self):
|
|
200
|
+
# https://dev.mysql.com/doc/internals/en/packet-OK_Packet.html
|
|
201
|
+
return self._data[0] == 0 and len(self._data) >= 7
|
|
202
|
+
|
|
203
|
+
def is_eof_packet(self):
|
|
204
|
+
# http://dev.mysql.com/doc/internals/en/generic-response-packets.html#packet-EOF_Packet
|
|
205
|
+
# Caution: \xFE may be LengthEncodedInteger.
|
|
206
|
+
# If \xFE is LengthEncodedInteger header, 8bytes followed.
|
|
207
|
+
return self._data[0] == 0xFE and len(self._data) < 9
|
|
208
|
+
|
|
209
|
+
def is_auth_switch_request(self):
|
|
210
|
+
# http://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::AuthSwitchRequest
|
|
211
|
+
return self._data[0] == 0xFE
|
|
212
|
+
|
|
213
|
+
def is_extra_auth_data(self):
|
|
214
|
+
# https://dev.mysql.com/doc/internals/en/successful-authentication.html
|
|
215
|
+
return self._data[0] == 1
|
|
216
|
+
|
|
217
|
+
def is_resultset_packet(self):
|
|
218
|
+
field_count = self._data[0]
|
|
219
|
+
return 1 <= field_count <= 250
|
|
220
|
+
|
|
221
|
+
def is_load_local_packet(self):
|
|
222
|
+
return self._data[0] == 0xFB
|
|
223
|
+
|
|
224
|
+
def is_error_packet(self):
|
|
225
|
+
return self._data[0] == 0xFF
|
|
226
|
+
|
|
227
|
+
def check_error(self):
|
|
228
|
+
if self.is_error_packet():
|
|
229
|
+
self.raise_for_error()
|
|
230
|
+
|
|
231
|
+
def raise_for_error(self):
|
|
232
|
+
self.rewind()
|
|
233
|
+
self.advance(1) # field_count == error (we already know that)
|
|
234
|
+
errno = self.read_uint16()
|
|
235
|
+
if DEBUG:
|
|
236
|
+
print('errno =', errno)
|
|
237
|
+
err.raise_mysql_exception(self._data)
|
|
238
|
+
|
|
239
|
+
def dump(self):
|
|
240
|
+
dump_packet(self._data)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
class FieldDescriptorPacket(MysqlPacket):
|
|
244
|
+
"""
|
|
245
|
+
A MysqlPacket that represents a specific column's metadata in the result.
|
|
246
|
+
|
|
247
|
+
Parsing is automatically done and the results are exported via public
|
|
248
|
+
attributes on the class such as: db, table_name, name, length, type_code.
|
|
249
|
+
|
|
250
|
+
"""
|
|
251
|
+
|
|
252
|
+
def __init__(self, data, encoding):
|
|
253
|
+
MysqlPacket.__init__(self, data, encoding)
|
|
254
|
+
self._parse_field_descriptor(encoding)
|
|
255
|
+
|
|
256
|
+
def _parse_field_descriptor(self, encoding):
|
|
257
|
+
"""
|
|
258
|
+
Parse the 'Field Descriptor' (Metadata) packet.
|
|
259
|
+
|
|
260
|
+
This is compatible with MySQL 4.1+ (not compatible with MySQL 4.0).
|
|
261
|
+
|
|
262
|
+
"""
|
|
263
|
+
self.catalog = self.read_length_coded_string()
|
|
264
|
+
self.db = self.read_length_coded_string()
|
|
265
|
+
self.table_name = self.read_length_coded_string().decode(encoding)
|
|
266
|
+
self.org_table = self.read_length_coded_string().decode(encoding)
|
|
267
|
+
self.name = self.read_length_coded_string().decode(encoding)
|
|
268
|
+
self.org_name = self.read_length_coded_string().decode(encoding)
|
|
269
|
+
n_bytes = 0
|
|
270
|
+
(
|
|
271
|
+
n_bytes,
|
|
272
|
+
self.charsetnr,
|
|
273
|
+
self.length,
|
|
274
|
+
self.type_code,
|
|
275
|
+
self.flags,
|
|
276
|
+
self.scale,
|
|
277
|
+
) = self.read_struct('<BHIBHBxx')
|
|
278
|
+
|
|
279
|
+
# 'default' is a length coded binary and is still in the buffer?
|
|
280
|
+
# not used for normal result sets...
|
|
281
|
+
|
|
282
|
+
# Extended types
|
|
283
|
+
if n_bytes > 12:
|
|
284
|
+
ext_type_code = self.read_uint8()
|
|
285
|
+
if ext_type_code == EXTENDED_TYPE.NONE:
|
|
286
|
+
pass
|
|
287
|
+
elif ext_type_code == EXTENDED_TYPE.BSON:
|
|
288
|
+
self.type_code = FIELD_TYPE.BSON
|
|
289
|
+
elif ext_type_code == EXTENDED_TYPE.VECTOR:
|
|
290
|
+
(self.length, vec_type) = self.read_struct('<IB')
|
|
291
|
+
if vec_type == VECTOR_TYPE.FLOAT32:
|
|
292
|
+
if self.charsetnr == 63:
|
|
293
|
+
self.type_code = FIELD_TYPE.FLOAT32_VECTOR
|
|
294
|
+
else:
|
|
295
|
+
self.type_code = FIELD_TYPE.FLOAT32_VECTOR_JSON
|
|
296
|
+
elif vec_type == VECTOR_TYPE.FLOAT64:
|
|
297
|
+
if self.charsetnr == 63:
|
|
298
|
+
self.type_code = FIELD_TYPE.FLOAT64_VECTOR
|
|
299
|
+
else:
|
|
300
|
+
self.type_code = FIELD_TYPE.FLOAT64_VECTOR_JSON
|
|
301
|
+
elif vec_type == VECTOR_TYPE.INT8:
|
|
302
|
+
if self.charsetnr == 63:
|
|
303
|
+
self.type_code = FIELD_TYPE.INT8_VECTOR
|
|
304
|
+
else:
|
|
305
|
+
self.type_code = FIELD_TYPE.INT8_VECTOR_JSON
|
|
306
|
+
elif vec_type == VECTOR_TYPE.INT16:
|
|
307
|
+
if self.charsetnr == 63:
|
|
308
|
+
self.type_code = FIELD_TYPE.INT16_VECTOR
|
|
309
|
+
else:
|
|
310
|
+
self.type_code = FIELD_TYPE.INT16_VECTOR_JSON
|
|
311
|
+
elif vec_type == VECTOR_TYPE.INT32:
|
|
312
|
+
if self.charsetnr == 63:
|
|
313
|
+
self.type_code = FIELD_TYPE.INT32_VECTOR
|
|
314
|
+
else:
|
|
315
|
+
self.type_code = FIELD_TYPE.INT32_VECTOR_JSON
|
|
316
|
+
elif vec_type == VECTOR_TYPE.INT64:
|
|
317
|
+
if self.charsetnr == 63:
|
|
318
|
+
self.type_code = FIELD_TYPE.INT64_VECTOR
|
|
319
|
+
else:
|
|
320
|
+
self.type_code = FIELD_TYPE.INT64_VECTOR_JSON
|
|
321
|
+
else:
|
|
322
|
+
raise TypeError(f'unrecognized vector data type: {vec_type}')
|
|
323
|
+
else:
|
|
324
|
+
raise TypeError(f'unrecognized extended data type: {ext_type_code}')
|
|
325
|
+
|
|
326
|
+
def description(self):
|
|
327
|
+
"""
|
|
328
|
+
Provides a 9-item tuple.
|
|
329
|
+
|
|
330
|
+
Standard descriptions only have 7 fields according to the Python
|
|
331
|
+
PEP249 DB Spec, but we need to surface information about unsigned
|
|
332
|
+
types and charsetnr for proper type handling.
|
|
333
|
+
|
|
334
|
+
"""
|
|
335
|
+
precision = self.get_column_length()
|
|
336
|
+
if self.type_code in (FIELD_TYPE.DECIMAL, FIELD_TYPE.NEWDECIMAL):
|
|
337
|
+
if precision:
|
|
338
|
+
precision -= 1 # for the sign
|
|
339
|
+
if self.scale > 0:
|
|
340
|
+
precision -= 1 # for the decimal point
|
|
341
|
+
return Description(
|
|
342
|
+
self.name,
|
|
343
|
+
self.type_code,
|
|
344
|
+
None, # TODO: display_length; should this be self.length?
|
|
345
|
+
self.get_column_length(), # 'internal_size'
|
|
346
|
+
precision, # 'precision'
|
|
347
|
+
self.scale,
|
|
348
|
+
self.flags % 2 == 0,
|
|
349
|
+
self.flags,
|
|
350
|
+
self.charsetnr,
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
def get_column_length(self):
|
|
354
|
+
if self.type_code == FIELD_TYPE.VAR_STRING:
|
|
355
|
+
mblen = MBLENGTH.get(self.charsetnr, 1)
|
|
356
|
+
return self.length // mblen
|
|
357
|
+
return self.length
|
|
358
|
+
|
|
359
|
+
def __str__(self):
|
|
360
|
+
return '%s %r.%r.%r, type=%s, flags=%x, charsetnr=%s' % (
|
|
361
|
+
self.__class__,
|
|
362
|
+
self.db,
|
|
363
|
+
self.table_name,
|
|
364
|
+
self.name,
|
|
365
|
+
self.type_code,
|
|
366
|
+
self.flags,
|
|
367
|
+
self.charsetnr,
|
|
368
|
+
)
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
class OKPacketWrapper:
|
|
372
|
+
"""
|
|
373
|
+
OK Packet Wrapper.
|
|
374
|
+
|
|
375
|
+
It uses an existing packet object, and wraps around it, exposing
|
|
376
|
+
useful variables while still providing access to the original packet
|
|
377
|
+
objects variables and methods.
|
|
378
|
+
|
|
379
|
+
"""
|
|
380
|
+
|
|
381
|
+
def __init__(self, from_packet):
|
|
382
|
+
if not from_packet.is_ok_packet():
|
|
383
|
+
raise ValueError(
|
|
384
|
+
'Cannot create '
|
|
385
|
+
+ str(self.__class__.__name__)
|
|
386
|
+
+ ' object from invalid packet type',
|
|
387
|
+
)
|
|
388
|
+
|
|
389
|
+
self.packet = from_packet
|
|
390
|
+
self.packet.advance(1)
|
|
391
|
+
|
|
392
|
+
self.affected_rows = self.packet.read_length_encoded_integer()
|
|
393
|
+
self.insert_id = self.packet.read_length_encoded_integer()
|
|
394
|
+
self.server_status, self.warning_count = self.read_struct('<HH')
|
|
395
|
+
self.message = self.packet.read_all()
|
|
396
|
+
self.has_next = self.server_status & SERVER_STATUS.SERVER_MORE_RESULTS_EXISTS
|
|
397
|
+
|
|
398
|
+
def __getattr__(self, key):
|
|
399
|
+
return getattr(self.packet, key)
|
|
400
|
+
|
|
401
|
+
|
|
402
|
+
class EOFPacketWrapper:
|
|
403
|
+
"""
|
|
404
|
+
EOF Packet Wrapper.
|
|
405
|
+
|
|
406
|
+
It uses an existing packet object, and wraps around it, exposing
|
|
407
|
+
useful variables while still providing access to the original packet
|
|
408
|
+
objects variables and methods.
|
|
409
|
+
|
|
410
|
+
"""
|
|
411
|
+
|
|
412
|
+
def __init__(self, from_packet):
|
|
413
|
+
if not from_packet.is_eof_packet():
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"Cannot create '{self.__class__}' object from invalid packet type",
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
self.packet = from_packet
|
|
419
|
+
self.warning_count, self.server_status = self.packet.read_struct('<xhh')
|
|
420
|
+
if DEBUG:
|
|
421
|
+
print('server_status=', self.server_status)
|
|
422
|
+
self.has_next = self.server_status & SERVER_STATUS.SERVER_MORE_RESULTS_EXISTS
|
|
423
|
+
|
|
424
|
+
def __getattr__(self, key):
|
|
425
|
+
return getattr(self.packet, key)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
class LoadLocalPacketWrapper:
|
|
429
|
+
"""
|
|
430
|
+
Load Local Packet Wrapper.
|
|
431
|
+
|
|
432
|
+
It uses an existing packet object, and wraps around it, exposing useful
|
|
433
|
+
variables while still providing access to the original packet
|
|
434
|
+
objects variables and methods.
|
|
435
|
+
|
|
436
|
+
"""
|
|
437
|
+
|
|
438
|
+
def __init__(self, from_packet):
|
|
439
|
+
if not from_packet.is_load_local_packet():
|
|
440
|
+
raise ValueError(
|
|
441
|
+
f"Cannot create '{self.__class__}' object from invalid packet type",
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
self.packet = from_packet
|
|
445
|
+
self.filename = self.packet.get_all_data()[1:]
|
|
446
|
+
if DEBUG:
|
|
447
|
+
print('filename=', self.filename)
|
|
448
|
+
|
|
449
|
+
def __getattr__(self, key):
|
|
450
|
+
return getattr(self.packet, key)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
# Sorted by alphabetical order
|
|
3
|
+
from singlestoredb.mysql.tests.test_basic import * # noqa: F403, F401
|
|
4
|
+
from singlestoredb.mysql.tests.test_connection import * # noqa: F403, F401
|
|
5
|
+
from singlestoredb.mysql.tests.test_converters import * # noqa: F403, F401
|
|
6
|
+
from singlestoredb.mysql.tests.test_cursor import * # noqa: F403, F401
|
|
7
|
+
from singlestoredb.mysql.tests.test_DictCursor import * # noqa: F403, F401
|
|
8
|
+
from singlestoredb.mysql.tests.test_err import * # noqa: F403, F401
|
|
9
|
+
from singlestoredb.mysql.tests.test_issues import * # noqa: F403, F401
|
|
10
|
+
from singlestoredb.mysql.tests.test_load_local import * # noqa: F403, F401
|
|
11
|
+
from singlestoredb.mysql.tests.test_nextset import * # noqa: F403, F401
|
|
12
|
+
from singlestoredb.mysql.tests.test_optionfile import * # noqa: F403, F401
|
|
13
|
+
from singlestoredb.mysql.tests.test_SSCursor import * # noqa: F403, F401
|
|
14
|
+
from singlestoredb.mysql.tests.thirdparty import * # noqa: F403, F401
|
|
15
|
+
|
|
16
|
+
if __name__ == '__main__':
|
|
17
|
+
import unittest
|
|
18
|
+
|
|
19
|
+
unittest.main()
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# type: ignore
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import platform
|
|
5
|
+
import re
|
|
6
|
+
import unittest
|
|
7
|
+
import warnings
|
|
8
|
+
|
|
9
|
+
import singlestoredb.mysql as sv
|
|
10
|
+
from singlestoredb.connection import build_params
|
|
11
|
+
|
|
12
|
+
DBNAME_BASE = 'singlestoredb__test_%s_%s_%s_%s_' % \
|
|
13
|
+
(
|
|
14
|
+
*platform.python_version_tuple()[:2],
|
|
15
|
+
platform.system(), platform.machine(),
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class PyMySQLTestCase(unittest.TestCase):
|
|
20
|
+
# You can specify your test environment creating a file named
|
|
21
|
+
# "databases.json" or editing the `databases` variable below.
|
|
22
|
+
fname = os.path.join(os.path.dirname(__file__), 'databases.json')
|
|
23
|
+
if os.path.exists(fname):
|
|
24
|
+
with open(fname) as f:
|
|
25
|
+
databases = json.load(f)
|
|
26
|
+
else:
|
|
27
|
+
params = build_params()
|
|
28
|
+
databases = [
|
|
29
|
+
{
|
|
30
|
+
'host': params['host'],
|
|
31
|
+
'port': params['port'],
|
|
32
|
+
'user': params['user'],
|
|
33
|
+
'passwd': params['password'],
|
|
34
|
+
'database': DBNAME_BASE + '1',
|
|
35
|
+
'use_unicode': True,
|
|
36
|
+
'local_infile': True,
|
|
37
|
+
'buffered': params['buffered'],
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
'host': params['host'], 'user': params['user'],
|
|
41
|
+
'port': params['port'], 'passwd': params['password'],
|
|
42
|
+
'database': DBNAME_BASE + '2',
|
|
43
|
+
'buffered': params['buffered'],
|
|
44
|
+
},
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
def mysql_server_is(self, conn, version_tuple):
|
|
48
|
+
"""
|
|
49
|
+
Return True if the given connection is on the version given or greater.
|
|
50
|
+
|
|
51
|
+
This only checks the server version string provided when the
|
|
52
|
+
connection is established, therefore any check for a version tuple
|
|
53
|
+
greater than (5, 5, 5) will always fail on MariaDB, as it always
|
|
54
|
+
starts with 5.5.5, e.g. 5.5.5-10.7.1-MariaDB-1:10.7.1+maria~focal.
|
|
55
|
+
|
|
56
|
+
Examples
|
|
57
|
+
--------
|
|
58
|
+
|
|
59
|
+
if self.mysql_server_is(conn, (5, 6, 4)):
|
|
60
|
+
# do something for MySQL 5.6.4 and above
|
|
61
|
+
|
|
62
|
+
"""
|
|
63
|
+
server_version = conn.get_server_info()
|
|
64
|
+
server_version_tuple = tuple(
|
|
65
|
+
(int(dig) if dig is not None else 0)
|
|
66
|
+
for dig in re.match(r'(\d+)\.(\d+)\.(\d+)', server_version).group(1, 2, 3)
|
|
67
|
+
)
|
|
68
|
+
return server_version_tuple >= version_tuple
|
|
69
|
+
|
|
70
|
+
_connections = None
|
|
71
|
+
|
|
72
|
+
@property
|
|
73
|
+
def connections(self):
|
|
74
|
+
if self._connections is None:
|
|
75
|
+
self._connections = []
|
|
76
|
+
for params in self.databases:
|
|
77
|
+
conn = sv.connect(**params)
|
|
78
|
+
self._connections.append(conn)
|
|
79
|
+
self.addCleanup(self._teardown_connections)
|
|
80
|
+
return self._connections
|
|
81
|
+
|
|
82
|
+
def connect(self, **params):
|
|
83
|
+
p = self.databases[0].copy()
|
|
84
|
+
p.update(params)
|
|
85
|
+
conn = sv.connect(**p)
|
|
86
|
+
|
|
87
|
+
@self.addCleanup
|
|
88
|
+
def teardown():
|
|
89
|
+
if conn.open:
|
|
90
|
+
conn.close()
|
|
91
|
+
|
|
92
|
+
return conn
|
|
93
|
+
|
|
94
|
+
def _teardown_connections(self):
|
|
95
|
+
if self._connections:
|
|
96
|
+
for connection in self._connections:
|
|
97
|
+
if connection.open:
|
|
98
|
+
connection.close()
|
|
99
|
+
self._connections = None
|
|
100
|
+
|
|
101
|
+
def safe_create_table(self, connection, tablename, ddl, cleanup=True):
|
|
102
|
+
"""
|
|
103
|
+
Create a table.
|
|
104
|
+
|
|
105
|
+
Ensures any existing version of that table is first dropped.
|
|
106
|
+
|
|
107
|
+
Also adds a cleanup rule to drop the table after the test
|
|
108
|
+
completes.
|
|
109
|
+
|
|
110
|
+
"""
|
|
111
|
+
cursor = connection.cursor()
|
|
112
|
+
|
|
113
|
+
with warnings.catch_warnings():
|
|
114
|
+
warnings.simplefilter('ignore')
|
|
115
|
+
cursor.execute('drop table if exists `%s`' % (tablename,))
|
|
116
|
+
cursor.execute(ddl)
|
|
117
|
+
cursor.close()
|
|
118
|
+
if cleanup:
|
|
119
|
+
self.addCleanup(self.drop_table, connection, tablename)
|
|
120
|
+
|
|
121
|
+
def drop_table(self, connection, tablename):
|
|
122
|
+
cursor = connection.cursor()
|
|
123
|
+
with warnings.catch_warnings():
|
|
124
|
+
warnings.simplefilter('ignore')
|
|
125
|
+
cursor.execute('drop table if exists `%s`' % (tablename,))
|
|
126
|
+
cursor.close()
|