singlestoredb 0.3.3__py3-none-any.whl → 1.0.3__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 (121) hide show
  1. singlestoredb/__init__.py +33 -2
  2. singlestoredb/alchemy/__init__.py +90 -0
  3. singlestoredb/auth.py +6 -4
  4. singlestoredb/config.py +116 -16
  5. singlestoredb/connection.py +489 -523
  6. singlestoredb/converters.py +275 -26
  7. singlestoredb/exceptions.py +30 -4
  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/__init__.py +0 -0
  22. singlestoredb/fusion/handlers/stage.py +257 -0
  23. singlestoredb/fusion/handlers/utils.py +162 -0
  24. singlestoredb/fusion/handlers/workspace.py +412 -0
  25. singlestoredb/fusion/registry.py +164 -0
  26. singlestoredb/fusion/result.py +399 -0
  27. singlestoredb/http/__init__.py +27 -0
  28. singlestoredb/http/connection.py +1192 -0
  29. singlestoredb/management/__init__.py +3 -2
  30. singlestoredb/management/billing_usage.py +148 -0
  31. singlestoredb/management/cluster.py +19 -14
  32. singlestoredb/management/manager.py +100 -40
  33. singlestoredb/management/organization.py +188 -0
  34. singlestoredb/management/region.py +6 -8
  35. singlestoredb/management/utils.py +253 -4
  36. singlestoredb/management/workspace.py +1153 -35
  37. singlestoredb/mysql/__init__.py +177 -0
  38. singlestoredb/mysql/_auth.py +298 -0
  39. singlestoredb/mysql/charset.py +214 -0
  40. singlestoredb/mysql/connection.py +1814 -0
  41. singlestoredb/mysql/constants/CLIENT.py +38 -0
  42. singlestoredb/mysql/constants/COMMAND.py +32 -0
  43. singlestoredb/mysql/constants/CR.py +78 -0
  44. singlestoredb/mysql/constants/ER.py +474 -0
  45. singlestoredb/mysql/constants/FIELD_TYPE.py +32 -0
  46. singlestoredb/mysql/constants/FLAG.py +15 -0
  47. singlestoredb/mysql/constants/SERVER_STATUS.py +10 -0
  48. singlestoredb/mysql/constants/__init__.py +0 -0
  49. singlestoredb/mysql/converters.py +271 -0
  50. singlestoredb/mysql/cursors.py +713 -0
  51. singlestoredb/mysql/err.py +92 -0
  52. singlestoredb/mysql/optionfile.py +20 -0
  53. singlestoredb/mysql/protocol.py +388 -0
  54. singlestoredb/mysql/tests/__init__.py +19 -0
  55. singlestoredb/mysql/tests/base.py +126 -0
  56. singlestoredb/mysql/tests/conftest.py +37 -0
  57. singlestoredb/mysql/tests/test_DictCursor.py +132 -0
  58. singlestoredb/mysql/tests/test_SSCursor.py +141 -0
  59. singlestoredb/mysql/tests/test_basic.py +452 -0
  60. singlestoredb/mysql/tests/test_connection.py +851 -0
  61. singlestoredb/mysql/tests/test_converters.py +58 -0
  62. singlestoredb/mysql/tests/test_cursor.py +141 -0
  63. singlestoredb/mysql/tests/test_err.py +16 -0
  64. singlestoredb/mysql/tests/test_issues.py +514 -0
  65. singlestoredb/mysql/tests/test_load_local.py +75 -0
  66. singlestoredb/mysql/tests/test_nextset.py +88 -0
  67. singlestoredb/mysql/tests/test_optionfile.py +27 -0
  68. singlestoredb/mysql/tests/thirdparty/__init__.py +6 -0
  69. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/__init__.py +9 -0
  70. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/capabilities.py +323 -0
  71. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/dbapi20.py +865 -0
  72. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_capabilities.py +110 -0
  73. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_dbapi20.py +224 -0
  74. singlestoredb/mysql/tests/thirdparty/test_MySQLdb/test_MySQLdb_nonstandard.py +101 -0
  75. singlestoredb/mysql/times.py +23 -0
  76. singlestoredb/pytest.py +283 -0
  77. singlestoredb/tests/empty.sql +0 -0
  78. singlestoredb/tests/ext_funcs/__init__.py +385 -0
  79. singlestoredb/tests/test.sql +210 -0
  80. singlestoredb/tests/test2.sql +1 -0
  81. singlestoredb/tests/test_basics.py +482 -117
  82. singlestoredb/tests/test_config.py +13 -15
  83. singlestoredb/tests/test_connection.py +241 -289
  84. singlestoredb/tests/test_dbapi.py +27 -0
  85. singlestoredb/tests/test_exceptions.py +0 -2
  86. singlestoredb/tests/test_ext_func.py +1193 -0
  87. singlestoredb/tests/test_ext_func_data.py +1101 -0
  88. singlestoredb/tests/test_fusion.py +465 -0
  89. singlestoredb/tests/test_http.py +32 -28
  90. singlestoredb/tests/test_management.py +588 -10
  91. singlestoredb/tests/test_plugin.py +33 -0
  92. singlestoredb/tests/test_results.py +11 -14
  93. singlestoredb/tests/test_types.py +0 -2
  94. singlestoredb/tests/test_udf.py +687 -0
  95. singlestoredb/tests/test_xdict.py +0 -2
  96. singlestoredb/tests/utils.py +3 -4
  97. singlestoredb/types.py +4 -5
  98. singlestoredb/utils/config.py +71 -12
  99. singlestoredb/utils/convert_rows.py +0 -2
  100. singlestoredb/utils/debug.py +13 -0
  101. singlestoredb/utils/mogrify.py +151 -0
  102. singlestoredb/utils/results.py +4 -3
  103. singlestoredb/utils/xdict.py +12 -12
  104. singlestoredb-1.0.3.dist-info/METADATA +139 -0
  105. singlestoredb-1.0.3.dist-info/RECORD +112 -0
  106. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/WHEEL +1 -1
  107. singlestoredb-1.0.3.dist-info/entry_points.txt +2 -0
  108. singlestoredb/drivers/__init__.py +0 -46
  109. singlestoredb/drivers/base.py +0 -200
  110. singlestoredb/drivers/cymysql.py +0 -40
  111. singlestoredb/drivers/http.py +0 -49
  112. singlestoredb/drivers/mariadb.py +0 -42
  113. singlestoredb/drivers/mysqlconnector.py +0 -51
  114. singlestoredb/drivers/mysqldb.py +0 -62
  115. singlestoredb/drivers/pymysql.py +0 -39
  116. singlestoredb/drivers/pyodbc.py +0 -67
  117. singlestoredb/http.py +0 -794
  118. singlestoredb-0.3.3.dist-info/METADATA +0 -105
  119. singlestoredb-0.3.3.dist-info/RECORD +0 -46
  120. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/LICENSE +0 -0
  121. {singlestoredb-0.3.3.dist-info → singlestoredb-1.0.3.dist-info}/top_level.txt +0 -0
@@ -1,12 +1,10 @@
1
1
  #!/usr/bin/env python
2
2
  """SingleStoreDB connections and cursors."""
3
- from __future__ import annotations
4
-
3
+ import abc
5
4
  import inspect
6
- import pprint
7
5
  import re
6
+ import warnings
8
7
  import weakref
9
- from collections import namedtuple
10
8
  from collections.abc import Mapping
11
9
  from collections.abc import MutableMapping
12
10
  from typing import Any
@@ -30,23 +28,17 @@ except ImportError:
30
28
  pass
31
29
 
32
30
  from . import auth
33
- from . import drivers
34
31
  from . import exceptions
35
- from . import types
36
32
  from .config import get_option
37
- from .drivers.base import Driver
38
- from .utils.convert_rows import convert_row
39
- from .utils.convert_rows import convert_rows
40
33
  from .utils.results import Description
41
- from .utils.results import format_results
42
34
  from .utils.results import Result
43
35
 
44
36
 
45
37
  # DB-API settings
46
38
  apilevel = '2.0'
47
39
  threadsafety = 1
48
- paramstyle = map_paramstyle = 'named'
49
- positional_paramstyle = 'numeric'
40
+ paramstyle = map_paramstyle = 'pyformat'
41
+ positional_paramstyle = 'format'
50
42
 
51
43
 
52
44
  # Type codes for character-based columns
@@ -104,7 +96,7 @@ def cast_bool_param(val: Any) -> bool:
104
96
  if val.lower() in ['on', 't', 'true', 'y', 'yes', 'enabled', 'enable']:
105
97
  return True
106
98
  elif val.lower() in ['off', 'f', 'false', 'n', 'no', 'disabled', 'disable']:
107
- return True
99
+ return False
108
100
 
109
101
  raise ValueError('Unrecognized value for bool: {}'.format(val))
110
102
 
@@ -129,10 +121,18 @@ def build_params(**kwargs: Any) -> Dict[str, Any]:
129
121
 
130
122
  # Set known parameters
131
123
  for name in inspect.getfullargspec(connect).args:
132
- if name == 'converters':
133
- out[name] = kwargs.get(name, {})
134
- elif name == 'results_format':
135
- out[name] = kwargs.get(name, get_option('results.format'))
124
+ if name == 'conv':
125
+ out[name] = kwargs.get(name, None)
126
+ elif name == 'results_format': # deprecated
127
+ if kwargs.get(name, None) is not None:
128
+ warnings.warn(
129
+ 'The `results_format=` parameter has been '
130
+ 'renamed to `results_type=`.',
131
+ DeprecationWarning,
132
+ )
133
+ out['results_type'] = kwargs.get(name, get_option('results.type'))
134
+ elif name == 'results_type':
135
+ out[name] = kwargs.get(name, get_option('results.type'))
136
136
  else:
137
137
  out[name] = kwargs.get(name, get_option(name))
138
138
 
@@ -159,6 +159,9 @@ def build_params(**kwargs: Any) -> Dict[str, Any]:
159
159
  if 'user' not in out and not out.get('password', None):
160
160
  out.pop('password', None)
161
161
 
162
+ if out.get('ssl_ca', '') and not out.get('ssl_verify_cert', None):
163
+ out['ssl_verify_cert'] = True
164
+
162
165
  return out
163
166
 
164
167
 
@@ -217,7 +220,8 @@ def _cast_params(params: Dict[str, Any]) -> Dict[str, Any]:
217
220
  dtype = param_types[key]
218
221
  if dtype is bool:
219
222
  val = cast_bool_param(val)
220
- elif getattr(dtype, '_name', '') in ['Dict', 'Mapping']:
223
+ elif getattr(dtype, '_name', '') in ['Dict', 'Mapping'] or \
224
+ str(dtype).startswith('typing.Dict'):
221
225
  val = dict(val)
222
226
  elif getattr(dtype, '_name', '') == 'List':
223
227
  val = list(val)
@@ -303,10 +307,17 @@ def quote_identifier(name: str) -> str:
303
307
  return f'`{name}`'
304
308
 
305
309
 
310
+ class Driver(object):
311
+ """Compatibility class for driver name."""
312
+
313
+ def __init__(self, name: str):
314
+ self.name = name
315
+
316
+
306
317
  class VariableAccessor(MutableMapping): # type: ignore
307
318
  """Variable accessor class."""
308
319
 
309
- def __init__(self, conn: Connection, vtype: str):
320
+ def __init__(self, conn: 'Connection', vtype: str):
310
321
  object.__setattr__(self, 'connection', weakref.proxy(conn))
311
322
  object.__setattr__(self, 'vtype', vtype.lower())
312
323
  if self.vtype not in [
@@ -328,30 +339,30 @@ class VariableAccessor(MutableMapping): # type: ignore
328
339
 
329
340
  def __getitem__(self, name: str) -> Any:
330
341
  name = _name_check(name)
331
- with self.connection._i_cursor() as cur:
332
- cur.execute('show {} variables like "{}";'.format(self.vtype, name))
333
- out = list(cur)
334
- if not out:
335
- raise KeyError(f"No variable found with the name '{name}'.")
336
- if len(out) > 1:
337
- raise KeyError(f"Multiple variables found with the name '{name}'.")
338
- return self._cast_value(out[0][1])
342
+ out = self.connection._iquery(
343
+ 'show {} variables like %s;'.format(self.vtype),
344
+ [name],
345
+ )
346
+ if not out:
347
+ raise KeyError(f"No variable found with the name '{name}'.")
348
+ if len(out) > 1:
349
+ raise KeyError(f"Multiple variables found with the name '{name}'.")
350
+ return self._cast_value(out[0]['Value'])
339
351
 
340
352
  def __setitem__(self, name: str, value: Any) -> None:
341
353
  name = _name_check(name)
342
- with self.connection._i_cursor() as cur:
343
- if value is True:
344
- value = 'ON'
345
- elif value is False:
346
- value = 'OFF'
347
- if 'local' in self.vtype:
348
- cur.execute(
349
- 'set {} {}=:1;'.format(
350
- self.vtype.replace('local', 'session'), name,
351
- ), [value],
352
- )
353
- else:
354
- cur.execute('set {} {}=:1;'.format(self.vtype, name), [value])
354
+ if value is True:
355
+ value = 'ON'
356
+ elif value is False:
357
+ value = 'OFF'
358
+ if 'local' in self.vtype:
359
+ self.connection._iquery(
360
+ 'set {} {}=%s;'.format(
361
+ self.vtype.replace('local', 'session'), name,
362
+ ), [value],
363
+ )
364
+ else:
365
+ self.connection._iquery('set {} {}=%s;'.format(self.vtype, name), [value])
355
366
 
356
367
  def __delitem__(self, name: str) -> None:
357
368
  raise TypeError('Variables can not be deleted.')
@@ -366,17 +377,15 @@ class VariableAccessor(MutableMapping): # type: ignore
366
377
  del self[name]
367
378
 
368
379
  def __len__(self) -> int:
369
- with self.connection._i_cursor() as cur:
370
- cur.execute('show {} variables;'.format(self.vtype))
371
- return len(list(cur))
380
+ out = self.connection._iquery('show {} variables;'.format(self.vtype))
381
+ return len(list(out))
372
382
 
373
383
  def __iter__(self) -> Iterator[str]:
374
- with self.connection._i_cursor() as cur:
375
- cur.execute('show {} variables;'.format(self.vtype))
376
- return iter(x[0] for x in list(cur))
384
+ out = self.connection._iquery('show {} variables;'.format(self.vtype))
385
+ return iter(list(x.values())[0] for x in out)
377
386
 
378
387
 
379
- class Cursor(object):
388
+ class Cursor(metaclass=abc.ABCMeta):
380
389
  """
381
390
  Database cursor for submitting commands and queries.
382
391
 
@@ -385,21 +394,14 @@ class Cursor(object):
385
394
 
386
395
  """
387
396
 
388
- def __init__(
389
- self, connection: Connection, cursor: Any, driver: Driver,
390
- ):
397
+ def __init__(self, connection: 'Connection'):
391
398
  """Call ``Connection.cursor`` instead."""
392
399
  self.errorhandler = connection.errorhandler
393
- self._results_format: str = connection.results_format
394
- self._conn: Optional[Connection] = weakref.proxy(connection)
395
- self._cursor = cursor
396
- self._driver = driver
400
+ self._connection: Optional[Connection] = weakref.proxy(connection)
397
401
 
398
- #: Current row of the cursor.
399
- self.rownumber: Optional[int] = None
402
+ self._rownumber: Optional[int] = None
400
403
 
401
- #: Description of columns in the last executed query.
402
- self.description: Optional[List[Description]] = None
404
+ self._description: Optional[List[Description]] = None
403
405
 
404
406
  #: Default batch size of ``fetchmany`` calls.
405
407
  self.arraysize = get_option('results.arraysize')
@@ -414,86 +416,32 @@ class Cursor(object):
414
416
  #: Number of rows affected by the last query.
415
417
  self.rowcount: int = -1
416
418
 
417
- #: Messages generated during last query.
418
- self.messages: List[str] = []
419
+ self._messages: List[Tuple[int, str]] = []
419
420
 
420
421
  #: Row ID of the last modified row.
421
422
  self.lastrowid: Optional[int] = None
422
423
 
423
424
  @property
424
- def connection(self) -> Optional[Connection]:
425
- """
426
- Return the connection that the cursor belongs to.
427
-
428
- Returns
429
- -------
430
- Connection or None
431
-
432
- """
433
- return self._conn
425
+ def messages(self) -> List[Tuple[int, str]]:
426
+ """Messages created by the server."""
427
+ return self._messages
434
428
 
435
- def _set_description(self) -> None:
436
- """
437
- Return column descriptions for the current result set.
429
+ @abc.abstractproperty
430
+ def description(self) -> Optional[List[Description]]:
431
+ """The field descriptions of the last query."""
432
+ return self._description
438
433
 
439
- Returns
440
- -------
441
- list of Description
434
+ @abc.abstractproperty
435
+ def rownumber(self) -> Optional[int]:
436
+ """The last modified row number."""
437
+ return self._rownumber
442
438
 
443
- """
444
- if self._cursor.description:
445
- self._converters.clear()
446
- out = []
447
- for i, item in enumerate(self._cursor.description):
448
- item = list(item) + [None, None]
449
- item[1] = types.ColumnType.get_code(item[1])
450
- item[6] = not(not(item[6]))
451
- out.append(Description(*item[:9]))
452
-
453
- # Setup override converters, if the SET flag is set use that
454
- # converter but keep the same type code.
455
- if item[7] and item[7] & 2048: # SET_FLAG = 2048
456
- conv = self._driver.converters.get(247, None) # SET CODE = 247
457
- else:
458
- conv = self._driver.converters.get(item[1], None)
459
-
460
- encoding = None
461
-
462
- # Determine proper encoding for character fields as needed
463
- if self._driver.returns_bytes:
464
- if item[1] in CHAR_COLUMNS:
465
- if item[8] and item[8] == 63: # BINARY / BLOB
466
- pass
467
- elif self._conn is not None:
468
- encoding = self._conn.encoding
469
- else:
470
- encoding = 'utf-8'
471
- elif item[1] == 16: # BIT
472
- pass
473
- else:
474
- encoding = 'ascii'
475
-
476
- if conv is not None:
477
- self._converters.append((i, encoding, conv))
478
- elif encoding is not None:
479
- self._converters.append((i, encoding, None))
480
-
481
- self.description = out
482
-
483
- def _update_attrs(self) -> None:
484
- """Update cursor attributes from the last query."""
485
- if self._cursor is None:
486
- return
487
- self.messages[:] = getattr(self._cursor, 'messages', [])
488
- self.lastrowid = getattr(
489
- self._cursor, 'lastrowid',
490
- getattr(self._cursor, '_lastrowid', None),
491
- ) or None
492
- self.rowcount = getattr(
493
- self._cursor, 'rowcount',
494
- getattr(self._cursor, '_rowcount', -1),
495
- )
439
+ @property
440
+ def connection(self) -> Optional['Connection']:
441
+ """the connection that the cursor belongs to."""
442
+ return self._connection
496
443
 
444
+ @abc.abstractmethod
497
445
  def callproc(
498
446
  self, name: str,
499
447
  params: Optional[Sequence[Any]] = None,
@@ -507,18 +455,23 @@ class Cursor(object):
507
455
  multiple result sets, subsequent result sets can be accessed
508
456
  using :meth:`nextset`.
509
457
 
458
+ Examples
459
+ --------
460
+ >>> cur.callproc('myprocedure', ['arg1', 'arg2'])
461
+ >>> print(cur.fetchall())
462
+
510
463
  Parameters
511
464
  ----------
512
465
  name : str
513
466
  Name of the stored procedure
514
- params : iterable, optional
467
+ params : iterable, optional
515
468
  Parameters to the stored procedure
516
469
 
517
470
  """
518
471
  # NOTE: The `callproc` interface varies quite a bit between drivers
519
472
  # so it is implemented using `execute` here.
520
473
 
521
- if self._cursor is None:
474
+ if not self.is_connected():
522
475
  raise exceptions.InterfaceError(2048, 'Cursor is closed.')
523
476
 
524
477
  name = _name_check(name)
@@ -529,186 +482,144 @@ class Cursor(object):
529
482
  keys = ', '.join([f':{i+1}' for i in range(len(params))])
530
483
  self.execute(f'CALL {name}({keys});', params)
531
484
 
485
+ @abc.abstractmethod
486
+ def is_connected(self) -> bool:
487
+ """Is the cursor still connected?"""
488
+ raise NotImplementedError
489
+
490
+ @abc.abstractmethod
532
491
  def close(self) -> None:
533
492
  """Close the cursor."""
534
- if self._cursor is None:
535
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
536
-
537
- try:
538
- self._cursor.close()
539
-
540
- # Ignore weak reference errors. It just means the connection
541
- # was closed underneath us.
542
- except ReferenceError:
543
- pass
544
-
545
- except Exception as exc:
546
- raise self._driver.convert_exception(exc)
547
-
548
- self._cursor = None
549
- self._conn = None
493
+ raise NotImplementedError
550
494
 
495
+ @abc.abstractmethod
551
496
  def execute(
552
- self, oper: str,
553
- params: Optional[Union[Sequence[Any], Mapping[str, Any]]] = None,
554
- ) -> None:
497
+ self, query: str,
498
+ args: Optional[Union[Sequence[Any], Dict[str, Any], Any]] = None,
499
+ ) -> int:
555
500
  """
556
501
  Execute a SQL statement.
557
502
 
503
+ Queries can use the ``format``-style parameters (``%s``) when using a
504
+ list of paramters or ``pyformat``-style parameters (``%(key)s``)
505
+ when using a dictionary of parameters.
506
+
558
507
  Parameters
559
508
  ----------
560
- oper : str
509
+ query : str
561
510
  The SQL statement to execute
562
- params : iterable or dict, optional
511
+ args : Sequence or dict, optional
563
512
  Parameters to substitute into the SQL code
564
513
 
565
- """
566
- if self._cursor is None:
567
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
514
+ Examples
515
+ --------
516
+ >>> cur.execute('select * from mytable')
568
517
 
569
- self.description = None
570
- self.rownumber = None
518
+ >>> cur.execute('select * from mytable where id < %s', [100])
571
519
 
572
- try:
573
- if params:
574
- param_converter = sqlparams.SQLParams(
575
- isinstance(params, Mapping) and
576
- map_paramstyle or positional_paramstyle,
577
- self._driver.dbapi.paramstyle,
578
- escape_char=True,
579
- )
580
- self._cursor.execute(*param_converter.format(oper, params))
581
- else:
582
- self._cursor.execute(oper)
583
- except Exception as exc:
584
- raise self._driver.convert_exception(exc)
520
+ >>> cur.execute('select * from mytable where id < %(max)s', dict(max=100))
521
+
522
+ Returns
523
+ -------
524
+ Number of rows affected
585
525
 
586
- self._set_description()
587
- self._update_attrs()
588
- self.rownumber = 0
526
+ """
527
+ raise NotImplementedError
589
528
 
590
529
  def executemany(
591
- self, oper: str,
592
- param_seq: Optional[Sequence[Union[Sequence[Any], Mapping[str, Any]]]] = None,
593
- ) -> None:
530
+ self, query: str,
531
+ args: Optional[Sequence[Union[Sequence[Any], Dict[str, Any], Any]]] = None,
532
+ ) -> int:
594
533
  """
595
534
  Execute SQL code against multiple sets of parameters.
596
535
 
536
+ Queries can use the ``format``-style parameters (``%s``) when using
537
+ lists of paramters or ``pyformat``-style parameters (``%(key)s``)
538
+ when using dictionaries of parameters.
539
+
597
540
  Parameters
598
541
  ----------
599
- oper : str
542
+ query : str
600
543
  The SQL statement to execute
601
- params_seq : iterable of iterables or dicts, optional
544
+ args : iterable of iterables or dicts, optional
602
545
  Sets of parameters to substitute into the SQL code
603
546
 
604
- """
605
- if self._cursor is None:
606
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
607
-
608
- self.description = None
609
- self.rownumber = None
610
-
611
- is_dataframe = False
612
- if isinstance(param_seq, DataFrame):
613
- is_dataframe = True
614
- else:
615
- param_seq = param_seq or [[]]
616
-
617
- try:
618
- # NOTE: Just implement using `execute` to cover driver inconsistencies
619
- if is_dataframe:
620
- for params in param_seq.itertuples(index=False):
621
- self.execute(oper, params)
547
+ Examples
548
+ --------
549
+ >>> cur.executemany('select * from mytable where id < %s',
550
+ ... [[100], [200], [300]])
622
551
 
623
- elif param_seq[0]:
624
- for params in param_seq:
625
- self.execute(oper, params)
626
- else:
627
- self.execute(oper)
552
+ >>> cur.executemany('select * from mytable where id < %(max)s',
553
+ ... [dict(max=100), dict(max=100), dict(max=300)])
628
554
 
629
- except Exception as exc:
630
- raise self._driver.convert_exception(exc)
555
+ Returns
556
+ -------
557
+ Number of rows affected
631
558
 
632
- self._set_description()
633
- self._update_attrs()
634
- self.rownumber = 0
559
+ """
560
+ # NOTE: Just implement using `execute` to cover driver inconsistencies
561
+ if not args:
562
+ self.execute(query)
563
+ else:
564
+ for params in args:
565
+ self.execute(query, params)
566
+ return self.rowcount
635
567
 
568
+ @abc.abstractmethod
636
569
  def fetchone(self) -> Optional[Result]:
637
570
  """
638
571
  Fetch a single row from the result set.
639
572
 
573
+ Examples
574
+ --------
575
+ >>> while True:
576
+ ... row = cur.fetchone()
577
+ ... if row is None:
578
+ ... break
579
+ ... print(row)
580
+
640
581
  Returns
641
582
  -------
642
583
  tuple
643
584
  Values of the returned row if there are rows remaining
644
585
 
645
586
  """
646
- if self._cursor is None:
647
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
648
-
649
- try:
650
- out = self._cursor.fetchone()
651
- except Exception as exc:
652
- raise self._driver.convert_exception(exc)
653
-
654
- if out is not None and self.rownumber is not None:
655
- self.rownumber += 1
656
-
657
- if out is not None:
658
- out = convert_row(tuple(out), self._converters)
659
-
660
- return format_results(
661
- self._results_format,
662
- self.description or [],
663
- out, single=True,
664
- )
587
+ raise NotImplementedError
665
588
 
589
+ @abc.abstractmethod
666
590
  def fetchmany(self, size: Optional[int] = None) -> Result:
667
591
  """
668
592
  Fetch `size` rows from the result.
669
593
 
670
594
  If `size` is not specified, the `arraysize` attribute is used.
671
595
 
596
+ Examples
597
+ --------
598
+ >>> while True:
599
+ ... out = cur.fetchmany(100)
600
+ ... if not len(out):
601
+ ... break
602
+ ... for row in out:
603
+ ... print(row)
604
+
672
605
  Returns
673
606
  -------
674
607
  list of tuples
675
608
  Values of the returned rows if there are rows remaining
676
609
 
677
610
  """
678
- if self._cursor is None:
679
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
680
-
681
- if size is not None:
682
- size = max(int(size), 1)
683
- else:
684
- size = max(int(self.arraysize), 1)
685
-
686
- try:
687
- # This is to get around a bug in mysql.connector. For some reason,
688
- # fetchmany(1) returns the same row over and over again.
689
- if size == 1:
690
- out = [self._cursor.fetchone()]
691
- else:
692
- # Don't use a keyword parameter for size=. Pyodbc fails with that.
693
- out = self._cursor.fetchmany(size)
694
- except Exception as exc:
695
- raise self._driver.convert_exception(exc)
696
-
697
- out = convert_rows(out, self._converters)
698
-
699
- formatted: Result = format_results(
700
- self._results_format, self.description or [], out,
701
- )
702
-
703
- if self.rownumber is not None:
704
- self.rownumber += len(formatted)
705
-
706
- return formatted
611
+ raise NotImplementedError
707
612
 
613
+ @abc.abstractmethod
708
614
  def fetchall(self) -> Result:
709
615
  """
710
616
  Fetch all rows in the result set.
711
617
 
618
+ Examples
619
+ --------
620
+ >>> for row in cur.fetchall():
621
+ ... print(row)
622
+
712
623
  Returns
713
624
  -------
714
625
  list of tuples
@@ -717,29 +628,22 @@ class Cursor(object):
717
628
  If there are no rows to return
718
629
 
719
630
  """
720
- if self._cursor is None:
721
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
722
-
723
- try:
724
- out = self._cursor.fetchall()
725
- except Exception as exc:
726
- raise self._driver.convert_exception(exc)
727
-
728
- out = convert_rows(out, self._converters)
729
-
730
- formatted: Result = format_results(
731
- self._results_format, self.description or [], out,
732
- )
733
-
734
- if self.rownumber is not None:
735
- self.rownumber += len(formatted)
736
-
737
- return formatted
631
+ raise NotImplementedError
738
632
 
633
+ @abc.abstractmethod
739
634
  def nextset(self) -> Optional[bool]:
740
635
  """
741
636
  Skip to the next available result set.
742
637
 
638
+ This is used when calling a procedure that returns multiple
639
+ results sets.
640
+
641
+ Note
642
+ ----
643
+ The ``nextset`` method must be called until it returns an empty
644
+ set (i.e., once more than the number of expected result sets).
645
+ This is to retain compatibility with PyMySQL and MySOLdb.
646
+
743
647
  Returns
744
648
  -------
745
649
  ``True``
@@ -748,46 +652,19 @@ class Cursor(object):
748
652
  If no other result set is available
749
653
 
750
654
  """
751
- if self._cursor is None:
752
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
753
-
754
- self.rownumber = None
755
-
756
- try:
757
- out = self._cursor.nextset()
758
- self._set_description()
759
- if out:
760
- self.rownumber = 0
761
- return True
762
- return False
763
-
764
- except Exception as exc:
765
- exc = self._driver.convert_exception(exc)
766
- if getattr(exc, 'errno', -1) == 2053:
767
- return False
768
- self.rownumber = 0
769
- return True
655
+ raise NotImplementedError
770
656
 
657
+ @abc.abstractmethod
771
658
  def setinputsizes(self, sizes: Sequence[int]) -> None:
772
659
  """Predefine memory areas for parameters."""
773
- if self._cursor is None:
774
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
775
-
776
- try:
777
- self._cursor.setinputsizes(sizes)
778
- except Exception as exc:
779
- raise self._driver.convert_exception(exc)
660
+ raise NotImplementedError
780
661
 
662
+ @abc.abstractmethod
781
663
  def setoutputsize(self, size: int, column: Optional[str] = None) -> None:
782
664
  """Set a column buffer size for fetches of large columns."""
783
- if self._cursor is None:
784
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
785
-
786
- try:
787
- self._cursor.setoutputsize(size, column)
788
- except Exception as exc:
789
- raise self._driver.convert_exception(exc)
665
+ raise NotImplementedError
790
666
 
667
+ @abc.abstractmethod
791
668
  def scroll(self, value: int, mode: str = 'relative') -> None:
792
669
  """
793
670
  Scroll the cursor to the position in the result set.
@@ -800,21 +677,7 @@ class Cursor(object):
800
677
  Where to move the cursor from: 'relative' or 'absolute'
801
678
 
802
679
  """
803
- if self._cursor is None:
804
- raise exceptions.InterfaceError(2048, 'Cursor is closed.')
805
-
806
- value = int(value)
807
- try:
808
- self._cursor.scroll(value, mode=mode)
809
- if self.rownumber is not None:
810
- if mode == 'relative':
811
- self.rownumber += value
812
- elif mode == 'absolute':
813
- self.rownumber = value
814
- else:
815
- raise ValueError(f'Unrecognized scroll mode {mode}')
816
- except Exception as exc:
817
- raise self._driver.convert_exception(exc)
680
+ raise NotImplementedError
818
681
 
819
682
  def next(self) -> Optional[Result]:
820
683
  """
@@ -830,18 +693,12 @@ class Cursor(object):
830
693
  tuple of values
831
694
 
832
695
  """
833
- if self._cursor is None:
696
+ if not self.is_connected():
834
697
  raise exceptions.InterfaceError(2048, 'Cursor is closed.')
835
-
836
- try:
837
- out = self.fetchone()
838
- if out is None:
839
- raise StopIteration
840
- return out
841
- except StopIteration:
842
- raise
843
- except Exception as exc:
844
- raise self._driver.convert_exception(exc)
698
+ out = self.fetchone()
699
+ if out is None:
700
+ raise StopIteration
701
+ return out
845
702
 
846
703
  __next__ = next
847
704
 
@@ -849,7 +706,7 @@ class Cursor(object):
849
706
  """Return result iterator."""
850
707
  return self
851
708
 
852
- def __enter__(self) -> Cursor:
709
+ def __enter__(self) -> 'Cursor':
853
710
  """Enter a context."""
854
711
  return self
855
712
 
@@ -860,24 +717,22 @@ class Cursor(object):
860
717
  """Exit a context."""
861
718
  self.close()
862
719
 
863
- def is_connected(self) -> bool:
864
- """
865
- Check if the cursor is connected.
866
-
867
- Returns
868
- -------
869
- bool
870
-
871
- """
872
- if self._conn is None:
873
- return False
874
- return self._conn.is_connected()
875
-
876
720
 
877
721
  class ShowResult(Sequence[Any]):
878
722
  """
879
723
  Simple result object.
880
724
 
725
+ This object is primarily used for displaying results to a
726
+ terminal or web browser, but it can also be treated like a
727
+ simple data frame where columns are accessible using either
728
+ dictionary key-like syntax or attribute syntax.
729
+
730
+ Examples
731
+ --------
732
+ >>> conn.show.status().Value[10]
733
+
734
+ >>> conn.show.status()[10]['Value']
735
+
881
736
  Parameters
882
737
  ----------
883
738
  *args : Any
@@ -885,10 +740,14 @@ class ShowResult(Sequence[Any]):
885
740
  **kwargs : Any
886
741
  Keyword parameters to send to underlying list constructor
887
742
 
743
+ See Also
744
+ --------
745
+ :attr:`Connection.show`
746
+
888
747
  """
889
748
 
890
749
  def __init__(self, *args: Any, **kwargs: Any) -> None:
891
- self._data: List[Any] = []
750
+ self._data: List[Dict[str, Any]] = []
892
751
  item: Any = None
893
752
  for item in list(*args, **kwargs):
894
753
  self._data.append(item)
@@ -897,41 +756,69 @@ class ShowResult(Sequence[Any]):
897
756
  return self._data[item]
898
757
 
899
758
  def __getattr__(self, name: str) -> List[Any]:
759
+ if name.startswith('_ipython'):
760
+ raise AttributeError(name)
900
761
  out = []
901
762
  for item in self._data:
902
- out.append(getattr(item, name))
763
+ out.append(item[name])
903
764
  return out
904
765
 
905
766
  def __len__(self) -> int:
906
767
  return len(self._data)
907
768
 
908
- def _repr_pretty_(self, p: Any, cycle: bool) -> None:
909
- if cycle:
910
- p.text('[...]')
911
- else:
912
- p.text('[\n')
913
- for item in self._data:
914
- p.text(' ')
915
- p.text(pprint.pformat(item))
916
- p.text('\n')
917
- p.text(']')
769
+ def __repr__(self) -> str:
770
+ if not self._data:
771
+ return ''
772
+ return '\n{}\n'.format(self._format_table(self._data))
773
+
774
+ @property
775
+ def columns(self) -> List[str]:
776
+ """The columns in the result."""
777
+ if not self._data:
778
+ return []
779
+ return list(self._data[0].keys())
780
+
781
+ def _format_table(self, rows: Sequence[Dict[str, Any]]) -> str:
782
+ if not self._data:
783
+ return ''
784
+
785
+ keys = rows[0].keys()
786
+ lens = [len(x) for x in keys]
787
+
788
+ for row in self._data:
789
+ align = ['<'] * len(keys)
790
+ for i, k in enumerate(keys):
791
+ lens[i] = max(lens[i], len(str(row[k])))
792
+ align[i] = '<' if isinstance(row[k], (bytes, bytearray, str)) else '>'
793
+
794
+ fmt = '| %s |' % '|'.join([' {:%s%d} ' % (x, y) for x, y in zip(align, lens)])
795
+
796
+ out = []
797
+ out.append(fmt.format(*keys))
798
+ out.append('-' * len(out[0]))
799
+ for row in rows:
800
+ out.append(fmt.format(*[str(x) for x in row.values()]))
801
+ return '\n'.join(out)
802
+
803
+ def __str__(self) -> str:
804
+ return self.__repr__()
918
805
 
919
806
  def _repr_html_(self) -> str:
920
807
  if not self._data:
921
808
  return ''
922
809
  cell_style = 'style="text-align: left; vertical-align: top"'
923
810
  out = []
924
- out.append('<table>')
811
+ out.append('<table border="1" class="dataframe">')
925
812
  out.append('<thead>')
926
813
  out.append('<tr>')
927
- for name in self._data[0]._fields:
814
+ for name in self._data[0].keys():
928
815
  out.append(f'<th {cell_style}>{name}</th>')
929
816
  out.append('</tr>')
930
817
  out.append('</thead>')
931
818
  out.append('<tbody>')
932
819
  for row in self._data:
933
820
  out.append('<tr>')
934
- for item in row:
821
+ for item in row.values():
935
822
  out.append(f'<td {cell_style}>{item}</td>')
936
823
  out.append('</tr>')
937
824
  out.append('</tbody>')
@@ -940,161 +827,188 @@ class ShowResult(Sequence[Any]):
940
827
 
941
828
 
942
829
  class ShowAccessor(object):
943
- """Accessor for ``SHOW`` commands."""
830
+ """
831
+ Accessor for ``SHOW`` commands.
944
832
 
945
- def __init__(self, conn: Connection):
833
+ See Also
834
+ --------
835
+ :attr:`Connection.show`
836
+
837
+ """
838
+
839
+ def __init__(self, conn: 'Connection'):
946
840
  self._conn = conn
947
841
 
948
842
  def columns(self, table: str, full: bool = False) -> ShowResult:
949
843
  """Show the column information for the given table."""
950
844
  table = quote_identifier(table)
951
845
  if full:
952
- return self._query(f'full columns in {table}')
953
- return self._query(f'columns in {table}')
846
+ return self._iquery(f'full columns in {table}')
847
+ return self._iquery(f'columns in {table}')
954
848
 
955
849
  def tables(self, extended: bool = False) -> ShowResult:
956
850
  """Show tables in the current database."""
957
851
  if extended:
958
- return self._query('tables extended')
959
- return self._query('tables')
852
+ return self._iquery('tables extended')
853
+ return self._iquery('tables')
960
854
 
961
855
  def warnings(self) -> ShowResult:
962
856
  """Show warnings."""
963
- return self._query('warnings')
857
+ return self._iquery('warnings')
964
858
 
965
859
  def errors(self) -> ShowResult:
966
860
  """Show errors."""
967
- return self._query('errors')
861
+ return self._iquery('errors')
968
862
 
969
863
  def databases(self, extended: bool = False) -> ShowResult:
970
864
  """Show all databases in the server."""
971
865
  if extended:
972
- return self._query('databases extended')
973
- return self._query('databases')
866
+ return self._iquery('databases extended')
867
+ return self._iquery('databases')
974
868
 
975
869
  def database_status(self) -> ShowResult:
976
870
  """Show status of the current database."""
977
- return self._query('database status')
871
+ return self._iquery('database status')
978
872
 
979
873
  def global_status(self) -> ShowResult:
980
874
  """Show global status of the current server."""
981
- return self._query('global status')
875
+ return self._iquery('global status')
982
876
 
983
877
  def indexes(self, table: str) -> ShowResult:
984
878
  """Show all indexes in the given table."""
985
879
  table = quote_identifier(table)
986
- return self._query('indexes in {table}')
880
+ return self._iquery(f'indexes in {table}')
987
881
 
988
882
  def functions(self) -> ShowResult:
989
883
  """Show all functions in the current database."""
990
- return self._query('functions')
884
+ return self._iquery('functions')
991
885
 
992
886
  def partitions(self, extended: bool = False) -> ShowResult:
993
887
  """Show partitions in the current database."""
994
888
  if extended:
995
- return self._query('partitions extended')
996
- return self._query('partitions')
889
+ return self._iquery('partitions extended')
890
+ return self._iquery('partitions')
997
891
 
998
892
  def pipelines(self) -> ShowResult:
999
893
  """Show all pipelines in the current database."""
1000
- return self._query('pipelines')
894
+ return self._iquery('pipelines')
1001
895
 
1002
- def plan(self, plan_id: str, json: bool = False) -> ShowResult:
896
+ def plan(self, plan_id: int, json: bool = False) -> ShowResult:
1003
897
  """Show the plan for the given plan ID."""
1004
- plan_id = quote_identifier(plan_id)
898
+ plan_id = int(plan_id)
1005
899
  if json:
1006
- return self._query(f'plan json {plan_id}')
1007
- return self._query(f'plan {plan_id}')
900
+ return self._iquery(f'plan json {plan_id}')
901
+ return self._iquery(f'plan {plan_id}')
1008
902
 
1009
903
  def plancache(self) -> ShowResult:
1010
904
  """Show all query statements compiled and executed."""
1011
- return self._query('plancache')
905
+ return self._iquery('plancache')
1012
906
 
1013
907
  def processlist(self) -> ShowResult:
1014
908
  """Show details about currently running threads."""
1015
- return self._query('processlist')
909
+ return self._iquery('processlist')
1016
910
 
1017
911
  def reproduction(self, outfile: Optional[str] = None) -> ShowResult:
1018
912
  """Show troubleshooting data for query optimizer and code generation."""
1019
913
  if outfile:
1020
914
  outfile = outfile.replace('"', r'\"')
1021
- return self._query('reproduction into outfile "{outfile}"')
1022
- return self._query('reproduction')
915
+ return self._iquery('reproduction into outfile "{outfile}"')
916
+ return self._iquery('reproduction')
1023
917
 
1024
918
  def schemas(self) -> ShowResult:
1025
919
  """Show schemas in the server."""
1026
- return self._query('schemas')
920
+ return self._iquery('schemas')
1027
921
 
1028
922
  def session_status(self) -> ShowResult:
1029
923
  """Show server status information for a session."""
1030
- return self._query('session status')
924
+ return self._iquery('session status')
1031
925
 
1032
926
  def status(self, extended: bool = False) -> ShowResult:
1033
927
  """Show server status information."""
1034
928
  if extended:
1035
- return self._query('status extended')
1036
- return self._query('status')
929
+ return self._iquery('status extended')
930
+ return self._iquery('status')
1037
931
 
1038
932
  def table_status(self) -> ShowResult:
1039
933
  """Show table status information for the current database."""
1040
- return self._query('table status')
934
+ return self._iquery('table status')
1041
935
 
1042
936
  def procedures(self) -> ShowResult:
1043
937
  """Show all procedures in the current database."""
1044
- return self._query('procedures')
938
+ return self._iquery('procedures')
1045
939
 
1046
940
  def aggregates(self) -> ShowResult:
1047
941
  """Show all aggregate functions in the current database."""
1048
- return self._query('aggregates')
942
+ return self._iquery('aggregates')
1049
943
 
1050
944
  def create_aggregate(self, name: str) -> ShowResult:
1051
945
  """Show the function creation code for the given aggregate function."""
1052
946
  name = quote_identifier(name)
1053
- return self._query(f'create aggregate {name}')
947
+ return self._iquery(f'create aggregate {name}')
1054
948
 
1055
949
  def create_function(self, name: str) -> ShowResult:
1056
950
  """Show the function creation code for the given function."""
1057
951
  name = quote_identifier(name)
1058
- return self._query(f'create function {name}')
952
+ return self._iquery(f'create function {name}')
1059
953
 
1060
954
  def create_pipeline(self, name: str, extended: bool = False) -> ShowResult:
1061
955
  """Show the pipeline creation code for the given pipeline."""
1062
956
  name = quote_identifier(name)
1063
957
  if extended:
1064
- return self._query(f'create pipeline {name} extended')
1065
- return self._query(f'create pipeline {name}')
958
+ return self._iquery(f'create pipeline {name} extended')
959
+ return self._iquery(f'create pipeline {name}')
1066
960
 
1067
961
  def create_table(self, name: str) -> ShowResult:
1068
962
  """Show the table creation code for the given table."""
1069
963
  name = quote_identifier(name)
1070
- return self._query(f'create table {name}')
964
+ return self._iquery(f'create table {name}')
1071
965
 
1072
966
  def create_view(self, name: str) -> ShowResult:
1073
967
  """Show the view creation code for the given view."""
1074
968
  name = quote_identifier(name)
1075
- return self._query(f'create view {name}')
1076
-
1077
- def _query(self, qtype: str) -> ShowResult:
969
+ return self._iquery(f'create view {name}')
970
+
971
+ # def grants(
972
+ # self,
973
+ # user: Optional[str] = None,
974
+ # hostname: Optional[str] = None,
975
+ # role: Optional[str] = None
976
+ # ) -> ShowResult:
977
+ # """Show the privileges for the given user or role."""
978
+ # if user:
979
+ # if not re.match(r'^[\w+-_]+$', user):
980
+ # raise ValueError(f'User name is not valid: {user}')
981
+ # if hostname and not re.match(r'^[\w+-_\.]+$', hostname):
982
+ # raise ValueError(f'Hostname is not valid: {hostname}')
983
+ # if hostname:
984
+ # return self._iquery(f"grants for '{user}@{hostname}'")
985
+ # return self._iquery(f"grants for '{user}'")
986
+ # if role:
987
+ # if not re.match(r'^[\w+-_]+$', role):
988
+ # raise ValueError(f'Role is not valid: {role}')
989
+ # return self._iquery(f"grants for role '{role}'")
990
+ # return self._iquery('grants')
991
+
992
+ def _iquery(self, qtype: str) -> ShowResult:
1078
993
  """Query the given object type."""
1079
- with self._conn._i_cursor() as cur:
1080
- cur.execute(f'show {qtype}')
1081
- out = []
1082
- if cur.description:
1083
- names = [under2camel(str(x[0]).replace(' ', '')) for x in cur.description]
1084
- names[0] = 'Name'
1085
- item_type = namedtuple('Row', names) # type: ignore
1086
- for item in cur.fetchall():
1087
- out.append(item_type(*item))
1088
- return ShowResult(out)
1089
-
1090
-
1091
- class Connection(object):
994
+ out = self._conn._iquery(f'show {qtype}')
995
+ for i, row in enumerate(out):
996
+ new_row = {}
997
+ for j, (k, v) in enumerate(row.items()):
998
+ if j == 0:
999
+ k = 'Name'
1000
+ new_row[under2camel(k)] = v
1001
+ out[i] = new_row
1002
+ return ShowResult(out)
1003
+
1004
+
1005
+ class Connection(metaclass=abc.ABCMeta):
1092
1006
  """
1093
1007
  SingleStoreDB connection.
1094
1008
 
1095
1009
  Instances of this object are typically created through the
1096
1010
  :func:`singlestoredb.connect` function rather than creating them directly.
1097
- See the :func:`connect` function for parameter definitions.
1011
+ See the :func:`singlestoredb.connect` function for parameter definitions.
1098
1012
 
1099
1013
  See Also
1100
1014
  --------
@@ -1113,20 +1027,25 @@ class Connection(object):
1113
1027
  ProgrammingError = exceptions.ProgrammingError
1114
1028
  NotSupportedError = exceptions.NotSupportedError
1115
1029
 
1030
+ #: Read-only DB-API parameter style
1031
+ paramstyle = 'pyformat'
1032
+
1033
+ # Must be set by subclass
1034
+ driver = ''
1035
+
1036
+ # Populated when first needed
1037
+ _map_param_converter: Optional[sqlparams.SQLParams] = None
1038
+ _positional_param_converter: Optional[sqlparams.SQLParams] = None
1039
+
1116
1040
  def __init__(self, **kwargs: Any):
1117
1041
  """Call :func:`singlestoredb.connect` instead."""
1118
- self._conn: Optional[Any] = None
1042
+ self.connection_params: Dict[str, Any] = kwargs
1119
1043
  self.errorhandler = None
1120
- self.connection_params: Dict[str, Any] = build_params(**kwargs)
1121
-
1122
- #: Query results format ('tuple', 'namedtuple', 'dict', 'dataframe')
1123
- self.results_format = self.connection_params.pop(
1124
- 'results_format',
1125
- get_option('results.format'),
1126
- )
1044
+ self._results_type: str = kwargs.get('results_type', None) or 'tuples'
1127
1045
 
1128
1046
  #: Session encoding
1129
- self.encoding = self.connection_params.get('charset', 'utf-8').replace('mb4', '')
1047
+ self.encoding = self.connection_params.get('charset', None) or 'utf-8'
1048
+ self.encoding = self.encoding.replace('mb4', '')
1130
1049
 
1131
1050
  # Handle various authentication types
1132
1051
  credential_type = self.connection_params.get('credential_type', None)
@@ -1136,14 +1055,6 @@ class Connection(object):
1136
1055
  self.connection_params['password'] = str(info)
1137
1056
  self.connection_params['credential_type'] = auth.JWT
1138
1057
 
1139
- drv_name = re.sub(r'^\w+\+', r'', self.connection_params['driver']).lower()
1140
- self._driver = drivers.get_driver(drv_name, self.connection_params)
1141
-
1142
- try:
1143
- self._conn = self._driver.connect()
1144
- except Exception as exc:
1145
- raise self._driver.convert_exception(exc)
1146
-
1147
1058
  #: Attribute-like access to global server variables
1148
1059
  self.globals = VariableAccessor(self, 'global')
1149
1060
 
@@ -1162,41 +1073,93 @@ class Connection(object):
1162
1073
  #: Attribute-like access to all cluster server variables
1163
1074
  self.cluster_vars = VariableAccessor(self, 'cluster')
1164
1075
 
1076
+ # For backwards compatibility with SQLAlchemy package
1077
+ self._driver = Driver(self.driver)
1078
+
1079
+ # Output decoders
1080
+ self.decoders: Dict[int, Callable[[Any], Any]] = {}
1081
+
1082
+ @classmethod
1083
+ def _convert_params(
1084
+ cls, oper: str,
1085
+ params: Optional[Union[Sequence[Any], Dict[str, Any], Any]],
1086
+ ) -> Tuple[Any, ...]:
1087
+ """Convert query to correct parameter format."""
1088
+ if params:
1089
+
1090
+ if cls._map_param_converter is None:
1091
+ cls._map_param_converter = sqlparams.SQLParams(
1092
+ map_paramstyle, cls.paramstyle, escape_char=True,
1093
+ )
1094
+
1095
+ if cls._positional_param_converter is None:
1096
+ cls._positional_param_converter = sqlparams.SQLParams(
1097
+ positional_paramstyle, cls.paramstyle, escape_char=True,
1098
+ )
1099
+
1100
+ is_sequence = isinstance(params, Sequence) \
1101
+ and not isinstance(params, str) \
1102
+ and not isinstance(params, bytes)
1103
+ is_mapping = isinstance(params, Mapping)
1104
+
1105
+ param_converter = cls._map_param_converter \
1106
+ if is_mapping else cls._positional_param_converter
1107
+
1108
+ if not is_sequence and not is_mapping:
1109
+ params = [params]
1110
+
1111
+ return param_converter.format(oper, params)
1112
+
1113
+ return (oper, None)
1114
+
1165
1115
  def autocommit(self, value: bool = True) -> None:
1166
1116
  """Set autocommit mode."""
1167
- if self._conn is None:
1168
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1169
1117
  self.locals.autocommit = bool(value)
1170
1118
 
1119
+ @abc.abstractmethod
1120
+ def connect(self) -> 'Connection':
1121
+ """Connect to the server."""
1122
+ raise NotImplementedError
1123
+
1124
+ def _iquery(
1125
+ self, oper: str,
1126
+ params: Optional[Union[Sequence[Any], Dict[str, Any]]] = None,
1127
+ fix_names: bool = True,
1128
+ ) -> List[Dict[str, Any]]:
1129
+ """Return the results of a query as a list of dicts (for internal use)."""
1130
+ with self.cursor() as cur:
1131
+ cur.execute(oper, params)
1132
+ if not re.match(r'^\s*(select|show|call|echo)\s+', oper, flags=re.I):
1133
+ return []
1134
+ out = list(cur.fetchall())
1135
+ if not out:
1136
+ return []
1137
+ if isinstance(out, DataFrame):
1138
+ out = out.to_dict(orient='records')
1139
+ elif isinstance(out[0], (tuple, list)):
1140
+ if cur.description:
1141
+ names = [x[0] for x in cur.description]
1142
+ if fix_names:
1143
+ names = [under2camel(str(x).replace(' ', '')) for x in names]
1144
+ out = [{k: v for k, v in zip(names, row)} for row in out]
1145
+ return out
1146
+
1147
+ @abc.abstractmethod
1171
1148
  def close(self) -> None:
1172
1149
  """Close the database connection."""
1173
- if self._conn is None:
1174
- return None
1175
- try:
1176
- self._conn.close()
1177
- except Exception as exc:
1178
- raise self._driver.convert_exception(exc)
1179
- finally:
1180
- self._conn = None
1150
+ raise NotImplementedError
1181
1151
 
1152
+ @abc.abstractmethod
1182
1153
  def commit(self) -> None:
1183
1154
  """Commit the pending transaction."""
1184
- if self._conn is None:
1185
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1186
- try:
1187
- self._conn.commit()
1188
- except Exception as exc:
1189
- raise self._driver.convert_exception(exc)
1155
+ raise NotImplementedError
1190
1156
 
1157
+ @abc.abstractmethod
1191
1158
  def rollback(self) -> None:
1192
1159
  """Rollback the pending transaction."""
1193
- if self._conn is None:
1194
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1195
- try:
1196
- self._conn.rollback()
1197
- except Exception as exc:
1198
- raise self._driver.convert_exception(exc)
1160
+ raise NotImplementedError
1199
1161
 
1162
+ @abc.abstractmethod
1200
1163
  def cursor(self) -> Cursor:
1201
1164
  """
1202
1165
  Create a new cursor object.
@@ -1210,48 +1173,14 @@ class Connection(object):
1210
1173
  :class:`Cursor`
1211
1174
 
1212
1175
  """
1213
- if self._conn is None:
1214
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1215
- try:
1216
- cur = self._conn.cursor()
1217
- except Exception as exc:
1218
- raise self._driver.convert_exception(exc)
1219
- return Cursor(self, cur, self._driver)
1220
-
1221
- def _i_cursor(self) -> Cursor:
1222
- """
1223
- Create a cursor for internal use.
1224
-
1225
- Internal cursors always return tuples in results.
1226
- These are used to ensure that methods that query the database
1227
- have a consistent results structure regardless of the
1228
- `results.format` option.
1229
-
1230
- Returns
1231
- -------
1232
- Cursor
1233
-
1234
- """
1235
- out = self.cursor()
1236
- out._results_format = 'tuple'
1237
- return out
1238
-
1239
- @property
1240
- def messages(self) -> Sequence[tuple[int, str]]:
1241
- """
1242
- Return messages generated by the connection.
1243
-
1244
- Returns
1245
- -------
1246
- list of tuples
1247
- Each tuple contains an int code and a message
1176
+ raise NotImplementedError
1248
1177
 
1249
- """
1250
- if self._conn is None:
1251
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1252
- return self._conn.messages
1178
+ @abc.abstractproperty
1179
+ def messages(self) -> List[Tuple[int, str]]:
1180
+ """Messages generated during the connection."""
1181
+ raise NotImplementedError
1253
1182
 
1254
- def __enter__(self) -> Connection:
1183
+ def __enter__(self) -> 'Connection':
1255
1184
  """Enter a context."""
1256
1185
  return self
1257
1186
 
@@ -1262,6 +1191,7 @@ class Connection(object):
1262
1191
  """Exit a context."""
1263
1192
  self.close()
1264
1193
 
1194
+ @abc.abstractmethod
1265
1195
  def is_connected(self) -> bool:
1266
1196
  """
1267
1197
  Determine if the database is still connected.
@@ -1271,12 +1201,7 @@ class Connection(object):
1271
1201
  bool
1272
1202
 
1273
1203
  """
1274
- if self._conn is None:
1275
- return False
1276
- try:
1277
- return self._driver.is_connected(self._conn)
1278
- except Exception as exc:
1279
- raise self._driver.convert_exception(exc)
1204
+ raise NotImplementedError
1280
1205
 
1281
1206
  def enable_data_api(self, port: Optional[int] = None) -> int:
1282
1207
  """
@@ -1302,14 +1227,11 @@ class Connection(object):
1302
1227
  port number of the HTTP server
1303
1228
 
1304
1229
  """
1305
- if self._conn is None:
1306
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1307
- with self._i_cursor() as cur:
1308
- if port is not None:
1309
- self.globals.http_proxy_port = int(port)
1310
- self.globals.http_api = True
1311
- cur.execute('restart proxy')
1312
- return int(self.globals.http_proxy_port)
1230
+ if port is not None:
1231
+ self.globals.http_proxy_port = int(port)
1232
+ self.globals.http_api = True
1233
+ self._iquery('restart proxy')
1234
+ return int(self.globals.http_proxy_port)
1313
1235
 
1314
1236
  enable_http_api = enable_data_api
1315
1237
 
@@ -1322,11 +1244,8 @@ class Connection(object):
1322
1244
  :meth:`enable_data_api`
1323
1245
 
1324
1246
  """
1325
- if self._conn is None:
1326
- raise exceptions.InterfaceError(2048, 'Connection is closed.')
1327
- with self._i_cursor() as cur:
1328
- self.globals.http_api = False
1329
- cur.execute('restart proxy')
1247
+ self.globals.http_api = False
1248
+ self._iquery('restart proxy')
1330
1249
 
1331
1250
  disable_http_api = disable_data_api
1332
1251
 
@@ -1347,14 +1266,25 @@ def connect(
1347
1266
  password: Optional[str] = None, port: Optional[int] = None,
1348
1267
  database: Optional[str] = None, driver: Optional[str] = None,
1349
1268
  pure_python: Optional[bool] = None, local_infile: Optional[bool] = None,
1350
- odbc_driver: Optional[str] = None, charset: Optional[str] = None,
1269
+ charset: Optional[str] = None,
1351
1270
  ssl_key: Optional[str] = None, ssl_cert: Optional[str] = None,
1352
1271
  ssl_ca: Optional[str] = None, ssl_disabled: Optional[bool] = None,
1353
- ssl_cipher: Optional[str] = None,
1354
- converters: Optional[Dict[int, Callable[..., Any]]] = None,
1355
- results_format: Optional[str] = None,
1272
+ ssl_cipher: Optional[str] = None, ssl_verify_cert: Optional[bool] = None,
1273
+ ssl_verify_identity: Optional[bool] = None,
1274
+ conv: Optional[Dict[int, Callable[..., Any]]] = None,
1356
1275
  credential_type: Optional[str] = None,
1357
1276
  autocommit: Optional[bool] = None,
1277
+ results_type: Optional[str] = None,
1278
+ buffered: Optional[bool] = None,
1279
+ results_format: Optional[str] = None,
1280
+ program_name: Optional[str] = None,
1281
+ conn_attrs: Optional[Dict[str, str]] = None,
1282
+ multi_statements: Optional[bool] = None,
1283
+ connect_timeout: Optional[int] = None,
1284
+ nan_as_null: Optional[bool] = None,
1285
+ inf_as_null: Optional[bool] = None,
1286
+ encoding_errors: Optional[str] = None,
1287
+ track_env: Optional[bool] = None,
1358
1288
  ) -> Connection:
1359
1289
  """
1360
1290
  Return a SingleStoreDB connection.
@@ -1364,7 +1294,7 @@ def connect(
1364
1294
  host : str, optional
1365
1295
  Hostname, IP address, or URL that describes the connection.
1366
1296
  The scheme or protocol defines which database connector to use.
1367
- By default, the ``pymysql`` scheme is used. To connect to the
1297
+ By default, the ``mysql`` scheme is used. To connect to the
1368
1298
  HTTP API, the scheme can be set to ``http`` or ``https``. The username,
1369
1299
  password, host, and port are specified as in a standard URL. The path
1370
1300
  indicates the database name. The overall form of the URL is:
@@ -1384,8 +1314,6 @@ def connect(
1384
1314
  Use the connector in pure Python mode
1385
1315
  local_infile : bool, optional
1386
1316
  Allow local file uploads
1387
- odbc_driver : str, optional
1388
- Name of the ODBC driver to use for ODBC connections
1389
1317
  charset : str, optional
1390
1318
  Character set for string values
1391
1319
  ssl_key : str, optional
@@ -1398,14 +1326,41 @@ def connect(
1398
1326
  Sets the SSL cipher list
1399
1327
  ssl_disabled : bool, optional
1400
1328
  Disable SSL usage
1401
- converters : dict[int, Callable], optional
1329
+ ssl_verify_cert : bool, optional
1330
+ Verify the server's certificate. This is automatically enabled if
1331
+ ``ssl_ca`` is also specified.
1332
+ ssl_verify_identity : bool, optional
1333
+ Verify the server's identity
1334
+ conv : dict[int, Callable], optional
1402
1335
  Dictionary of data conversion functions
1403
- results_format : str, optional
1404
- Format of query results: tuple, namedtuple, dict, or dataframe
1405
1336
  credential_type : str, optional
1406
1337
  Type of authentication to use: auth.PASSWORD, auth.JWT, or auth.BROWSER_SSO
1407
1338
  autocommit : bool, optional
1408
1339
  Enable autocommits
1340
+ results_type : str, optional
1341
+ The form of the query results: tuples, namedtuples, dicts
1342
+ results_format : str, optional
1343
+ Deprecated. This option has been renamed to results_type.
1344
+ program_name : str, optional
1345
+ Name of the program
1346
+ conn_attrs : dict, optional
1347
+ Additional connection attributes for telemetry. Example:
1348
+ {'program_version': "1.0.2", "_connector_name": "dbt connector"}
1349
+ multi_statements: bool, optional
1350
+ Should multiple statements be allowed within a single query?
1351
+ connect_timeout : int, optional
1352
+ The timeout for connecting to the database in seconds.
1353
+ (default: 10, min: 1, max: 31536000)
1354
+ nan_as_null : bool, optional
1355
+ Should NaN values be treated as NULLs when used in parameter
1356
+ substitutions including uploaded data?
1357
+ inf_as_null : bool, optional
1358
+ Should Inf values be treated as NULLs when used in parameter
1359
+ substitutions including uploaded data?
1360
+ encoding_errors : str, optional
1361
+ The error handler name for value decoding errors
1362
+ track_env : bool, optional
1363
+ Should the connection track the SINGLESTOREDB_URL environment variable?
1409
1364
 
1410
1365
  Examples
1411
1366
  --------
@@ -1461,4 +1416,15 @@ def connect(
1461
1416
  :class:`Connection`
1462
1417
 
1463
1418
  """
1464
- return Connection(**dict(locals()))
1419
+ params = build_params(**dict(locals()))
1420
+ driver = params.get('driver', 'mysql')
1421
+
1422
+ if not driver or driver == 'mysql':
1423
+ from .mysql.connection import Connection # type: ignore
1424
+ return Connection(**params)
1425
+
1426
+ if driver in ['http', 'https']:
1427
+ from .http.connection import Connection
1428
+ return Connection(**params)
1429
+
1430
+ raise ValueError(f'Unrecognized protocol: {driver}')