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.

Files changed (120) hide show
  1. singlestoredb/__init__.py +33 -1
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +5 -1
  4. singlestoredb/config.py +116 -14
  5. singlestoredb/connection.py +483 -516
  6. singlestoredb/converters.py +238 -135
  7. singlestoredb/exceptions.py +30 -2
  8. singlestoredb/functions/__init__.py +1 -0
  9. singlestoredb/functions/decorator.py +142 -0
  10. singlestoredb/functions/dtypes.py +1639 -0
  11. singlestoredb/functions/ext/__init__.py +2 -0
  12. singlestoredb/functions/ext/arrow.py +375 -0
  13. singlestoredb/functions/ext/asgi.py +661 -0
  14. singlestoredb/functions/ext/json.py +427 -0
  15. singlestoredb/functions/ext/mmap.py +306 -0
  16. singlestoredb/functions/ext/rowdat_1.py +744 -0
  17. singlestoredb/functions/signature.py +673 -0
  18. singlestoredb/fusion/__init__.py +11 -0
  19. singlestoredb/fusion/graphql.py +213 -0
  20. singlestoredb/fusion/handler.py +621 -0
  21. singlestoredb/fusion/handlers/stage.py +257 -0
  22. singlestoredb/fusion/handlers/utils.py +162 -0
  23. singlestoredb/fusion/handlers/workspace.py +412 -0
  24. singlestoredb/fusion/registry.py +164 -0
  25. singlestoredb/fusion/result.py +399 -0
  26. singlestoredb/http/__init__.py +27 -0
  27. singlestoredb/{http.py → http/connection.py} +555 -154
  28. singlestoredb/management/__init__.py +3 -0
  29. singlestoredb/management/billing_usage.py +148 -0
  30. singlestoredb/management/cluster.py +14 -6
  31. singlestoredb/management/manager.py +100 -38
  32. singlestoredb/management/organization.py +188 -0
  33. singlestoredb/management/region.py +5 -5
  34. singlestoredb/management/utils.py +281 -2
  35. singlestoredb/management/workspace.py +1344 -49
  36. singlestoredb/{clients/pymysqlsv → mysql}/__init__.py +16 -21
  37. singlestoredb/{clients/pymysqlsv → mysql}/_auth.py +39 -8
  38. singlestoredb/{clients/pymysqlsv → mysql}/charset.py +26 -23
  39. singlestoredb/{clients/pymysqlsv/connections.py → mysql/connection.py} +532 -165
  40. singlestoredb/{clients/pymysqlsv → mysql}/constants/CLIENT.py +0 -1
  41. singlestoredb/{clients/pymysqlsv → mysql}/constants/COMMAND.py +0 -1
  42. singlestoredb/{clients/pymysqlsv → mysql}/constants/CR.py +0 -2
  43. singlestoredb/{clients/pymysqlsv → mysql}/constants/ER.py +0 -1
  44. singlestoredb/{clients/pymysqlsv → mysql}/constants/FIELD_TYPE.py +1 -1
  45. singlestoredb/{clients/pymysqlsv → mysql}/constants/FLAG.py +0 -1
  46. singlestoredb/{clients/pymysqlsv → mysql}/constants/SERVER_STATUS.py +0 -1
  47. singlestoredb/mysql/converters.py +271 -0
  48. singlestoredb/{clients/pymysqlsv → mysql}/cursors.py +228 -112
  49. singlestoredb/mysql/err.py +92 -0
  50. singlestoredb/{clients/pymysqlsv → mysql}/optionfile.py +5 -4
  51. singlestoredb/{clients/pymysqlsv → mysql}/protocol.py +49 -20
  52. singlestoredb/mysql/tests/__init__.py +19 -0
  53. singlestoredb/{clients/pymysqlsv → mysql}/tests/base.py +32 -12
  54. singlestoredb/mysql/tests/conftest.py +37 -0
  55. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_DictCursor.py +11 -7
  56. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_SSCursor.py +17 -12
  57. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_basic.py +32 -24
  58. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_connection.py +130 -119
  59. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_converters.py +9 -7
  60. singlestoredb/mysql/tests/test_cursor.py +141 -0
  61. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_err.py +3 -2
  62. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_issues.py +35 -27
  63. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_load_local.py +13 -11
  64. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_nextset.py +7 -3
  65. singlestoredb/{clients/pymysqlsv → mysql}/tests/test_optionfile.py +2 -1
  66. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/__init__.py +1 -1
  67. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  68. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/capabilities.py +19 -17
  69. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/dbapi20.py +31 -22
  70. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +3 -4
  71. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +24 -20
  72. singlestoredb/{clients/pymysqlsv → mysql}/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +4 -4
  73. singlestoredb/{clients/pymysqlsv → mysql}/times.py +3 -4
  74. singlestoredb/pytest.py +283 -0
  75. singlestoredb/tests/empty.sql +0 -0
  76. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  77. singlestoredb/tests/test.sql +210 -0
  78. singlestoredb/tests/test2.sql +1 -0
  79. singlestoredb/tests/test_basics.py +482 -115
  80. singlestoredb/tests/test_config.py +13 -13
  81. singlestoredb/tests/test_connection.py +241 -305
  82. singlestoredb/tests/test_dbapi.py +27 -0
  83. singlestoredb/tests/test_ext_func.py +1193 -0
  84. singlestoredb/tests/test_ext_func_data.py +1101 -0
  85. singlestoredb/tests/test_fusion.py +465 -0
  86. singlestoredb/tests/test_http.py +32 -26
  87. singlestoredb/tests/test_management.py +588 -8
  88. singlestoredb/tests/test_plugin.py +33 -0
  89. singlestoredb/tests/test_results.py +11 -12
  90. singlestoredb/tests/test_udf.py +687 -0
  91. singlestoredb/tests/utils.py +3 -2
  92. singlestoredb/utils/config.py +58 -0
  93. singlestoredb/utils/debug.py +13 -0
  94. singlestoredb/utils/mogrify.py +151 -0
  95. singlestoredb/utils/results.py +4 -1
  96. singlestoredb-1.0.4.dist-info/METADATA +139 -0
  97. singlestoredb-1.0.4.dist-info/RECORD +112 -0
  98. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/WHEEL +1 -1
  99. singlestoredb-1.0.4.dist-info/entry_points.txt +2 -0
  100. singlestoredb/clients/pymysqlsv/converters.py +0 -365
  101. singlestoredb/clients/pymysqlsv/err.py +0 -144
  102. singlestoredb/clients/pymysqlsv/tests/__init__.py +0 -19
  103. singlestoredb/clients/pymysqlsv/tests/test_cursor.py +0 -133
  104. singlestoredb/clients/pymysqlsv/tests/thirdparty/test_MySQLdb/__init__.py +0 -9
  105. singlestoredb/drivers/__init__.py +0 -45
  106. singlestoredb/drivers/base.py +0 -198
  107. singlestoredb/drivers/cymysql.py +0 -38
  108. singlestoredb/drivers/http.py +0 -47
  109. singlestoredb/drivers/mariadb.py +0 -40
  110. singlestoredb/drivers/mysqlconnector.py +0 -49
  111. singlestoredb/drivers/mysqldb.py +0 -60
  112. singlestoredb/drivers/pymysql.py +0 -37
  113. singlestoredb/drivers/pymysqlsv.py +0 -35
  114. singlestoredb/drivers/pyodbc.py +0 -65
  115. singlestoredb-0.4.0.dist-info/METADATA +0 -111
  116. singlestoredb-0.4.0.dist-info/RECORD +0 -86
  117. /singlestoredb/{clients → fusion/handlers}/__init__.py +0 -0
  118. /singlestoredb/{clients/pymysqlsv → mysql}/constants/__init__.py +0 -0
  119. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/LICENSE +0 -0
  120. {singlestoredb-0.4.0.dist-info → singlestoredb-1.0.4.dist-info}/top_level.txt +0 -0
@@ -13,11 +13,9 @@ import traceback
13
13
  import warnings
14
14
 
15
15
  try:
16
- import _pymysqlsv
16
+ import _singlestoredb_accel
17
17
  except (ImportError, ModuleNotFoundError):
18
- _pymysqlsv = None
19
- # warnings.warn('Accelerator extension could not be loaded; '
20
- # 'running in pure Python mode.', RuntimeWarning)
18
+ _singlestoredb_accel = None
21
19
 
22
20
  from . import _auth
23
21
 
@@ -26,11 +24,17 @@ from .constants import CLIENT, COMMAND, CR, ER, FIELD_TYPE, SERVER_STATUS
26
24
  from . import converters
27
25
  from .cursors import (
28
26
  Cursor,
29
- SSCursor,
27
+ CursorSV,
30
28
  DictCursor,
29
+ DictCursorSV,
30
+ NamedtupleCursor,
31
+ NamedtupleCursorSV,
32
+ SSCursor,
33
+ SSCursorSV,
31
34
  SSDictCursor,
32
- # SSCursorSV,
33
- # SSDictCursorSV,
35
+ SSDictCursorSV,
36
+ SSNamedtupleCursor,
37
+ SSNamedtupleCursorSV,
34
38
  )
35
39
  from .optionfile import Parser
36
40
  from .protocol import (
@@ -41,7 +45,12 @@ from .protocol import (
41
45
  EOFPacketWrapper,
42
46
  LoadLocalPacketWrapper,
43
47
  )
44
- from . import err, VERSION_STRING
48
+ from . import err
49
+ from ..config import get_option
50
+ from .. import fusion
51
+ from .. import connection
52
+ from ..connection import Connection as BaseConnection
53
+ from ..utils.debug import log_query
45
54
 
46
55
  try:
47
56
  import ssl
@@ -60,7 +69,7 @@ except (ImportError, KeyError):
60
69
  # KeyError occurs when there's no entry in OS database for a current user.
61
70
  DEFAULT_USER = None
62
71
 
63
- DEBUG = False
72
+ DEBUG = get_option('debug.connection')
64
73
 
65
74
  TEXT_TYPES = {
66
75
  FIELD_TYPE.BIT,
@@ -106,87 +115,149 @@ def _lenenc_int(i):
106
115
  )
107
116
 
108
117
 
109
- class Connection:
118
+ class Connection(BaseConnection):
110
119
  """
111
120
  Representation of a socket with a mysql server.
112
121
 
113
122
  The proper way to get an instance of this class is to call
114
- connect().
115
-
116
- Establish a connection to the MySQL database. Accepts several
117
- arguments:
118
-
119
- :param host: Host where the database server is located.
120
- :param user: Username to log in as.
121
- :param password: Password to use.
122
- :param database: Database to use, None to not use a particular one.
123
- :param port: MySQL port to use, default is usually OK. (default: 3306)
124
- :param bind_address: When the client has multiple network interfaces, specify
123
+ ``connect()``.
124
+
125
+ Establish a connection to the SingleStoreDB database.
126
+
127
+ Parameters
128
+ ----------
129
+ host : str, optional
130
+ Host where the database server is located.
131
+ user : str, optional
132
+ Username to log in as.
133
+ password : str, optional
134
+ Password to use.
135
+ database : str, optional
136
+ Database to use, None to not use a particular one.
137
+ port : int, optional
138
+ Server port to use, default is usually OK. (default: 3306)
139
+ bind_address : str, optional
140
+ When the client has multiple network interfaces, specify
125
141
  the interface from which to connect to the host. Argument can be
126
142
  a hostname or an IP address.
127
- :param unix_socket: Use a unix socket rather than TCP/IP.
128
- :param read_timeout: The timeout for reading from the connection in seconds (default: None - no timeout)
129
- :param write_timeout: The timeout for writing to the connection in seconds (default: None - no timeout)
130
- :param charset: Charset to use.
131
- :param sql_mode: Default SQL_MODE to use.
132
- :param read_default_file:
133
- Specifies my.cnf file to read these parameters from under the [client] section.
134
- :param conv:
143
+ unix_socket : str, optional
144
+ Use a unix socket rather than TCP/IP.
145
+ read_timeout : int, optional
146
+ The timeout for reading from the connection in seconds
147
+ (default: None - no timeout)
148
+ write_timeout : int, optional
149
+ The timeout for writing to the connection in seconds
150
+ (default: None - no timeout)
151
+ charset : str, optional
152
+ Charset to use.
153
+ collation : str, optional
154
+ The charset collation
155
+ sql_mode : str, optional
156
+ Default SQL_MODE to use.
157
+ read_default_file : str, optional
158
+ Specifies my.cnf file to read these parameters from under the
159
+ [client] section.
160
+ conv : Dict[str, Callable[Any]], optional
135
161
  Conversion dictionary to use instead of the default one.
136
162
  This is used to provide custom marshalling and unmarshalling of types.
137
163
  See converters.
138
- :param use_unicode:
164
+ use_unicode : bool, optional
139
165
  Whether or not to default to unicode strings.
140
166
  This option defaults to true.
141
- :param client_flag: Custom flags to send to MySQL. Find potential values in constants.CLIENT.
142
- :param cursorclass: Custom cursor class to use.
143
- :param init_command: Initial SQL statement to run when connection is established.
144
- :param connect_timeout: The timeout for connecting to the database in seconds.
167
+ client_flag : int, optional
168
+ Custom flags to send to MySQL. Find potential values in constants.CLIENT.
169
+ cursorclass : type, optional
170
+ Custom cursor class to use.
171
+ init_command : str, optional
172
+ Initial SQL statement to run when connection is established.
173
+ connect_timeout : int, optional
174
+ The timeout for connecting to the database in seconds.
145
175
  (default: 10, min: 1, max: 31536000)
146
- :param ssl: A dict of arguments similar to mysql_ssl_set()'s parameters or an ssl.SSLContext.
147
- :param ssl_ca: Path to the file that contains a PEM-formatted CA certificate.
148
- :param ssl_cert: Path to the file that contains a PEM-formatted client certificate.
149
- :param ssl_disabled: A boolean value that disables usage of TLS.
150
- :param ssl_key: Path to the file that contains a PEM-formatted private key for the client certificate.
151
- :param ssl_verify_cert: Set to true to check the server certificate's validity.
152
- :param ssl_verify_identity: Set to true to check the server's identity.
153
- :param read_default_group: Group to read from in the configuration file.
154
- :param autocommit: Autocommit mode. None means use server default. (default: False)
155
- :param local_infile: Boolean to enable the use of LOAD DATA LOCAL command. (default: False)
156
- :param max_allowed_packet: Max size of packet sent to server in bytes. (default: 16MB)
157
- Only used to limit size of "LOAD LOCAL INFILE" data packet smaller than default (16KB).
158
- :param defer_connect: Don't explicitly connect on construction - wait for connect call.
176
+ ssl : Dict[str, str], optional
177
+ A dict of arguments similar to mysql_ssl_set()'s parameters or
178
+ an ssl.SSLContext.
179
+ ssl_ca : str, optional
180
+ Path to the file that contains a PEM-formatted CA certificate.
181
+ ssl_cert : str, optional
182
+ Path to the file that contains a PEM-formatted client certificate.
183
+ ssl_cipher : str, optional
184
+ SSL ciphers to allow.
185
+ ssl_disabled : bool, optional
186
+ A boolean value that disables usage of TLS.
187
+ ssl_key : str, optional
188
+ Path to the file that contains a PEM-formatted private key for the
189
+ client certificate.
190
+ ssl_verify_cert : str, optional
191
+ Set to true to check the server certificate's validity.
192
+ ssl_verify_identity : bool, optional
193
+ Set to true to check the server's identity.
194
+ read_default_group : str, optional
195
+ Group to read from in the configuration file.
196
+ autocommit : bool, optional
197
+ Autocommit mode. None means use server default. (default: False)
198
+ local_infile : bool, optional
199
+ Boolean to enable the use of LOAD DATA LOCAL command. (default: False)
200
+ max_allowed_packet : int, optional
201
+ Max size of packet sent to server in bytes. (default: 16MB)
202
+ Only used to limit size of "LOAD LOCAL INFILE" data packet smaller
203
+ than default (16KB).
204
+ defer_connect : bool, optional
205
+ Don't explicitly connect on construction - wait for connect call.
159
206
  (default: False)
160
- :param auth_plugin_map: A dict of plugin names to a class that processes that plugin.
161
- The class will take the Connection object as the argument to the constructor.
162
- The class needs an authenticate method taking an authentication packet as
163
- an argument. For the dialog plugin, a prompt(echo, prompt) method can be used
164
- (if no authenticate method) for returning a string from the user. (experimental)
165
- :param server_public_key: SHA256 authentication plugin public key value. (default: None)
166
- :param binary_prefix: Add _binary prefix on bytes and bytearray. (default: False)
167
- :param compress: Not supported.
168
- :param named_pipe: Not supported.
169
- :param db: **DEPRECATED** Alias for database.
170
- :param passwd: **DEPRECATED** Alias for password.
171
- :param parse_json: Parse JSON values into Python objects?
172
- :param invalid_values: Dictionary of values to use in place of invalid values
207
+ auth_plugin_map : Dict[str, type], optional
208
+ A dict of plugin names to a class that processes that plugin.
209
+ The class will take the Connection object as the argument to the
210
+ constructor. The class needs an authenticate method taking an
211
+ authentication packet as an argument. For the dialog plugin, a
212
+ prompt(echo, prompt) method can be used (if no authenticate method)
213
+ for returning a string from the user. (experimental)
214
+ server_public_key : str, optional
215
+ SHA256 authentication plugin public key value. (default: None)
216
+ binary_prefix : bool, optional
217
+ Add _binary prefix on bytes and bytearray. (default: False)
218
+ compress :
219
+ Not supported.
220
+ named_pipe :
221
+ Not supported.
222
+ db : str, optional
223
+ **DEPRECATED** Alias for database.
224
+ passwd : str, optional
225
+ **DEPRECATED** Alias for password.
226
+ parse_json : bool, optional
227
+ Parse JSON values into Python objects?
228
+ invalid_values : Dict[int, Any], optional
229
+ Dictionary of values to use in place of invalid values
173
230
  found during conversion of data. The default is to return the byte content
174
231
  containing the invalid value. The keys are the integers associtated with
175
232
  the column type.
176
- :param pure_python: Should we ignore the C extension even if it's available?
233
+ pure_python : bool, optional
234
+ Should we ignore the C extension even if it's available?
177
235
  This can be given explicitly using True or False, or if the value is None,
178
- the C extension will be loaded if it is available.
236
+ the C extension will be loaded if it is available. If set to False and
237
+ the C extension can't be loaded, a NotSupportedError is raised.
238
+ nan_as_null : bool, optional
239
+ Should NaN values be treated as NULLs in parameter substitution including
240
+ uploading data?
241
+ inf_as_null : bool, optional
242
+ Should Inf values be treated as NULLs in parameter substitution including
243
+ uploading data?
244
+ track_env : bool, optional
245
+ Should the connection track the SINGLESTOREDB_URL environment variable?
246
+
247
+ See `Connection <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_
248
+ in the specification.
179
249
 
180
- See `Connection <https://www.python.org/dev/peps/pep-0249/#connection-objects>`_ in the
181
- specification.
182
250
  """
183
251
 
252
+ driver = 'mysql'
253
+ paramstyle = 'pyformat'
254
+
184
255
  _sock = None
185
256
  _auth_plugin_name = ''
186
257
  _closed = False
187
258
  _secure = False
188
259
 
189
- def __init__(
260
+ def __init__( # noqa: C901
190
261
  self,
191
262
  *,
192
263
  user=None, # The first four arguments is based on DB-API 2.0 recommendation.
@@ -196,12 +267,13 @@ class Connection:
196
267
  unix_socket=None,
197
268
  port=0,
198
269
  charset='',
270
+ collation=None,
199
271
  sql_mode=None,
200
272
  read_default_file=None,
201
273
  conv=None,
202
274
  use_unicode=True,
203
275
  client_flag=0,
204
- cursorclass=Cursor,
276
+ cursorclass=None,
205
277
  init_command=None,
206
278
  connect_timeout=10,
207
279
  read_default_group=None,
@@ -219,18 +291,30 @@ class Connection:
219
291
  ssl=None,
220
292
  ssl_ca=None,
221
293
  ssl_cert=None,
294
+ ssl_cipher=None,
222
295
  ssl_disabled=None,
223
296
  ssl_key=None,
224
297
  ssl_verify_cert=None,
225
298
  ssl_verify_identity=None,
226
- parse_json=False,
299
+ parse_json=True,
227
300
  invalid_values=None,
228
301
  pure_python=None,
302
+ buffered=True,
303
+ results_type='tuples',
229
304
  compress=None, # not supported
230
305
  named_pipe=None, # not supported
231
306
  passwd=None, # deprecated
232
307
  db=None, # deprecated
308
+ driver=None, # internal use
309
+ conn_attrs=None,
310
+ multi_statements=None,
311
+ nan_as_null=None,
312
+ inf_as_null=None,
313
+ encoding_errors='strict',
314
+ track_env=False,
233
315
  ):
316
+ BaseConnection.__init__(**dict(locals()))
317
+
234
318
  if db is not None and database is None:
235
319
  # We will raise warning in 2022 or later.
236
320
  # See https://github.com/PyMySQL/PyMySQL/issues/939
@@ -252,6 +336,8 @@ class Connection:
252
336
  self._local_infile = bool(local_infile)
253
337
  if self._local_infile:
254
338
  client_flag |= CLIENT.LOCAL_FILES
339
+ if multi_statements:
340
+ client_flag |= CLIENT.MULTI_STATEMENTS
255
341
 
256
342
  if read_default_group and not read_default_file:
257
343
  if sys.platform.startswith('win'):
@@ -292,7 +378,8 @@ class Connection:
292
378
 
293
379
  self.ssl = False
294
380
  if not ssl_disabled:
295
- if ssl_ca or ssl_cert or ssl_key or ssl_verify_cert or ssl_verify_identity:
381
+ if ssl_ca or ssl_cert or ssl_key or ssl_cipher or \
382
+ ssl_verify_cert or ssl_verify_identity:
296
383
  ssl = {
297
384
  'ca': ssl_ca,
298
385
  'check_hostname': bool(ssl_verify_identity),
@@ -304,6 +391,8 @@ class Connection:
304
391
  ssl['cert'] = ssl_cert
305
392
  if ssl_key is not None:
306
393
  ssl['key'] = ssl_key
394
+ if ssl_cipher is not None:
395
+ ssl['cipher'] = ssl_cipher
307
396
  if ssl:
308
397
  if not SSL_ENABLED:
309
398
  raise NotImplementedError('ssl module not found')
@@ -333,31 +422,71 @@ class Connection:
333
422
  self._write_timeout = write_timeout
334
423
 
335
424
  self.charset = charset or DEFAULT_CHARSET
425
+ self.collation = collation
336
426
  self.use_unicode = use_unicode
427
+ self.encoding_errors = encoding_errors
337
428
 
338
429
  self.encoding = charset_by_name(self.charset).encoding
339
430
 
340
431
  client_flag |= CLIENT.CAPABILITIES
341
- if self.db:
342
- client_flag |= CLIENT.CONNECT_WITH_DB
432
+ client_flag |= CLIENT.CONNECT_WITH_DB
343
433
 
344
434
  self.client_flag = client_flag
345
435
 
346
436
  self.pure_python = pure_python
347
- self.output_type = 'tuples'
348
- self.cursorclass = cursorclass
437
+ self.results_type = results_type
349
438
  self.resultclass = MySQLResult
439
+ if cursorclass is not None:
440
+ self.cursorclass = cursorclass
441
+ elif buffered:
442
+ if 'dict' in self.results_type:
443
+ self.cursorclass = DictCursor
444
+ elif 'namedtuple' in self.results_type:
445
+ self.cursorclass = NamedtupleCursor
446
+ else:
447
+ self.cursorclass = Cursor
448
+ else:
449
+ if 'dict' in self.results_type:
450
+ self.cursorclass = SSDictCursor
451
+ elif 'namedtuple' in self.results_type:
452
+ self.cursorclass = SSNamedtupleCursor
453
+ else:
454
+ self.cursorclass = SSCursor
455
+
456
+ if self.pure_python is False and _singlestoredb_accel is None:
457
+ try:
458
+ import _singlestortedb_accel # noqa: F401
459
+ except Exception:
460
+ import traceback
461
+ traceback.print_exc(file=sys.stderr)
462
+ finally:
463
+ raise err.NotSupportedError(
464
+ 'pure_python=False, but the '
465
+ 'C extension can not be loaded',
466
+ )
467
+
468
+ if self.pure_python is True:
469
+ pass
350
470
 
351
471
  # The C extension handles these types internally.
352
- # if _pymysqlsv is not None and not self.pure_python:
353
- # self.resultclass = MySQLResultSV
354
- # if self.cursorclass is SSCursor:
355
- # self.cursorclass = SSCursorSV
356
- # elif self.cursorclass is DictCursor:
357
- # self.output_type = 'dicts'
358
- # elif self.cursorclass is SSDictCursor:
359
- # self.cursorclass = SSDictCursorSV
360
- # self.output_type = 'dicts'
472
+ elif _singlestoredb_accel is not None:
473
+ self.resultclass = MySQLResultSV
474
+ if self.cursorclass is Cursor:
475
+ self.cursorclass = CursorSV
476
+ elif self.cursorclass is SSCursor:
477
+ self.cursorclass = SSCursorSV
478
+ elif self.cursorclass is DictCursor:
479
+ self.cursorclass = DictCursorSV
480
+ self.results_type = 'dicts'
481
+ elif self.cursorclass is SSDictCursor:
482
+ self.cursorclass = SSDictCursorSV
483
+ self.results_type = 'dicts'
484
+ elif self.cursorclass is NamedtupleCursor:
485
+ self.cursorclass = NamedtupleCursorSV
486
+ self.results_type = 'namedtuples'
487
+ elif self.cursorclass is SSNamedtupleCursor:
488
+ self.cursorclass = SSNamedtupleCursorSV
489
+ self.results_type = 'namedtuples'
361
490
 
362
491
  self._result = None
363
492
  self._affected_rows = 0
@@ -382,20 +511,46 @@ class Connection:
382
511
  self._binary_prefix = binary_prefix
383
512
  self.server_public_key = server_public_key
384
513
 
514
+ if self.connection_params['nan_as_null'] or \
515
+ self.connection_params['inf_as_null']:
516
+ float_encoder = self.encoders.get(float)
517
+ if float_encoder is not None:
518
+ self.encoders[float] = functools.partial(
519
+ float_encoder,
520
+ nan_as_null=self.connection_params['nan_as_null'],
521
+ inf_as_null=self.connection_params['inf_as_null'],
522
+ )
523
+
524
+ from .. import __version__ as VERSION_STRING
525
+
385
526
  self._connect_attrs = {
386
- '_client_name': 'pymysqlsv',
527
+ '_os': str(sys.platform),
387
528
  '_pid': str(os.getpid()),
529
+ '_client_name': 'SingleStoreDB Python Client',
388
530
  '_client_version': VERSION_STRING,
389
531
  }
390
532
 
391
533
  if program_name:
392
534
  self._connect_attrs['program_name'] = program_name
535
+ if conn_attrs is not None:
536
+ # do not overwrite the attributes that we set ourselves
537
+ for k, v in conn_attrs.items():
538
+ if k not in self._connect_attrs:
539
+ self._connect_attrs[k] = v
540
+
541
+ self._in_sync = False
542
+ self._track_env = bool(track_env) or self.host == 'singlestore.com'
393
543
 
394
- if defer_connect:
544
+ if defer_connect or self._track_env:
395
545
  self._sock = None
396
546
  else:
397
547
  self.connect()
398
548
 
549
+ @property
550
+ def messages(self):
551
+ # TODO
552
+ []
553
+
399
554
  def __enter__(self):
400
555
  return self
401
556
 
@@ -442,11 +597,17 @@ class Connection:
442
597
  """
443
598
  Send the quit message and close the socket.
444
599
 
445
- See `Connection.close() <https://www.python.org/dev/peps/pep-0249/#Connection.close>`_
600
+ See `Connection.close()
601
+ <https://www.python.org/dev/peps/pep-0249/#Connection.close>`_
446
602
  in the specification.
447
603
 
448
- :raise Error: If the connection is already closed.
604
+ Raises
605
+ ------
606
+ Error : If the connection is already closed.
607
+
449
608
  """
609
+ if self.host == 'singlestore.com':
610
+ return
450
611
  if self._closed:
451
612
  raise err.Error('Already closed')
452
613
  self._closed = True
@@ -465,6 +626,10 @@ class Connection:
465
626
  """Return True if the connection is open."""
466
627
  return self._sock is not None
467
628
 
629
+ def is_connected(self):
630
+ """Return True if the connection is open."""
631
+ return self.open
632
+
468
633
  def _force_close(self):
469
634
  """Close connection without QUIT message."""
470
635
  if self._sock:
@@ -478,12 +643,14 @@ class Connection:
478
643
  __del__ = _force_close
479
644
 
480
645
  def autocommit(self, value):
646
+ """Enable autocommit in the server."""
481
647
  self.autocommit_mode = bool(value)
482
648
  current = self.get_autocommit()
483
649
  if value != current:
484
650
  self._send_autocommit_mode()
485
651
 
486
652
  def get_autocommit(self):
653
+ """Retrieve autocommit status."""
487
654
  return bool(self.server_status & SERVER_STATUS.SERVER_STATUS_AUTOCOMMIT)
488
655
 
489
656
  def _read_ok_packet(self):
@@ -499,6 +666,7 @@ class Connection:
499
666
 
500
667
  def _send_autocommit_mode(self):
501
668
  """Set whether or not to commit after every execute()."""
669
+ log_query('SET AUTOCOMMIT = %s' % self.escape(self.autocommit_mode))
502
670
  self._execute_command(
503
671
  COMMAND.COM_QUERY, 'SET AUTOCOMMIT = %s' % self.escape(self.autocommit_mode),
504
672
  )
@@ -506,6 +674,9 @@ class Connection:
506
674
 
507
675
  def begin(self):
508
676
  """Begin transaction."""
677
+ log_query('BEGIN')
678
+ if self.host == 'singlestore.com':
679
+ return
509
680
  self._execute_command(COMMAND.COM_QUERY, 'BEGIN')
510
681
  self._read_ok_packet()
511
682
 
@@ -515,7 +686,11 @@ class Connection:
515
686
 
516
687
  See `Connection.commit() <https://www.python.org/dev/peps/pep-0249/#commit>`_
517
688
  in the specification.
689
+
518
690
  """
691
+ log_query('COMMIT')
692
+ if self.host == 'singlestore.com':
693
+ return
519
694
  self._execute_command(COMMAND.COM_QUERY, 'COMMIT')
520
695
  self._read_ok_packet()
521
696
 
@@ -525,12 +700,17 @@ class Connection:
525
700
 
526
701
  See `Connection.rollback() <https://www.python.org/dev/peps/pep-0249/#rollback>`_
527
702
  in the specification.
703
+
528
704
  """
705
+ log_query('ROLLBACK')
706
+ if self.host == 'singlestore.com':
707
+ return
529
708
  self._execute_command(COMMAND.COM_QUERY, 'ROLLBACK')
530
709
  self._read_ok_packet()
531
710
 
532
711
  def show_warnings(self):
533
712
  """Send the "SHOW WARNINGS" SQL command."""
713
+ log_query('SHOW WARNINGS')
534
714
  self._execute_command(COMMAND.COM_QUERY, 'SHOW WARNINGS')
535
715
  result = self.resultclass(self)
536
716
  result.read()
@@ -540,80 +720,102 @@ class Connection:
540
720
  """
541
721
  Set current db.
542
722
 
543
- :param db: The name of the db.
723
+ db : str
724
+ The name of the db.
725
+
544
726
  """
545
727
  self._execute_command(COMMAND.COM_INIT_DB, db)
546
728
  self._read_ok_packet()
547
729
 
548
730
  def escape(self, obj, mapping=None):
549
- """Escape whatever value is passed.
731
+ """
732
+ Escape whatever value is passed.
550
733
 
551
734
  Non-standard, for internal use; do not use this in your applications.
735
+
552
736
  """
553
- if isinstance(obj, str):
554
- return "'" + self.escape_string(obj) + "'"
555
- if isinstance(obj, (bytes, bytearray)):
556
- ret = self._quote_bytes(obj)
557
- if self._binary_prefix:
558
- ret = '_binary' + ret
559
- return ret
737
+ dtype = type(obj)
738
+ if dtype is str or isinstance(obj, str):
739
+ return "'{}'".format(self.escape_string(obj))
740
+ if dtype is bytes or dtype is bytearray or isinstance(obj, (bytes, bytearray)):
741
+ return self._quote_bytes(obj)
742
+ if mapping is None:
743
+ mapping = self.encoders
560
744
  return converters.escape_item(obj, self.charset, mapping=mapping)
561
745
 
562
746
  def literal(self, obj):
563
- """Alias for escape().
747
+ """
748
+ Alias for escape().
564
749
 
565
750
  Non-standard, for internal use; do not use this in your applications.
751
+
566
752
  """
567
753
  return self.escape(obj, self.encoders)
568
754
 
569
755
  def escape_string(self, s):
756
+ """Escape a string value."""
570
757
  if self.server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES:
571
758
  return s.replace("'", "''")
572
759
  return converters.escape_string(s)
573
760
 
574
761
  def _quote_bytes(self, s):
575
762
  if self.server_status & SERVER_STATUS.SERVER_STATUS_NO_BACKSLASH_ESCAPES:
576
- return "'%s'" % (s.replace(b"'", b"''").decode('ascii', 'surrogateescape'),)
763
+ if self._binary_prefix:
764
+ return "_binary X'{}'".format(s.hex())
765
+ return "X'{}'".format(s.hex())
577
766
  return converters.escape_bytes(s)
578
767
 
579
- def cursor(self, cursor=None):
580
- """
581
- Create a new cursor to execute queries with.
582
-
583
- :param cursor: The type of cursor to create. None means use Cursor.
584
- :type cursor: :py:class:`Cursor`, :py:class:`SSCursor`, :py:class:`DictCursor`, or :py:class:`SSDictCursor`.
585
- """
586
- if cursor:
587
- # if cursor is SSCursor:
588
- # cursor = SSCursorSV
589
- # elif cursor is DictCursor:
590
- # # TODO: Passing in a cursor shouldn't affect connection output type
591
- # self.output_type = 'dicts'
592
- # elif cursor is SSDictCursor:
593
- # cursor = SSDictCursorSV
594
- # # TODO: Passing in a cursor shouldn't affect connection output type
595
- # self.output_type = 'dicts'
596
- return cursor(self)
768
+ def cursor(self):
769
+ """Create a new cursor to execute queries with."""
597
770
  return self.cursorclass(self)
598
771
 
599
772
  # The following methods are INTERNAL USE ONLY (called from Cursor)
600
773
  def query(self, sql, unbuffered=False):
774
+ """
775
+ Run a query on the server.
776
+
777
+ Internal use only.
778
+
779
+ """
601
780
  # if DEBUG:
602
781
  # print("DEBUG: sending query:", sql)
603
- if isinstance(sql, str):
604
- sql = sql.encode(self.encoding, 'surrogateescape')
605
- self._execute_command(COMMAND.COM_QUERY, sql)
606
- self._affected_rows = self._read_query_result(unbuffered=unbuffered)
782
+ handler = fusion.get_handler(sql)
783
+ if handler is not None:
784
+ self._result = fusion.execute(self, sql, handler=handler)
785
+ self._affected_rows = self._result.affected_rows
786
+ else:
787
+ if isinstance(sql, str):
788
+ sql = sql.encode(self.encoding, 'surrogateescape')
789
+ self._execute_command(COMMAND.COM_QUERY, sql)
790
+ self._affected_rows = self._read_query_result(unbuffered=unbuffered)
607
791
  return self._affected_rows
608
792
 
609
793
  def next_result(self, unbuffered=False):
794
+ """
795
+ Retrieve the next result set.
796
+
797
+ Internal use only.
798
+
799
+ """
610
800
  self._affected_rows = self._read_query_result(unbuffered=unbuffered)
611
801
  return self._affected_rows
612
802
 
613
803
  def affected_rows(self):
804
+ """
805
+ Return number of affected rows.
806
+
807
+ Internal use only.
808
+
809
+ """
614
810
  return self._affected_rows
615
811
 
616
812
  def kill(self, thread_id):
813
+ """
814
+ Execute kill command.
815
+
816
+ Internal use only.
817
+
818
+ """
617
819
  arg = struct.pack('<I', thread_id)
618
820
  self._execute_command(COMMAND.COM_PROCESS_KILL, arg)
619
821
  return self._read_ok_packet()
@@ -622,10 +824,15 @@ class Connection:
622
824
  """
623
825
  Check if the server is alive.
624
826
 
625
- :param reconnect: If the connection is closed, reconnect.
626
- :type reconnect: boolean
827
+ Parameters
828
+ ----------
829
+ reconnect : bool, optional
830
+ If the connection is closed, reconnect.
831
+
832
+ Raises
833
+ ------
834
+ Error : If the connection is closed and reconnect=False.
627
835
 
628
- :raise Error: If the connection is closed and reconnect=False.
629
836
  """
630
837
  if self._sock is None:
631
838
  if reconnect:
@@ -644,15 +851,97 @@ class Connection:
644
851
  raise
645
852
 
646
853
  def set_charset(self, charset):
854
+ """Deprecated. Use set_character_set() instead."""
855
+ # This function has been implemented in old PyMySQL.
856
+ # But this name is different from MySQLdb.
857
+ # So we keep this function for compatibility and add
858
+ # new set_character_set() function.
859
+ self.set_character_set(charset)
860
+
861
+ def set_character_set(self, charset, collation=None):
862
+ """
863
+ Set charaset (and collation) on the server.
864
+
865
+ Send "SET NAMES charset [COLLATE collation]" query.
866
+ Update Connection.encoding based on charset.
867
+
868
+ Parameters
869
+ ----------
870
+ charset : str
871
+ The charset to enable.
872
+ collation : str, optional
873
+ The collation value
874
+
875
+ """
647
876
  # Make sure charset is supported.
648
877
  encoding = charset_by_name(charset).encoding
649
878
 
650
- self._execute_command(COMMAND.COM_QUERY, 'SET NAMES %s' % self.escape(charset))
879
+ if collation:
880
+ query = f'SET NAMES {charset} COLLATE {collation}'
881
+ else:
882
+ query = f'SET NAMES {charset}'
883
+ self._execute_command(COMMAND.COM_QUERY, query)
651
884
  self._read_packet()
652
885
  self.charset = charset
653
886
  self.encoding = encoding
887
+ self.collation = collation
888
+
889
+ def _sync_connection(self):
890
+ """Synchronize connection with env variable."""
891
+ if self._in_sync:
892
+ return
893
+
894
+ if not self._track_env:
895
+ return
896
+
897
+ url = os.environ.get('SINGLESTOREDB_URL')
898
+ if not url:
899
+ return
900
+
901
+ out = {}
902
+ urlp = connection._parse_url(url)
903
+ out.update(urlp)
904
+
905
+ out = connection._cast_params(out)
906
+
907
+ # Set default port based on driver.
908
+ if 'port' not in out or not out['port']:
909
+ out['port'] = int(get_option('port') or 3306)
910
+
911
+ # If there is no user and the password is empty, remove the password key.
912
+ if 'user' not in out and not out.get('password', None):
913
+ out.pop('password', None)
914
+
915
+ if out['host'] == 'singlestore.com':
916
+ raise err.InterfaceError(0, 'Connection URL has not been established')
917
+
918
+ # If it's just a password change, we don't need to reconnect
919
+ if self._sock is not None and \
920
+ (self.host, self.port, self.user, self.db) == \
921
+ (out['host'], out['port'], out['user'], out.get('database')):
922
+ return
923
+
924
+ self.host = out['host']
925
+ self.port = out['port']
926
+ self.user = out['user']
927
+ if isinstance(out['password'], str):
928
+ self.password = out['password'].encode('latin-1')
929
+ else:
930
+ self.password = out['password'] or b''
931
+ self.db = out.get('database')
932
+ try:
933
+ self._in_sync = True
934
+ self.connect()
935
+ finally:
936
+ self._in_sync = False
654
937
 
655
938
  def connect(self, sock=None):
939
+ """
940
+ Connect to server using existing parameters.
941
+
942
+ Internal use only.
943
+
944
+ """
656
945
  self._closed = False
657
946
  try:
658
947
  if sock is None:
@@ -674,7 +963,7 @@ class Connection:
674
963
  (self.host, self.port), self.connect_timeout, **kwargs,
675
964
  )
676
965
  break
677
- except (OSError, IOError) as e:
966
+ except OSError as e:
678
967
  if e.errno == errno.EINTR:
679
968
  continue
680
969
  raise
@@ -692,18 +981,34 @@ class Connection:
692
981
  self._get_server_information()
693
982
  self._request_authentication()
694
983
 
984
+ # Send "SET NAMES" query on init for:
985
+ # - Ensure charaset (and collation) is set to the server.
986
+ # - collation_id in handshake packet may be ignored.
987
+ # - If collation is not specified, we don't know what is server's
988
+ # default collation for the charset. For example, default collation
989
+ # of utf8mb4 is:
990
+ # - MySQL 5.7, MariaDB 10.x: utf8mb4_general_ci
991
+ # - MySQL 8.0: utf8mb4_0900_ai_ci
992
+ #
993
+ # Reference:
994
+ # - https://github.com/PyMySQL/PyMySQL/issues/1092
995
+ # - https://github.com/wagtail/wagtail/issues/9477
996
+ # - https://zenn.dev/methane/articles/2023-mysql-collation (Japanese)
997
+ self.set_character_set(self.charset, self.collation)
998
+
695
999
  if self.sql_mode is not None:
696
1000
  c = self.cursor()
697
1001
  c.execute('SET sql_mode=%s', (self.sql_mode,))
1002
+ c.close()
698
1003
 
699
1004
  if self.init_command is not None:
700
1005
  c = self.cursor()
701
1006
  c.execute(self.init_command)
702
1007
  c.close()
703
- self.commit()
704
1008
 
705
1009
  if self.autocommit_mode is not None:
706
1010
  self.autocommit(self.autocommit_mode)
1011
+
707
1012
  except BaseException as e:
708
1013
  self._rfile = None
709
1014
  if sock is not None:
@@ -712,10 +1017,10 @@ class Connection:
712
1017
  except: # noqa
713
1018
  pass
714
1019
 
715
- if isinstance(e, (OSError, IOError)):
1020
+ if isinstance(e, (OSError, IOError, socket.error)):
716
1021
  exc = err.OperationalError(
717
1022
  CR.CR_CONN_HOST_ERROR,
718
- "Can't connect to MySQL server on %r (%s)" % (self.host, e),
1023
+ f'Can\'t connect to MySQL server on {self.host!r} ({e})',
719
1024
  )
720
1025
  # Keep original exception and traceback to investigate error.
721
1026
  exc.original_exception = e
@@ -730,8 +1035,11 @@ class Connection:
730
1035
  raise
731
1036
 
732
1037
  def write_packet(self, payload):
733
- """Writes an entire "mysql packet" in its entirety to the network
734
- adding its length and sequence number.
1038
+ """
1039
+ Writes an entire "mysql packet" in its entirety to the network.
1040
+
1041
+ Adds its length and sequence number.
1042
+
735
1043
  """
736
1044
  # Internal note: when you build packet manually and calls _write_bytes()
737
1045
  # directly, you should set self._next_seq_id properly.
@@ -742,11 +1050,18 @@ class Connection:
742
1050
  self._next_seq_id = (self._next_seq_id + 1) % 256
743
1051
 
744
1052
  def _read_packet(self, packet_type=MysqlPacket):
745
- """Read an entire "mysql packet" in its entirety from the network
746
- and return a MysqlPacket type that represents the results.
1053
+ """
1054
+ Read an entire "mysql packet" in its entirety from the network.
1055
+
1056
+ Raises
1057
+ ------
1058
+ OperationalError : If the connection to the MySQL server is lost.
1059
+ InternalError : If the packet sequence number is wrong.
1060
+
1061
+ Returns
1062
+ -------
1063
+ MysqlPacket
747
1064
 
748
- :raise OperationalError: If the connection to the MySQL server is lost.
749
- :raise InternalError: If the packet sequence number is wrong.
750
1065
  """
751
1066
  buff = bytearray()
752
1067
  while True:
@@ -787,12 +1102,13 @@ class Connection:
787
1102
  return packet
788
1103
 
789
1104
  def _read_bytes(self, num_bytes):
790
- self._sock.settimeout(self._read_timeout)
1105
+ if self._read_timeout is not None:
1106
+ self._sock.settimeout(self._read_timeout)
791
1107
  while True:
792
1108
  try:
793
1109
  data = self._rfile.read(num_bytes)
794
1110
  break
795
- except (IOError, OSError) as e:
1111
+ except OSError as e:
796
1112
  if e.errno == errno.EINTR:
797
1113
  continue
798
1114
  self._force_close()
@@ -812,13 +1128,14 @@ class Connection:
812
1128
  return data
813
1129
 
814
1130
  def _write_bytes(self, data):
815
- self._sock.settimeout(self._write_timeout)
1131
+ if self._write_timeout is not None:
1132
+ self._sock.settimeout(self._write_timeout)
816
1133
  try:
817
1134
  self._sock.sendall(data)
818
- except IOError as e:
1135
+ except OSError as e:
819
1136
  self._force_close()
820
1137
  raise err.OperationalError(
821
- CR.CR_SERVER_GONE_ERROR, 'MySQL server has gone away (%r)' % (e,),
1138
+ CR.CR_SERVER_GONE_ERROR, f'MySQL server has gone away ({e!r})',
822
1139
  )
823
1140
 
824
1141
  def _read_query_result(self, unbuffered=False):
@@ -841,11 +1158,18 @@ class Connection:
841
1158
 
842
1159
  def _execute_command(self, command, sql):
843
1160
  """
844
- :raise InterfaceError: If the connection is closed.
845
- :raise ValueError: If no username was specified.
1161
+ Execute command.
1162
+
1163
+ Raises
1164
+ ------
1165
+ InterfaceError : If the connection is closed.
1166
+ ValueError : If no username was specified.
1167
+
846
1168
  """
847
- if not self._sock:
848
- raise err.InterfaceError(0, '')
1169
+ self._sync_connection()
1170
+
1171
+ if self._sock is None:
1172
+ raise err.InterfaceError(0, 'The connection has been closed')
849
1173
 
850
1174
  # If the last query was unbuffered, make sure it finishes before
851
1175
  # sending new commands
@@ -882,7 +1206,7 @@ class Connection:
882
1206
  if not sql and packet_size < MAX_PACKET_LEN:
883
1207
  break
884
1208
 
885
- def _request_authentication(self):
1209
+ def _request_authentication(self): # noqa: C901
886
1210
  # https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::HandshakeResponse
887
1211
  if int(self.server_version.split('.', 1)[0]) >= 5:
888
1212
  self.client_flag |= CLIENT.MULTI_RESULTS
@@ -938,13 +1262,14 @@ class Connection:
938
1262
  data += _lenenc_int(len(authresp)) + authresp
939
1263
  elif self.server_capabilities & CLIENT.SECURE_CONNECTION:
940
1264
  data += struct.pack('B', len(authresp)) + authresp
941
- else: # pragma: no cover - not testing against servers without secure auth (>=5.0)
1265
+ else: # pragma: no cover - no testing against servers w/o secure auth (>=5.0)
942
1266
  data += authresp + b'\0'
943
1267
 
944
- if self.db and self.server_capabilities & CLIENT.CONNECT_WITH_DB:
945
- if isinstance(self.db, str):
946
- self.db = self.db.encode(self.encoding)
947
- data += self.db + b'\0'
1268
+ if self.server_capabilities & CLIENT.CONNECT_WITH_DB:
1269
+ db = self.db
1270
+ if isinstance(db, str):
1271
+ db = db.encode(self.encoding)
1272
+ data += (db or b'') + b'\0'
948
1273
 
949
1274
  if self.server_capabilities & CLIENT.PLUGIN_AUTH:
950
1275
  data += (plugin_name or b'') + b'\0'
@@ -1021,6 +1346,8 @@ class Connection:
1021
1346
  elif plugin_name == b'mysql_clear_password':
1022
1347
  # https://dev.mysql.com/doc/internals/en/clear-text-authentication.html
1023
1348
  data = self.password + b'\0'
1349
+ elif plugin_name == b'auth_gssapi_client':
1350
+ data = _auth.gssapi_auth(auth_packet.read_all())
1024
1351
  elif plugin_name == b'dialog':
1025
1352
  pkt = auth_packet
1026
1353
  while True:
@@ -1157,7 +1484,8 @@ class Connection:
1157
1484
  if self.server_capabilities & CLIENT.PLUGIN_AUTH and len(data) >= i:
1158
1485
  # Due to Bug#59453 the auth-plugin-name is missing the terminating
1159
1486
  # NUL-char in versions prior to 5.5.10 and 5.6.2.
1160
- # ref: https://dev.mysql.com/doc/internals/en/connection-phase-packets.html#packet-Protocol::Handshake
1487
+ # ref: https://dev.mysql.com/doc/internals/en/
1488
+ # connection-phase-packets.html#packet-Protocol::Handshake
1161
1489
  # didn't use version checks as mariadb is corrected and reports
1162
1490
  # earlier than those two.
1163
1491
  server_end = data.find(b'\0', i)
@@ -1183,10 +1511,19 @@ class Connection:
1183
1511
 
1184
1512
 
1185
1513
  class MySQLResult:
1514
+ """
1515
+ Results of a SQL query.
1516
+
1517
+ Parameters
1518
+ ----------
1519
+ connection : Connection
1520
+ The connection the result came from.
1521
+ unbuffered : bool, optional
1522
+ Should the reads be unbuffered?
1523
+
1524
+ """
1525
+
1186
1526
  def __init__(self, connection, unbuffered=False):
1187
- """
1188
- :type connection: Connection
1189
- """
1190
1527
  self.connection = connection
1191
1528
  self.affected_rows = None
1192
1529
  self.insert_id = None
@@ -1198,8 +1535,16 @@ class MySQLResult:
1198
1535
  self.rows = None
1199
1536
  self.has_next = None
1200
1537
  self.unbuffered_active = False
1538
+ self.converters = []
1539
+ self.fields = []
1540
+ self.encoding_errors = self.connection.encoding_errors
1201
1541
  if unbuffered:
1202
- self.init_unbuffered_query()
1542
+ try:
1543
+ self.init_unbuffered_query()
1544
+ except Exception:
1545
+ self.connection = None
1546
+ self.unbuffered_active = False
1547
+ raise
1203
1548
 
1204
1549
  def __del__(self):
1205
1550
  if self.unbuffered_active:
@@ -1220,8 +1565,13 @@ class MySQLResult:
1220
1565
 
1221
1566
  def init_unbuffered_query(self):
1222
1567
  """
1223
- :raise OperationalError: If the connection to the MySQL server is lost.
1224
- :raise InternalError:
1568
+ Initialize an unbuffered query.
1569
+
1570
+ Raises
1571
+ ------
1572
+ OperationalError : If the connection to the MySQL server is lost.
1573
+ InternalError : Other errors.
1574
+
1225
1575
  """
1226
1576
  self.unbuffered_active = True
1227
1577
  first_packet = self.connection._read_packet()
@@ -1261,7 +1611,7 @@ class MySQLResult:
1261
1611
  sender = LoadLocalFile(load_packet.filename, self.connection)
1262
1612
  try:
1263
1613
  sender.send_data()
1264
- except:
1614
+ except Exception:
1265
1615
  self.connection._read_packet() # skip ok packet
1266
1616
  raise
1267
1617
 
@@ -1281,7 +1631,8 @@ class MySQLResult:
1281
1631
  # TODO: Support CLIENT.DEPRECATE_EOF
1282
1632
  # 1) Add DEPRECATE_EOF to CAPABILITIES
1283
1633
  # 2) Mask CAPABILITIES with server_capabilities
1284
- # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: use OKPacketWrapper instead of EOFPacketWrapper
1634
+ # 3) if server_capabilities & CLIENT.DEPRECATE_EOF: use OKPacketWrapper
1635
+ # instead of EOFPacketWrapper
1285
1636
  wp = EOFPacketWrapper(packet)
1286
1637
  self.warning_count = wp.warning_count
1287
1638
  self.has_next = wp.has_next
@@ -1314,8 +1665,21 @@ class MySQLResult:
1314
1665
  # After much reading on the MySQL protocol, it appears that there is,
1315
1666
  # in fact, no way to stop MySQL from sending all the data after
1316
1667
  # executing a query, so we just spin, and wait for an EOF packet.
1317
- while self.unbuffered_active:
1318
- packet = self.connection._read_packet()
1668
+ while self.unbuffered_active and self.connection._sock is not None:
1669
+ try:
1670
+ packet = self.connection._read_packet()
1671
+ except err.OperationalError as e:
1672
+ if e.args[0] in (
1673
+ ER.QUERY_TIMEOUT,
1674
+ ER.STATEMENT_TIMEOUT,
1675
+ ):
1676
+ # if the query timed out we can simply ignore this error
1677
+ self.unbuffered_active = False
1678
+ self.connection = None
1679
+ return
1680
+
1681
+ raise
1682
+
1319
1683
  if self._check_packet_is_eof(packet):
1320
1684
  self.unbuffered_active = False
1321
1685
  self.connection = None # release reference to kill cyclic reference.
@@ -1344,7 +1708,7 @@ class MySQLResult:
1344
1708
  break
1345
1709
  if data is not None:
1346
1710
  if encoding is not None:
1347
- data = data.decode(encoding)
1711
+ data = data.decode(encoding, errors=self.encoding_errors)
1348
1712
  if DEBUG:
1349
1713
  print('DEBUG: DATA = ', data)
1350
1714
  if converter is not None:
@@ -1397,26 +1761,28 @@ class MySQLResult:
1397
1761
 
1398
1762
 
1399
1763
  class MySQLResultSV(MySQLResult):
1764
+
1400
1765
  def __init__(self, connection, unbuffered=False):
1401
1766
  MySQLResult.__init__(self, connection, unbuffered=unbuffered)
1402
1767
  self.options = {
1403
1768
  k: v for k, v in dict(
1404
1769
  default_converters=converters.decoders,
1405
- output_type=connection.output_type,
1770
+ results_type=connection.results_type,
1406
1771
  parse_json=connection.parse_json,
1407
1772
  invalid_values=connection.invalid_values,
1408
1773
  unbuffered=unbuffered,
1409
1774
  ).items() if v is not UNSET
1410
1775
  }
1411
1776
  self._read_rowdata_packet = functools.partial(
1412
- _pymysqlsv.read_rowdata_packet, self,
1777
+ _singlestoredb_accel.read_rowdata_packet, self, False,
1413
1778
  )
1414
1779
  self._read_rowdata_packet_unbuffered = functools.partial(
1415
- _pymysqlsv.read_rowdata_packet, self,
1780
+ _singlestoredb_accel.read_rowdata_packet, self, True,
1416
1781
  )
1417
1782
 
1418
1783
 
1419
1784
  class LoadLocalFile:
1785
+
1420
1786
  def __init__(self, filename, connection):
1421
1787
  self.filename = filename
1422
1788
  self.connection = connection
@@ -1437,11 +1803,12 @@ class LoadLocalFile:
1437
1803
  if not chunk:
1438
1804
  break
1439
1805
  conn.write_packet(chunk)
1440
- except IOError:
1806
+ except OSError:
1441
1807
  raise err.OperationalError(
1442
1808
  ER.FILE_NOT_FOUND,
1443
1809
  f"Can't find file '{self.filename}'",
1444
1810
  )
1445
1811
  finally:
1446
- # send the empty packet to signify we are done sending data
1447
- conn.write_packet(b'')
1812
+ if not conn._closed:
1813
+ # send the empty packet to signify we are done sending data
1814
+ conn.write_packet(b'')