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
@@ -0,0 +1,713 @@
1
+ # type: ignore
2
+ import re
3
+ from collections import namedtuple
4
+
5
+ from . import err
6
+ from ..connection import Cursor as BaseCursor
7
+ from ..utils.debug import log_query
8
+
9
+
10
+ #: Regular expression for :meth:`Cursor.executemany`.
11
+ #: executemany only supports simple bulk insert.
12
+ #: You can use it to load large dataset.
13
+ RE_INSERT_VALUES = re.compile(
14
+ r'\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)'
15
+ + r'(\(\s*(?:%s|%\(.+\)s)\s*(?:,\s*(?:%s|%\(.+\)s)\s*)*\))'
16
+ + r'(\s*(?:ON DUPLICATE.*)?);?\s*\Z',
17
+ re.IGNORECASE | re.DOTALL,
18
+ )
19
+
20
+
21
+ class Cursor(BaseCursor):
22
+ """
23
+ This is the object used to interact with the database.
24
+
25
+ Do not create an instance of a Cursor yourself. Call
26
+ connection.Connection.cursor().
27
+
28
+ See `Cursor <https://www.python.org/dev/peps/pep-0249/#cursor-objects>`_ in
29
+ the specification.
30
+
31
+ Parameters
32
+ ----------
33
+ connection : Connection
34
+ The connection the cursor is associated with.
35
+
36
+ """
37
+
38
+ #: Max statement size which :meth:`executemany` generates.
39
+ #:
40
+ #: Max size of allowed statement is max_allowed_packet - packet_header_size.
41
+ #: Default value of max_allowed_packet is 1048576.
42
+ max_stmt_length = 1024000
43
+
44
+ def __init__(self, connection):
45
+ self._connection = connection
46
+ self.warning_count = 0
47
+ self._description = None
48
+ self._rownumber = 0
49
+ self.rowcount = -1
50
+ self.arraysize = 1
51
+ self._executed = None
52
+ self._result = None
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
72
+
73
+ def close(self):
74
+ """Closing a cursor just exhausts all remaining data."""
75
+ conn = self._connection
76
+ if conn is None:
77
+ return
78
+ try:
79
+ while self.nextset():
80
+ pass
81
+ finally:
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
93
+
94
+ def __enter__(self):
95
+ return self
96
+
97
+ def __exit__(self, *exc_info):
98
+ del exc_info
99
+ self.close()
100
+
101
+ def _get_db(self):
102
+ if not self._connection:
103
+ raise err.ProgrammingError('Cursor closed')
104
+ return self._connection
105
+
106
+ def _check_executed(self):
107
+ if not self._executed:
108
+ raise err.ProgrammingError('execute() first')
109
+
110
+ def _conv_row(self, row):
111
+ return row
112
+
113
+ def setinputsizes(self, *args):
114
+ """Does nothing, required by DB API."""
115
+
116
+ def setoutputsizes(self, *args):
117
+ """Does nothing, required by DB API."""
118
+
119
+ setoutputsize = setoutputsizes
120
+
121
+ def _nextset(self, unbuffered=False):
122
+ """Get the next query set."""
123
+ conn = self._get_db()
124
+ current_result = self._result
125
+ if current_result is None or current_result is not conn._result:
126
+ return None
127
+ if not current_result.has_next:
128
+ return None
129
+ self._result = None
130
+ self._clear_result()
131
+ conn.next_result(unbuffered=unbuffered)
132
+ self._do_get_result()
133
+ return True
134
+
135
+ def nextset(self):
136
+ return self._nextset(False)
137
+
138
+ def _escape_args(self, args, conn):
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)
148
+
149
+ def mogrify(self, query, args=None):
150
+ """
151
+ Returns the exact string sent to the database by calling the execute() method.
152
+
153
+ This method follows the extension to the DB API 2.0 followed by Psycopg.
154
+
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)
161
+
162
+ Returns
163
+ -------
164
+ str : The query with argument binding applied.
165
+
166
+ """
167
+ conn = self._get_db()
168
+
169
+ if args:
170
+ query = query % self._escape_args(args, conn)
171
+
172
+ return query
173
+
174
+ def execute(self, query, args=None):
175
+ """
176
+ Execute a query.
177
+
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.
181
+
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)
188
+
189
+ Returns
190
+ -------
191
+ int : Number of affected rows.
192
+
193
+ """
194
+ while self.nextset():
195
+ pass
196
+
197
+ log_query(query, args)
198
+
199
+ query = self.mogrify(query, args)
200
+
201
+ result = self._query(query)
202
+ self._executed = query
203
+ return result
204
+
205
+ def executemany(self, query, args=None):
206
+ """
207
+ Run several data against one query.
208
+
209
+ This method improves performance on multiple-row INSERT and
210
+ REPLACE. Otherwise it is equivalent to looping over args with
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
+
224
+ """
225
+ if args is None or len(args) == 0:
226
+ return
227
+
228
+ m = RE_INSERT_VALUES.match(query)
229
+ if m:
230
+ q_prefix = m.group(1) % ()
231
+ q_values = m.group(2).rstrip()
232
+ q_postfix = m.group(3) or ''
233
+ assert q_values[0] == '(' and q_values[-1] == ')'
234
+ return self._do_execute_many(
235
+ q_prefix,
236
+ q_values,
237
+ q_postfix,
238
+ args,
239
+ self.max_stmt_length,
240
+ self._get_db().encoding,
241
+ )
242
+
243
+ self.rowcount = sum(self.execute(query, arg) for arg in args)
244
+ return self.rowcount
245
+
246
+ def _do_execute_many(
247
+ self, prefix, values, postfix, args, max_stmt_length, encoding,
248
+ ):
249
+ conn = self._get_db()
250
+ escape = self._escape_args
251
+ if isinstance(prefix, str):
252
+ prefix = prefix.encode(encoding)
253
+ if isinstance(postfix, str):
254
+ postfix = postfix.encode(encoding)
255
+ sql = bytearray(prefix)
256
+ # Detect dataframes
257
+ if hasattr(args, 'itertuples'):
258
+ args = args.itertuples(index=False)
259
+ else:
260
+ args = iter(args)
261
+ v = values % escape(next(args), conn)
262
+ if isinstance(v, str):
263
+ v = v.encode(encoding, 'surrogateescape')
264
+ sql += v
265
+ rows = 0
266
+ for arg in args:
267
+ v = values % escape(arg, conn)
268
+ if type(v) is str or isinstance(v, str):
269
+ v = v.encode(encoding, 'surrogateescape')
270
+ if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length:
271
+ rows += self.execute(sql + postfix)
272
+ sql = bytearray(prefix)
273
+ else:
274
+ sql += b','
275
+ sql += v
276
+ rows += self.execute(sql + postfix)
277
+ self.rowcount = rows
278
+ return rows
279
+
280
+ def callproc(self, procname, args=()):
281
+ """
282
+ Execute stored procedure procname with args.
283
+
284
+ Compatibility warning: PEP-249 specifies that any modified
285
+ parameters must be returned. This is currently impossible
286
+ as they are only available by storing them in a server
287
+ variable and then retrieved by a query. Since stored
288
+ procedures return zero or more result sets, there is no
289
+ reliable way to get at OUT or INOUT parameters via callproc.
290
+ The server variables are named @_procname_n, where procname
291
+ is the parameter above and n is the position of the parameter
292
+ (from zero). Once all result sets generated by the procedure
293
+ have been fetched, you can issue a SELECT @_procname_0, ...
294
+ query using .execute() to get any OUT or INOUT values.
295
+
296
+ Compatibility warning: The act of calling a stored procedure
297
+ itself creates an empty result set. This appears after any
298
+ result sets generated by the procedure. This is non-standard
299
+ behavior with respect to the DB-API. Be sure to use nextset()
300
+ to advance through all result sets; otherwise you may get
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
+
314
+ """
315
+ conn = self._get_db()
316
+ if args:
317
+ fmt = f'@_{procname}_%d=%s'
318
+ self._query(
319
+ 'SET %s'
320
+ % ','.join(
321
+ fmt % (index, conn.escape(arg)) for index, arg in enumerate(args)
322
+ ),
323
+ )
324
+ self.nextset()
325
+
326
+ q = 'CALL {}({})'.format(
327
+ procname,
328
+ ','.join(['@_%s_%d' % (procname, i) for i in range(len(args))]),
329
+ )
330
+ self._query(q)
331
+ self._executed = q
332
+ return args
333
+
334
+ def fetchone(self):
335
+ """Fetch the next row."""
336
+ self._check_executed()
337
+ return self._unchecked_fetchone()
338
+
339
+ def _unchecked_fetchone(self):
340
+ """Fetch the next row."""
341
+ if self._rows is None or self._rownumber >= len(self._rows):
342
+ return None
343
+ result = self._rows[self._rownumber]
344
+ self._rownumber += 1
345
+ return result
346
+
347
+ def fetchmany(self, size=None):
348
+ """Fetch several rows."""
349
+ self._check_executed()
350
+ if self._rows is None:
351
+ self.warning_count = self._result.warning_count
352
+ return ()
353
+ end = self._rownumber + (size or self.arraysize)
354
+ result = self._rows[self._rownumber: end]
355
+ self._rownumber = min(end, len(self._rows))
356
+ return result
357
+
358
+ def fetchall(self):
359
+ """Fetch all the rows."""
360
+ self._check_executed()
361
+ if self._rows is None:
362
+ return ()
363
+ if self._rownumber:
364
+ result = self._rows[self._rownumber:]
365
+ else:
366
+ result = self._rows
367
+ self._rownumber = len(self._rows)
368
+ return result
369
+
370
+ def scroll(self, value, mode='relative'):
371
+ self._check_executed()
372
+ if mode == 'relative':
373
+ r = self._rownumber + value
374
+ elif mode == 'absolute':
375
+ r = value
376
+ else:
377
+ raise err.ProgrammingError('unknown scroll mode %s' % mode)
378
+
379
+ if not (0 <= r < len(self._rows)):
380
+ raise IndexError('out of range')
381
+ self._rownumber = r
382
+
383
+ def _query(self, q):
384
+ conn = self._get_db()
385
+ self._clear_result()
386
+ conn.query(q)
387
+ self._do_get_result()
388
+ return self.rowcount
389
+
390
+ def _clear_result(self):
391
+ self._rownumber = 0
392
+ self._result = None
393
+
394
+ self.rowcount = 0
395
+ self.warning_count = 0
396
+ self._description = None
397
+ self.lastrowid = None
398
+ self._rows = None
399
+
400
+ def _do_get_result(self):
401
+ conn = self._get_db()
402
+
403
+ self._result = result = conn._result
404
+
405
+ self.rowcount = result.affected_rows
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
412
+ self.lastrowid = result.insert_id
413
+ self._rows = result.rows
414
+
415
+ def __iter__(self):
416
+ self._check_executed()
417
+
418
+ def fetchall_unbuffered_gen(_unchecked_fetchone=self._unchecked_fetchone):
419
+ while True:
420
+ out = _unchecked_fetchone()
421
+ if out is not None:
422
+ yield out
423
+ else:
424
+ break
425
+ return fetchall_unbuffered_gen()
426
+
427
+ Warning = err.Warning
428
+ Error = err.Error
429
+ InterfaceError = err.InterfaceError
430
+ DatabaseError = err.DatabaseError
431
+ DataError = err.DataError
432
+ OperationalError = err.OperationalError
433
+ IntegrityError = err.IntegrityError
434
+ InternalError = err.InternalError
435
+ ProgrammingError = err.ProgrammingError
436
+ NotSupportedError = err.NotSupportedError
437
+
438
+
439
+ class CursorSV(Cursor):
440
+ """Cursor class for C extension."""
441
+
442
+
443
+ class DictCursorMixin:
444
+ # You can override this to use OrderedDict or other dict-like types.
445
+ dict_type = dict
446
+
447
+ def _do_get_result(self):
448
+ super(DictCursorMixin, self)._do_get_result()
449
+ fields = []
450
+ if self._description:
451
+ for f in self._result.fields:
452
+ name = f.name
453
+ if name in fields:
454
+ name = f.table_name + '.' + name
455
+ fields.append(name)
456
+ self._fields = fields
457
+
458
+ if fields and self._rows:
459
+ self._rows = [self._conv_row(r) for r in self._rows]
460
+
461
+ def _conv_row(self, row):
462
+ if row is None:
463
+ return None
464
+ return self.dict_type(zip(self._fields, row))
465
+
466
+
467
+ class DictCursor(DictCursorMixin, Cursor):
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."""
504
+
505
+
506
+ class SSCursor(Cursor):
507
+ """
508
+ Unbuffered Cursor, mainly useful for queries that return a lot of data,
509
+ or for connections to remote servers over a slow network.
510
+
511
+ Instead of copying every row of data into a buffer, this will fetch
512
+ rows as needed. The upside of this is the client uses much less memory,
513
+ and rows are returned much faster when traveling over a slow network
514
+ or if the result set is very big.
515
+
516
+ There are limitations, though. The MySQL protocol doesn't support
517
+ returning the total number of rows, so the only way to tell how many rows
518
+ there are is to iterate over every row returned. Also, it currently isn't
519
+ possible to scroll backwards, as only the current row is held in memory.
520
+
521
+ """
522
+
523
+ def _conv_row(self, row):
524
+ return row
525
+
526
+ def close(self):
527
+ conn = self._connection
528
+ if conn is None:
529
+ return
530
+
531
+ if self._result is not None and self._result is conn._result:
532
+ self._result._finish_unbuffered_query()
533
+
534
+ try:
535
+ while self.nextset():
536
+ pass
537
+ finally:
538
+ self._connection = None
539
+
540
+ __del__ = close
541
+
542
+ def _query(self, q):
543
+ conn = self._get_db()
544
+ self._clear_result()
545
+ conn.query(q, unbuffered=True)
546
+ self._do_get_result()
547
+ return self.rowcount
548
+
549
+ def nextset(self):
550
+ return self._nextset(unbuffered=True)
551
+
552
+ def read_next(self):
553
+ """Read next row."""
554
+ return self._conv_row(self._result._read_rowdata_packet_unbuffered())
555
+
556
+ def fetchone(self):
557
+ """Fetch next row."""
558
+ self._check_executed()
559
+ return self._unchecked_fetchone()
560
+
561
+ def _unchecked_fetchone(self):
562
+ """Fetch next row."""
563
+ row = self.read_next()
564
+ if row is None:
565
+ self.warning_count = self._result.warning_count
566
+ return None
567
+ self._rownumber += 1
568
+ return row
569
+
570
+ def fetchall(self):
571
+ """
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
576
+ generator version of this method.
577
+
578
+ """
579
+ return list(self.fetchall_unbuffered())
580
+
581
+ def fetchall_unbuffered(self):
582
+ """
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
+
589
+ """
590
+ self._check_executed()
591
+
592
+ def fetchall_unbuffered_gen(_unchecked_fetchone=self._unchecked_fetchone):
593
+ while True:
594
+ out = _unchecked_fetchone()
595
+ if out is not None:
596
+ yield out
597
+ else:
598
+ break
599
+ return fetchall_unbuffered_gen()
600
+
601
+ def __iter__(self):
602
+ return self.fetchall_unbuffered()
603
+
604
+ def fetchmany(self, size=None):
605
+ """Fetch many."""
606
+ self._check_executed()
607
+ if size is None:
608
+ size = self.arraysize
609
+
610
+ rows = []
611
+ for i in range(size):
612
+ row = self.read_next()
613
+ if row is None:
614
+ self.warning_count = self._result.warning_count
615
+ break
616
+ rows.append(row)
617
+ self._rownumber += 1
618
+ return rows
619
+
620
+ def scroll(self, value, mode='relative'):
621
+ self._check_executed()
622
+
623
+ if mode == 'relative':
624
+ if value < 0:
625
+ raise err.NotSupportedError(
626
+ 'Backwards scrolling not supported by this cursor',
627
+ )
628
+
629
+ for _ in range(value):
630
+ self.read_next()
631
+ self._rownumber += value
632
+ elif mode == 'absolute':
633
+ if value < self._rownumber:
634
+ raise err.NotSupportedError(
635
+ 'Backwards scrolling not supported by this cursor',
636
+ )
637
+
638
+ end = value - self._rownumber
639
+ for _ in range(end):
640
+ self.read_next()
641
+ self._rownumber = value
642
+ else:
643
+ raise err.ProgrammingError('unknown scroll mode %s' % mode)
644
+
645
+
646
+ class SSCursorSV(SSCursor):
647
+ """An unbuffered cursor for use with PyMySQLsv."""
648
+
649
+ def _unchecked_fetchone(self):
650
+ """Fetch next row."""
651
+ row = self._result._read_rowdata_packet_unbuffered(1)
652
+ if row is None:
653
+ return None
654
+ self._rownumber += 1
655
+ return row
656
+
657
+ def fetchone(self):
658
+ """Fetch next row."""
659
+ self._check_executed()
660
+ return self._unchecked_fetchone()
661
+
662
+ def fetchmany(self, size=None):
663
+ """Fetch many."""
664
+ self._check_executed()
665
+ if size is None:
666
+ size = self.arraysize
667
+ out = self._result._read_rowdata_packet_unbuffered(size)
668
+ if out is None:
669
+ return []
670
+ if size == 1:
671
+ self._rownumber += 1
672
+ return [out]
673
+ self._rownumber += len(out)
674
+ return out
675
+
676
+ def scroll(self, value, mode='relative'):
677
+ self._check_executed()
678
+
679
+ if mode == 'relative':
680
+ if value < 0:
681
+ raise err.NotSupportedError(
682
+ 'Backwards scrolling not supported by this cursor',
683
+ )
684
+
685
+ self._result._read_rowdata_packet_unbuffered(value)
686
+ self._rownumber += value
687
+ elif mode == 'absolute':
688
+ if value < self._rownumber:
689
+ raise err.NotSupportedError(
690
+ 'Backwards scrolling not supported by this cursor',
691
+ )
692
+
693
+ end = value - self._rownumber
694
+ self._result._read_rowdata_packet_unbuffered(end)
695
+ self._rownumber = value
696
+ else:
697
+ raise err.ProgrammingError('unknown scroll mode %s' % mode)
698
+
699
+
700
+ class SSDictCursor(DictCursorMixin, SSCursor):
701
+ """An unbuffered cursor, which returns results as a dictionary"""
702
+
703
+
704
+ class SSDictCursorSV(SSCursorSV):
705
+ """An unbuffered cursor for the C extension, which returns a dictionary"""
706
+
707
+
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"""