Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pymysql/cursors.py: 19%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

273 statements  

1import re 

2import warnings 

3 

4from . import err 

5 

6#: Regular expression for :meth:`Cursor.executemany`. 

7#: executemany only supports simple bulk insert. 

8#: You can use it to load large dataset. 

9RE_INSERT_VALUES = re.compile( 

10 r"\s*((?:INSERT|REPLACE)\b.+\bVALUES?\s*)" 

11 + r"(\(\s*(?:%s|%\([^)]+\)s)\s*(?:,\s*(?:%s|%\([^)]+\)s)\s*)*\))" 

12 + r'(\s*(?:AS\s+(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*' 

13 + r'(?:\(\s*(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*' 

14 + r'(?:,\s*(?:`[^`]+`|"[^"]+"|[0-9A-Za-z_$]+)\s*)*\))?\s*)?' 

15 + r"(?:ON DUPLICATE.*)?);?\s*\Z", 

16 re.IGNORECASE | re.DOTALL, 

17) 

18 

19 

20def _backquote_escape(s): 

21 return s.replace("`", "``") 

22 

23 

24class Cursor: 

25 """ 

26 This is the object used to interact with the database. 

27 

28 Do not create an instance of a Cursor yourself. Call 

29 connections.Connection.cursor(). 

30 

31 See `Cursor <https://www.python.org/dev/peps/pep-0249/#cursor-objects>`_ in 

32 the specification. 

33 """ 

34 

35 #: Max statement size which :meth:`executemany` generates. 

36 #: 

37 #: Max size of allowed statement is max_allowed_packet - packet_header_size. 

38 #: Default value of max_allowed_packet is 1048576. 

39 max_stmt_length = 1024000 

40 

41 def __init__(self, connection): 

42 self.connection = connection 

43 self.warning_count = 0 

44 self.description = None 

45 self.rownumber = 0 

46 self.rowcount = -1 

47 self.arraysize = 1 

48 self._executed = None 

49 self._result = None 

50 self._rows = None 

51 

52 def close(self): 

53 """ 

54 Closing a cursor just exhausts all remaining data. 

55 """ 

56 conn = self.connection 

57 if conn is None: 

58 return 

59 try: 

60 while self.nextset(): 

61 pass 

62 finally: 

63 self.connection = None 

64 

65 def __enter__(self): 

66 return self 

67 

68 def __exit__(self, *exc_info): 

69 del exc_info 

70 self.close() 

71 

72 def _get_db(self): 

73 if not self.connection: 

74 raise err.ProgrammingError("Cursor closed") 

75 return self.connection 

76 

77 def _check_executed(self): 

78 if not self._executed: 

79 raise err.ProgrammingError("execute() first") 

80 

81 def _conv_row(self, row): 

82 return row 

83 

84 def setinputsizes(self, *args): 

85 """Does nothing, required by DB API.""" 

86 

87 def setoutputsizes(self, *args): 

88 """Does nothing, required by DB API.""" 

89 

90 def _nextset(self, unbuffered=False): 

91 """Get the next query set.""" 

92 conn = self._get_db() 

93 current_result = self._result 

94 if current_result is None or current_result is not conn._result: 

95 return None 

96 if not current_result.has_next: 

97 return None 

98 self._result = None 

99 self._clear_result() 

100 conn.next_result(unbuffered=unbuffered) 

101 self._do_get_result() 

102 return True 

103 

104 def nextset(self): 

105 return self._nextset(False) 

106 

107 def _mogrify(self, query, args) -> str: 

108 """Return query after binding args.""" 

109 escape = self._get_db().escape 

110 

111 if isinstance(args, dict): 

112 args = {key: escape(item) for key, item in args.items()} 

113 elif isinstance(args, (list, tuple)): 

114 args = tuple(map(escape, args)) 

115 else: 

116 # Escaping a single argument is not part of the DB-API or mysqlclient. 

117 # It will be removed in the next version. 

118 warnings.warn( 

119 "single argument is deprecated and will be removed in the next version.", 

120 DeprecationWarning, 

121 stacklevel=3, 

122 ) 

123 args = escape(args) 

124 try: 

125 return query % args 

126 except TypeError as m: 

127 raise err.ProgrammingError(str(m)) 

128 

129 def mogrify(self, query, args=None) -> str: 

130 """ 

131 Returns the exact string that would be sent to the database by calling the 

132 execute() method. 

133 

134 :param query: Query to mogrify. 

135 :type query: str 

136 

137 :param args: Parameters used with query. (optional) 

138 :type args: tuple, list or dict 

139 

140 :return: The query with argument binding applied. 

141 :rtype: str 

142 

143 This method follows the extension to the DB API 2.0 followed by Psycopg. 

144 """ 

145 if args is None: 

146 return query 

147 

148 return self._mogrify(query, args) 

149 

150 def execute(self, query, args=None): 

151 """Execute a query. 

152 

153 :param query: Query to execute. 

154 :type query: str 

155 

156 :param args: Parameters used with query. (optional) 

157 :type args: tuple, list or dict 

158 

159 :return: Number of affected rows. 

160 :rtype: int 

161 

162 If args is a list or tuple, %s can be used as a placeholder in the query. 

163 If args is a dict, %(name)s can be used as a placeholder in the query. 

164 """ 

165 while self.nextset(): 

166 pass 

167 

168 if args is not None: 

169 query = self._mogrify(query, args) 

170 result = self._query(query) 

171 self._executed = query 

172 return result 

173 

174 def executemany(self, query, args): 

175 """Run several data against one query. 

176 

177 :param query: Query to execute. 

178 :type query: str 

179 

180 :param args: Sequence of sequences or mappings. It is used as parameter. 

181 :type args: tuple or list 

182 

183 :return: Number of rows affected, if any. 

184 :rtype: int or None 

185 

186 This method improves performance on multiple-row INSERT and 

187 REPLACE. Otherwise it is equivalent to looping over args with 

188 execute(). 

189 """ 

190 if not args: 

191 return 

192 

193 m = RE_INSERT_VALUES.match(query) 

194 if m: 

195 q_prefix = m.group(1) % () 

196 q_values = m.group(2).rstrip() 

197 q_postfix = (m.group(3) or "") % () 

198 assert q_values[0] == "(" and q_values[-1] == ")" 

199 return self._do_execute_many( 

200 q_prefix, 

201 q_values, 

202 q_postfix, 

203 args, 

204 self.max_stmt_length, 

205 self._get_db().encoding, 

206 ) 

207 

208 self.rowcount = sum(self.execute(query, arg) for arg in args) 

209 return self.rowcount 

210 

211 def _do_execute_many( 

212 self, prefix, values, postfix, args, max_stmt_length, encoding 

213 ): 

214 encoding = self._get_db().encoding 

215 if isinstance(prefix, str): 

216 prefix = prefix.encode(encoding) 

217 if isinstance(postfix, str): 

218 postfix = postfix.encode(encoding) 

219 sql = bytearray(prefix) 

220 args = iter(args) 

221 sql += self._mogrify(values, next(args)).encode(encoding) 

222 rows = 0 

223 for arg in args: 

224 v = self._mogrify(values, arg).encode(encoding) 

225 if len(sql) + len(v) + len(postfix) + 1 > max_stmt_length: 

226 rows += self.execute(sql + postfix) 

227 sql = bytearray(prefix) 

228 else: 

229 sql += b"," 

230 sql += v 

231 rows += self.execute(sql + postfix) 

232 self.rowcount = rows 

233 return rows 

234 

235 def callproc(self, procname, args=()): 

236 """Execute stored procedure procname with args. 

237 

238 :param procname: Name of procedure to execute on server. 

239 :type procname: str 

240 

241 :param args: Sequence of parameters to use with procedure. 

242 :type args: tuple or list 

243 

244 Returns the original args. 

245 

246 Compatibility warning: PEP-249 specifies that any modified 

247 parameters must be returned. This is currently impossible 

248 as they are only available by storing them in a server 

249 variable and then retrieved by a query. Since stored 

250 procedures return zero or more result sets, there is no 

251 reliable way to get at OUT or INOUT parameters via callproc. 

252 The server variables are named @_procname_n, where procname 

253 is the parameter above and n is the position of the parameter 

254 (from zero). Once all result sets generated by the procedure 

255 have been fetched, you can issue a SELECT @_procname_0, ... 

256 query using .execute() to get any OUT or INOUT values. 

257 

258 Compatibility warning: The act of calling a stored procedure 

259 itself creates an empty result set. This appears after any 

260 result sets generated by the procedure. This is non-standard 

261 behavior with respect to the DB-API. Be sure to use nextset() 

262 to advance through all result sets; otherwise you may get 

263 disconnected. 

264 """ 

265 procname_escaped = _backquote_escape(procname) 

266 conn = self._get_db() 

267 

268 if args: 

269 fmt = f"@`_{procname_escaped}_%d`=%s" 

270 self._query( 

271 "SET %s" 

272 % ",".join( 

273 fmt % (index, conn.escape(arg)) for index, arg in enumerate(args) 

274 ) 

275 ) 

276 self.nextset() 

277 

278 q = "CALL `{}`({})".format( 

279 procname_escaped, 

280 ",".join([f"@`_{procname_escaped}_{i}`" for i in range(len(args))]), 

281 ) 

282 self._query(q) 

283 self._executed = q 

284 return args 

285 

286 def fetchone(self): 

287 """Fetch the next row.""" 

288 self._check_executed() 

289 if self._rows is None or self.rownumber >= len(self._rows): 

290 return None 

291 result = self._rows[self.rownumber] 

292 self.rownumber += 1 

293 return result 

294 

295 def fetchmany(self, size=None): 

296 """Fetch several rows.""" 

297 self._check_executed() 

298 if self._rows is None: 

299 # Django expects () for EOF. 

300 # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 

301 return () 

302 end = self.rownumber + (size or self.arraysize) 

303 result = self._rows[self.rownumber : end] 

304 self.rownumber = min(end, len(self._rows)) 

305 return result 

306 

307 def fetchall(self): 

308 """Fetch all the rows.""" 

309 self._check_executed() 

310 if self._rows is None: 

311 return [] 

312 if self.rownumber: 

313 result = self._rows[self.rownumber :] 

314 else: 

315 result = self._rows 

316 self.rownumber = len(self._rows) 

317 return result 

318 

319 def scroll(self, value, mode="relative"): 

320 self._check_executed() 

321 if mode == "relative": 

322 r = self.rownumber + value 

323 elif mode == "absolute": 

324 r = value 

325 else: 

326 raise err.ProgrammingError("unknown scroll mode %s" % mode) 

327 

328 if not (0 <= r < len(self._rows)): 

329 raise IndexError("out of range") 

330 self.rownumber = r 

331 

332 def _query(self, q): 

333 conn = self._get_db() 

334 self._clear_result() 

335 conn.query(q) 

336 self._do_get_result() 

337 return self.rowcount 

338 

339 def _clear_result(self): 

340 self.rownumber = 0 

341 self._result = None 

342 

343 self.rowcount = 0 

344 self.warning_count = 0 

345 self.description = None 

346 self.lastrowid = None 

347 self._rows = None 

348 

349 def _do_get_result(self): 

350 conn = self._get_db() 

351 

352 self._result = result = conn._result 

353 

354 self.rowcount = result.affected_rows 

355 self.warning_count = result.warning_count 

356 self.description = result.description 

357 self.lastrowid = result.insert_id 

358 self._rows = result.rows 

359 

360 def __iter__(self): 

361 return self 

362 

363 def __next__(self): 

364 row = self.fetchone() 

365 if row is None: 

366 raise StopIteration 

367 return row 

368 

369 

370class DictCursorMixin: 

371 # You can override this to use OrderedDict or other dict-like types. 

372 dict_type = dict 

373 

374 def _do_get_result(self): 

375 super()._do_get_result() 

376 fields = [] 

377 if self.description: 

378 for f in self._result.fields: 

379 name = f.name 

380 if name in fields: 

381 name = f.table_name + "." + name 

382 fields.append(name) 

383 self._fields = fields 

384 

385 if fields and self._rows: 

386 self._rows = [self._conv_row(r) for r in self._rows] 

387 

388 def _conv_row(self, row): 

389 if row is None: 

390 return None 

391 return self.dict_type(zip(self._fields, row)) 

392 

393 

394class DictCursor(DictCursorMixin, Cursor): 

395 """A cursor which returns results as a dictionary""" 

396 

397 

398class SSCursor(Cursor): 

399 """ 

400 Unbuffered Cursor, mainly useful for queries that return a lot of data, 

401 or for connections to remote servers over a slow network. 

402 

403 Instead of copying every row of data into a buffer, this will fetch 

404 rows as needed. The upside of this is the client uses much less memory, 

405 and rows are returned much faster when traveling over a slow network 

406 or if the result set is very big. 

407 

408 There are limitations, though. The MySQL protocol doesn't support 

409 returning the total number of rows, so the only way to tell how many rows 

410 there are is to iterate over every row returned. Also, it currently isn't 

411 possible to scroll backwards, as only the current row is held in memory. 

412 """ 

413 

414 def _conv_row(self, row): 

415 return row 

416 

417 def close(self): 

418 conn = self.connection 

419 if conn is None: 

420 return 

421 

422 if self._result is not None and self._result is conn._result: 

423 self._result._finish_unbuffered_query() 

424 

425 try: 

426 while self.nextset(): 

427 pass 

428 finally: 

429 self.connection = None 

430 

431 __del__ = close 

432 

433 def _query(self, q): 

434 conn = self._get_db() 

435 self._clear_result() 

436 conn.query(q, unbuffered=True) 

437 self._do_get_result() 

438 return self.rowcount 

439 

440 def nextset(self): 

441 return self._nextset(unbuffered=True) 

442 

443 def read_next(self): 

444 """Read next row.""" 

445 return self._conv_row(self._result._read_rowdata_packet_unbuffered()) 

446 

447 def fetchone(self): 

448 """Fetch next row.""" 

449 self._check_executed() 

450 row = self.read_next() 

451 if row is None: 

452 self.warning_count = self._result.warning_count 

453 return None 

454 self.rownumber += 1 

455 return row 

456 

457 def fetchall(self): 

458 """ 

459 Fetch all, as per MySQLdb. Pretty useless for large queries, as 

460 it is buffered. See fetchall_unbuffered(), if you want an unbuffered 

461 generator version of this method. 

462 """ 

463 return list(self.fetchall_unbuffered()) 

464 

465 def fetchall_unbuffered(self): 

466 """ 

467 Fetch all, implemented as a generator, which isn't to standard, 

468 however, it doesn't make sense to return everything in a list, as that 

469 would use ridiculous memory for large result sets. 

470 """ 

471 return iter(self.fetchone, None) 

472 

473 def fetchmany(self, size=None): 

474 """Fetch many.""" 

475 self._check_executed() 

476 if size is None: 

477 size = self.arraysize 

478 

479 rows = [] 

480 for i in range(size): 

481 row = self.read_next() 

482 if row is None: 

483 self.warning_count = self._result.warning_count 

484 break 

485 rows.append(row) 

486 self.rownumber += 1 

487 if not rows: 

488 # Django expects () for EOF. 

489 # https://github.com/django/django/blob/0c1518ee429b01c145cf5b34eab01b0b92f8c246/django/db/backends/mysql/features.py#L8 

490 return () 

491 return rows 

492 

493 def scroll(self, value, mode="relative"): 

494 self._check_executed() 

495 

496 if mode == "relative": 

497 if value < 0: 

498 raise err.NotSupportedError( 

499 "Backwards scrolling not supported by this cursor" 

500 ) 

501 

502 for _ in range(value): 

503 self.read_next() 

504 self.rownumber += value 

505 elif mode == "absolute": 

506 if value < self.rownumber: 

507 raise err.NotSupportedError( 

508 "Backwards scrolling not supported by this cursor" 

509 ) 

510 

511 end = value - self.rownumber 

512 for _ in range(end): 

513 self.read_next() 

514 self.rownumber = value 

515 else: 

516 raise err.ProgrammingError("unknown scroll mode %s" % mode) 

517 

518 

519class SSDictCursor(DictCursorMixin, SSCursor): 

520 """An unbuffered cursor, which returns results as a dictionary"""