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
@@ -1,7 +1,10 @@
1
1
  # type: ignore
2
2
  import re
3
+ from collections import namedtuple
3
4
 
4
5
  from . import err
6
+ from ..connection import Cursor as BaseCursor
7
+ from ..utils.debug import log_query
5
8
 
6
9
 
7
10
  #: Regular expression for :meth:`Cursor.executemany`.
@@ -15,15 +18,21 @@ RE_INSERT_VALUES = re.compile(
15
18
  )
16
19
 
17
20
 
18
- class Cursor:
21
+ class Cursor(BaseCursor):
19
22
  """
20
23
  This is the object used to interact with the database.
21
24
 
22
25
  Do not create an instance of a Cursor yourself. Call
23
- connections.Connection.cursor().
26
+ connection.Connection.cursor().
24
27
 
25
28
  See `Cursor <https://www.python.org/dev/peps/pep-0249/#cursor-objects>`_ in
26
29
  the specification.
30
+
31
+ Parameters
32
+ ----------
33
+ connection : Connection
34
+ The connection the cursor is associated with.
35
+
27
36
  """
28
37
 
29
38
  #: Max statement size which :meth:`executemany` generates.
@@ -33,27 +42,54 @@ class Cursor:
33
42
  max_stmt_length = 1024000
34
43
 
35
44
  def __init__(self, connection):
36
- self.connection = connection
37
- self.description = None
38
- self.rownumber = 0
45
+ self._connection = connection
46
+ self.warning_count = 0
47
+ self._description = None
48
+ self._rownumber = 0
39
49
  self.rowcount = -1
40
50
  self.arraysize = 1
41
51
  self._executed = None
42
52
  self._result = None
43
53
  self._rows = None
54
+ self.lastrowid = None
55
+
56
+ @property
57
+ def messages(self):
58
+ # TODO
59
+ return []
60
+
61
+ @property
62
+ def description(self):
63
+ return self._description
64
+
65
+ @property
66
+ def connection(self):
67
+ return self._connection
68
+
69
+ @property
70
+ def rownumber(self):
71
+ return self._rownumber
44
72
 
45
73
  def close(self):
46
- """
47
- Closing a cursor just exhausts all remaining data.
48
- """
49
- conn = self.connection
74
+ """Closing a cursor just exhausts all remaining data."""
75
+ conn = self._connection
50
76
  if conn is None:
51
77
  return
52
78
  try:
53
79
  while self.nextset():
54
80
  pass
55
81
  finally:
56
- self.connection = None
82
+ self._connection = None
83
+
84
+ @property
85
+ def open(self) -> bool:
86
+ conn = self._connection
87
+ if conn is None:
88
+ return False
89
+ return True
90
+
91
+ def is_connected(self):
92
+ return self.open
57
93
 
58
94
  def __enter__(self):
59
95
  return self
@@ -63,9 +99,9 @@ class Cursor:
63
99
  self.close()
64
100
 
65
101
  def _get_db(self):
66
- if not self.connection:
102
+ if not self._connection:
67
103
  raise err.ProgrammingError('Cursor closed')
68
- return self.connection
104
+ return self._connection
69
105
 
70
106
  def _check_executed(self):
71
107
  if not self._executed:
@@ -99,87 +135,94 @@ class Cursor:
99
135
  def nextset(self):
100
136
  return self._nextset(False)
101
137
 
102
- def _ensure_bytes(self, x, encoding=None):
103
- if isinstance(x, str):
104
- x = x.encode(encoding)
105
- elif isinstance(x, (tuple, list)):
106
- x = type(x)(self._ensure_bytes(v, encoding=encoding) for v in x)
107
- return x
108
-
109
138
  def _escape_args(self, args, conn):
110
- if isinstance(args, (tuple, list)):
111
- return tuple(conn.literal(arg) for arg in args)
112
- elif isinstance(args, dict):
113
- return {key: conn.literal(val) for (key, val) in args.items()}
114
- else:
115
- # If it's not a dictionary let's try escaping it anyways.
116
- # Worst case it will throw a Value error
117
- return conn.escape(args)
139
+ dtype = type(args)
140
+ literal = conn.literal
141
+ if dtype is tuple or dtype is list or isinstance(args, (tuple, list)):
142
+ return tuple(literal(arg) for arg in args)
143
+ elif dtype is dict or isinstance(args, dict):
144
+ return {key: literal(val) for (key, val) in args.items()}
145
+ # If it's not a dictionary let's try escaping it anyways.
146
+ # Worst case it will throw a Value error
147
+ return conn.escape(args)
118
148
 
119
149
  def mogrify(self, query, args=None):
120
150
  """
121
- Returns the exact string that would be sent to the database by calling the
122
- execute() method.
151
+ Returns the exact string sent to the database by calling the execute() method.
123
152
 
124
- :param query: Query to mogrify.
125
- :type query: str
153
+ This method follows the extension to the DB API 2.0 followed by Psycopg.
126
154
 
127
- :param args: Parameters used with query. (optional)
128
- :type args: tuple, list or dict
155
+ Parameters
156
+ ----------
157
+ query : str
158
+ Query to mogrify.
159
+ args : Sequence[Any] or Dict[str, Any] or Any, optional
160
+ Parameters used with query. (optional)
129
161
 
130
- :return: The query with argument binding applied.
131
- :rtype: str
162
+ Returns
163
+ -------
164
+ str : The query with argument binding applied.
132
165
 
133
- This method follows the extension to the DB API 2.0 followed by Psycopg.
134
166
  """
135
167
  conn = self._get_db()
136
168
 
137
- if args is not None:
169
+ if args:
138
170
  query = query % self._escape_args(args, conn)
139
171
 
140
172
  return query
141
173
 
142
174
  def execute(self, query, args=None):
143
- """Execute a query.
175
+ """
176
+ Execute a query.
144
177
 
145
- :param query: Query to execute.
146
- :type query: str
178
+ If args is a list or tuple, :1, :2, etc. can be used as a
179
+ placeholder in the query. If args is a dict, :name can be used
180
+ as a placeholder in the query.
147
181
 
148
- :param args: Parameters used with query. (optional)
149
- :type args: tuple, list or dict
182
+ Parameters
183
+ ----------
184
+ query : str
185
+ Query to execute.
186
+ args : Sequence[Any] or Dict[str, Any] or Any, optional
187
+ Parameters used with query. (optional)
150
188
 
151
- :return: Number of affected rows.
152
- :rtype: int
189
+ Returns
190
+ -------
191
+ int : Number of affected rows.
153
192
 
154
- If args is a list or tuple, %s can be used as a placeholder in the query.
155
- If args is a dict, %(name)s can be used as a placeholder in the query.
156
193
  """
157
194
  while self.nextset():
158
195
  pass
159
196
 
197
+ log_query(query, args)
198
+
160
199
  query = self.mogrify(query, args)
161
200
 
162
201
  result = self._query(query)
163
202
  self._executed = query
164
203
  return result
165
204
 
166
- def executemany(self, query, args):
167
- """Run several data against one query.
168
-
169
- :param query: Query to execute.
170
- :type query: str
171
-
172
- :param args: Sequence of sequences or mappings. It is used as parameter.
173
- :type args: tuple or list
174
-
175
- :return: Number of rows affected, if any.
176
- :rtype: int or None
205
+ def executemany(self, query, args=None):
206
+ """
207
+ Run several data against one query.
177
208
 
178
209
  This method improves performance on multiple-row INSERT and
179
210
  REPLACE. Otherwise it is equivalent to looping over args with
180
211
  execute().
212
+
213
+ Parameters
214
+ ----------
215
+ query : str,
216
+ Query to execute.
217
+ args : Sequnce[Any], optional
218
+ Sequence of sequences or mappings. It is used as parameter.
219
+
220
+ Returns
221
+ -------
222
+ int : Number of rows affected, if any.
223
+
181
224
  """
182
- if not args:
225
+ if args is None or len(args) == 0:
183
226
  return
184
227
 
185
228
  m = RE_INSERT_VALUES.match(query)
@@ -210,7 +253,11 @@ class Cursor:
210
253
  if isinstance(postfix, str):
211
254
  postfix = postfix.encode(encoding)
212
255
  sql = bytearray(prefix)
213
- args = iter(args)
256
+ # Detect dataframes
257
+ if hasattr(args, 'itertuples'):
258
+ args = args.itertuples(index=False)
259
+ else:
260
+ args = iter(args)
214
261
  v = values % escape(next(args), conn)
215
262
  if isinstance(v, str):
216
263
  v = v.encode(encoding, 'surrogateescape')
@@ -218,7 +265,7 @@ class Cursor:
218
265
  rows = 0
219
266
  for arg in args:
220
267
  v = values % escape(arg, conn)
221
- if isinstance(v, str):
268
+ if type(v) is str or isinstance(v, str):
222
269
  v = v.encode(encoding, 'surrogateescape')
223
270
  if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length:
224
271
  rows += self.execute(sql + postfix)
@@ -231,15 +278,8 @@ class Cursor:
231
278
  return rows
232
279
 
233
280
  def callproc(self, procname, args=()):
234
- """Execute stored procedure procname with args.
235
-
236
- :param procname: Name of procedure to execute on server.
237
- :type procname: str
238
-
239
- :param args: Sequence of parameters to use with procedure.
240
- :type args: tuple or list
241
-
242
- Returns the original args.
281
+ """
282
+ Execute stored procedure procname with args.
243
283
 
244
284
  Compatibility warning: PEP-249 specifies that any modified
245
285
  parameters must be returned. This is currently impossible
@@ -259,6 +299,18 @@ class Cursor:
259
299
  behavior with respect to the DB-API. Be sure to use nextset()
260
300
  to advance through all result sets; otherwise you may get
261
301
  disconnected.
302
+
303
+ Parameters
304
+ ----------
305
+ procname : str
306
+ Name of procedure to execute on server.
307
+ args : Sequence[Any], optional
308
+ Sequence of parameters to use with procedure.
309
+
310
+ Returns
311
+ -------
312
+ Sequence[Any] : The original args.
313
+
262
314
  """
263
315
  conn = self._get_db()
264
316
  if args:
@@ -271,7 +323,7 @@ class Cursor:
271
323
  )
272
324
  self.nextset()
273
325
 
274
- q = 'CALL %s(%s)' % (
326
+ q = 'CALL {}({})'.format(
275
327
  procname,
276
328
  ','.join(['@_%s_%d' % (procname, i) for i in range(len(args))]),
277
329
  )
@@ -286,20 +338,21 @@ class Cursor:
286
338
 
287
339
  def _unchecked_fetchone(self):
288
340
  """Fetch the next row."""
289
- if self._rows is None or self.rownumber >= len(self._rows):
341
+ if self._rows is None or self._rownumber >= len(self._rows):
290
342
  return None
291
- result = self._rows[self.rownumber]
292
- self.rownumber += 1
343
+ result = self._rows[self._rownumber]
344
+ self._rownumber += 1
293
345
  return result
294
346
 
295
347
  def fetchmany(self, size=None):
296
348
  """Fetch several rows."""
297
349
  self._check_executed()
298
350
  if self._rows is None:
351
+ self.warning_count = self._result.warning_count
299
352
  return ()
300
- end = self.rownumber + (size or self.arraysize)
301
- result = self._rows[self.rownumber: end]
302
- self.rownumber = min(end, len(self._rows))
353
+ end = self._rownumber + (size or self.arraysize)
354
+ result = self._rows[self._rownumber: end]
355
+ self._rownumber = min(end, len(self._rows))
303
356
  return result
304
357
 
305
358
  def fetchall(self):
@@ -307,17 +360,17 @@ class Cursor:
307
360
  self._check_executed()
308
361
  if self._rows is None:
309
362
  return ()
310
- if self.rownumber:
311
- result = self._rows[self.rownumber:]
363
+ if self._rownumber:
364
+ result = self._rows[self._rownumber:]
312
365
  else:
313
366
  result = self._rows
314
- self.rownumber = len(self._rows)
367
+ self._rownumber = len(self._rows)
315
368
  return result
316
369
 
317
370
  def scroll(self, value, mode='relative'):
318
371
  self._check_executed()
319
372
  if mode == 'relative':
320
- r = self.rownumber + value
373
+ r = self._rownumber + value
321
374
  elif mode == 'absolute':
322
375
  r = value
323
376
  else:
@@ -325,7 +378,7 @@ class Cursor:
325
378
 
326
379
  if not (0 <= r < len(self._rows)):
327
380
  raise IndexError('out of range')
328
- self.rownumber = r
381
+ self._rownumber = r
329
382
 
330
383
  def _query(self, q):
331
384
  conn = self._get_db()
@@ -335,11 +388,12 @@ class Cursor:
335
388
  return self.rowcount
336
389
 
337
390
  def _clear_result(self):
338
- self.rownumber = 0
391
+ self._rownumber = 0
339
392
  self._result = None
340
393
 
341
394
  self.rowcount = 0
342
- self.description = None
395
+ self.warning_count = 0
396
+ self._description = None
343
397
  self.lastrowid = None
344
398
  self._rows = None
345
399
 
@@ -349,15 +403,19 @@ class Cursor:
349
403
  self._result = result = conn._result
350
404
 
351
405
  self.rowcount = result.affected_rows
352
- self.description = result.description
406
+ self.warning_count = result.warning_count
407
+ # Affected rows is set to max int64 for compatibility with MySQLdb, but
408
+ # the DB-API requires this value to be -1. This happens in unbuffered mode.
409
+ if self.rowcount == 18446744073709551615:
410
+ self.rowcount = -1
411
+ self._description = result.description
353
412
  self.lastrowid = result.insert_id
354
413
  self._rows = result.rows
355
414
 
356
415
  def __iter__(self):
357
416
  self._check_executed()
358
- _unchecked_fetchone = self._unchecked_fetchone
359
417
 
360
- def fetchall_unbuffered_gen():
418
+ def fetchall_unbuffered_gen(_unchecked_fetchone=self._unchecked_fetchone):
361
419
  while True:
362
420
  out = _unchecked_fetchone()
363
421
  if out is not None:
@@ -378,6 +436,10 @@ class Cursor:
378
436
  NotSupportedError = err.NotSupportedError
379
437
 
380
438
 
439
+ class CursorSV(Cursor):
440
+ """Cursor class for C extension."""
441
+
442
+
381
443
  class DictCursorMixin:
382
444
  # You can override this to use OrderedDict or other dict-like types.
383
445
  dict_type = dict
@@ -385,7 +447,7 @@ class DictCursorMixin:
385
447
  def _do_get_result(self):
386
448
  super(DictCursorMixin, self)._do_get_result()
387
449
  fields = []
388
- if self.description:
450
+ if self._description:
389
451
  for f in self._result.fields:
390
452
  name = f.name
391
453
  if name in fields:
@@ -403,7 +465,42 @@ class DictCursorMixin:
403
465
 
404
466
 
405
467
  class DictCursor(DictCursorMixin, Cursor):
406
- """A cursor which returns results as a dictionary"""
468
+ """A cursor which returns results as a dictionary."""
469
+
470
+
471
+ class DictCursorSV(Cursor):
472
+ """A cursor which returns results as a dictionary for C extension."""
473
+
474
+
475
+ class NamedtupleCursorMixin:
476
+
477
+ def _do_get_result(self):
478
+ super(NamedtupleCursorMixin, self)._do_get_result()
479
+ fields = []
480
+ if self._description:
481
+ for f in self._result.fields:
482
+ name = f.name
483
+ if name in fields:
484
+ name = f.table_name + '.' + name
485
+ fields.append(name)
486
+ self._fields = fields
487
+ self._namedtuple = namedtuple('Row', self._fields, rename=True)
488
+
489
+ if fields and self._rows:
490
+ self._rows = [self._conv_row(r) for r in self._rows]
491
+
492
+ def _conv_row(self, row):
493
+ if row is None:
494
+ return None
495
+ return self._namedtuple(*row)
496
+
497
+
498
+ class NamedtupleCursor(NamedtupleCursorMixin, Cursor):
499
+ """A cursor which returns results in a named tuple."""
500
+
501
+
502
+ class NamedtupleCursorSV(Cursor):
503
+ """A cursor which returns results as a named tuple for C extension."""
407
504
 
408
505
 
409
506
  class SSCursor(Cursor):
@@ -420,13 +517,14 @@ class SSCursor(Cursor):
420
517
  returning the total number of rows, so the only way to tell how many rows
421
518
  there are is to iterate over every row returned. Also, it currently isn't
422
519
  possible to scroll backwards, as only the current row is held in memory.
520
+
423
521
  """
424
522
 
425
523
  def _conv_row(self, row):
426
524
  return row
427
525
 
428
526
  def close(self):
429
- conn = self.connection
527
+ conn = self._connection
430
528
  if conn is None:
431
529
  return
432
530
 
@@ -437,7 +535,7 @@ class SSCursor(Cursor):
437
535
  while self.nextset():
438
536
  pass
439
537
  finally:
440
- self.connection = None
538
+ self._connection = None
441
539
 
442
540
  __del__ = close
443
541
 
@@ -464,28 +562,34 @@ class SSCursor(Cursor):
464
562
  """Fetch next row."""
465
563
  row = self.read_next()
466
564
  if row is None:
565
+ self.warning_count = self._result.warning_count
467
566
  return None
468
- self.rownumber += 1
567
+ self._rownumber += 1
469
568
  return row
470
569
 
471
570
  def fetchall(self):
472
571
  """
473
- Fetch all, as per MySQLdb. Pretty useless for large queries, as
474
- it is buffered. See fetchall_unbuffered(), if you want an unbuffered
572
+ Fetch all, as per MySQLdb.
573
+
574
+ Pretty useless for large queries, as it is buffered.
575
+ See fetchall_unbuffered(), if you want an unbuffered
475
576
  generator version of this method.
577
+
476
578
  """
477
579
  return list(self.fetchall_unbuffered())
478
580
 
479
581
  def fetchall_unbuffered(self):
480
582
  """
481
- Fetch all, implemented as a generator, which isn't to standard,
482
- however, it doesn't make sense to return everything in a list, as that
483
- would use ridiculous memory for large result sets.
583
+ Fetch all, implemented as a generator.
584
+
585
+ This is not a standard DB-API operation, however, it doesn't make
586
+ sense to return everything in a list, as that would use ridiculous
587
+ memory for large result sets.
588
+
484
589
  """
485
590
  self._check_executed()
486
- _unchecked_fetchone = self._unchecked_fetchone
487
591
 
488
- def fetchall_unbuffered_gen():
592
+ def fetchall_unbuffered_gen(_unchecked_fetchone=self._unchecked_fetchone):
489
593
  while True:
490
594
  out = _unchecked_fetchone()
491
595
  if out is not None:
@@ -507,9 +611,10 @@ class SSCursor(Cursor):
507
611
  for i in range(size):
508
612
  row = self.read_next()
509
613
  if row is None:
614
+ self.warning_count = self._result.warning_count
510
615
  break
511
616
  rows.append(row)
512
- self.rownumber += 1
617
+ self._rownumber += 1
513
618
  return rows
514
619
 
515
620
  def scroll(self, value, mode='relative'):
@@ -523,17 +628,17 @@ class SSCursor(Cursor):
523
628
 
524
629
  for _ in range(value):
525
630
  self.read_next()
526
- self.rownumber += value
631
+ self._rownumber += value
527
632
  elif mode == 'absolute':
528
- if value < self.rownumber:
633
+ if value < self._rownumber:
529
634
  raise err.NotSupportedError(
530
635
  'Backwards scrolling not supported by this cursor',
531
636
  )
532
637
 
533
- end = value - self.rownumber
638
+ end = value - self._rownumber
534
639
  for _ in range(end):
535
640
  self.read_next()
536
- self.rownumber = value
641
+ self._rownumber = value
537
642
  else:
538
643
  raise err.ProgrammingError('unknown scroll mode %s' % mode)
539
644
 
@@ -546,7 +651,7 @@ class SSCursorSV(SSCursor):
546
651
  row = self._result._read_rowdata_packet_unbuffered(1)
547
652
  if row is None:
548
653
  return None
549
- self.rownumber += 1
654
+ self._rownumber += 1
550
655
  return row
551
656
 
552
657
  def fetchone(self):
@@ -562,7 +667,10 @@ class SSCursorSV(SSCursor):
562
667
  out = self._result._read_rowdata_packet_unbuffered(size)
563
668
  if out is None:
564
669
  return []
565
- self.rownumber += len(out)
670
+ if size == 1:
671
+ self._rownumber += 1
672
+ return [out]
673
+ self._rownumber += len(out)
566
674
  return out
567
675
 
568
676
  def scroll(self, value, mode='relative'):
@@ -575,23 +683,31 @@ class SSCursorSV(SSCursor):
575
683
  )
576
684
 
577
685
  self._result._read_rowdata_packet_unbuffered(value)
578
- self.rownumber += value
686
+ self._rownumber += value
579
687
  elif mode == 'absolute':
580
- if value < self.rownumber:
688
+ if value < self._rownumber:
581
689
  raise err.NotSupportedError(
582
690
  'Backwards scrolling not supported by this cursor',
583
691
  )
584
692
 
585
- end = value - self.rownumber
693
+ end = value - self._rownumber
586
694
  self._result._read_rowdata_packet_unbuffered(end)
587
- self.rownumber = value
695
+ self._rownumber = value
588
696
  else:
589
697
  raise err.ProgrammingError('unknown scroll mode %s' % mode)
590
698
 
591
699
 
700
+ class SSDictCursor(DictCursorMixin, SSCursor):
701
+ """An unbuffered cursor, which returns results as a dictionary"""
702
+
703
+
592
704
  class SSDictCursorSV(SSCursorSV):
593
- """An unbuffered cursor for use with PyMySQLsv, which returns results as a dictionary"""
705
+ """An unbuffered cursor for the C extension, which returns a dictionary"""
594
706
 
595
707
 
596
- class SSDictCursor(DictCursorMixin, SSCursor):
597
- """An unbuffered cursor, which returns results as a dictionary"""
708
+ class SSNamedtupleCursor(NamedtupleCursorMixin, SSCursor):
709
+ """An unbuffered cursor, which returns results as a named tuple"""
710
+
711
+
712
+ class SSNamedtupleCursorSV(SSCursorSV):
713
+ """An unbuffered cursor for the C extension, which returns results as a named tuple"""