Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/pandas/io/sql.py: 17%

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

938 statements  

1""" 

2Collection of query wrappers / abstractions to both facilitate data 

3retrieval and to reduce dependency on DB-specific API. 

4""" 

5 

6from __future__ import annotations 

7 

8from abc import ( 

9 ABC, 

10 abstractmethod, 

11) 

12from contextlib import ( 

13 ExitStack, 

14 contextmanager, 

15) 

16from datetime import ( 

17 date, 

18 datetime, 

19 time, 

20) 

21from functools import partial 

22import re 

23from typing import ( 

24 TYPE_CHECKING, 

25 Any, 

26 Literal, 

27 Self, 

28 cast, 

29 overload, 

30) 

31import warnings 

32 

33import numpy as np 

34 

35from pandas._config import using_string_dtype 

36 

37from pandas._libs import lib 

38from pandas.compat._optional import ( 

39 VERSIONS, 

40 import_optional_dependency, 

41) 

42from pandas.errors import ( 

43 AbstractMethodError, 

44 DatabaseError, 

45) 

46from pandas.util._decorators import set_module 

47from pandas.util._exceptions import find_stack_level 

48from pandas.util._validators import check_dtype_backend 

49 

50from pandas.core.dtypes.common import ( 

51 is_dict_like, 

52 is_list_like, 

53 is_object_dtype, 

54 is_string_dtype, 

55) 

56from pandas.core.dtypes.dtypes import DatetimeTZDtype 

57from pandas.core.dtypes.missing import isna 

58 

59from pandas import get_option 

60from pandas.core.api import ( 

61 DataFrame, 

62 Series, 

63) 

64from pandas.core.arrays import ArrowExtensionArray 

65from pandas.core.arrays.string_ import StringDtype 

66from pandas.core.base import PandasObject 

67import pandas.core.common as com 

68from pandas.core.common import maybe_make_list 

69from pandas.core.internals.construction import convert_object_array 

70from pandas.core.tools.datetimes import to_datetime 

71 

72from pandas.io._util import arrow_table_to_pandas 

73 

74if TYPE_CHECKING: 

75 from collections.abc import ( 

76 Callable, 

77 Generator, 

78 Iterator, 

79 Mapping, 

80 ) 

81 

82 from sqlalchemy import Table 

83 from sqlalchemy.sql.expression import ( 

84 Delete, 

85 Select, 

86 TextClause, 

87 ) 

88 

89 from pandas._typing import ( 

90 DtypeArg, 

91 DtypeBackend, 

92 IndexLabel, 

93 ) 

94 

95 from pandas import Index 

96 

97# ----------------------------------------------------------------------------- 

98# -- Helper functions 

99 

100 

101def _process_parse_dates_argument(parse_dates): 

102 """Process parse_dates argument for read_sql functions""" 

103 # handle non-list entries for parse_dates gracefully 

104 if parse_dates is True or parse_dates is None or parse_dates is False: 

105 parse_dates = [] 

106 

107 elif not hasattr(parse_dates, "__iter__"): 

108 parse_dates = [parse_dates] 

109 return parse_dates 

110 

111 

112def _handle_date_column( 

113 col, utc: bool = False, format: str | dict[str, Any] | None = None 

114): 

115 if isinstance(format, dict): 

116 # GH35185 Allow custom error values in parse_dates argument of 

117 # read_sql like functions. 

118 # Format can take on custom to_datetime argument values such as 

119 # {"errors": "coerce"} or {"dayfirst": True} 

120 return to_datetime(col, **format) 

121 else: 

122 # Allow passing of formatting string for integers 

123 # GH17855 

124 if format is None and ( 

125 issubclass(col.dtype.type, np.floating) 

126 or issubclass(col.dtype.type, np.integer) 

127 ): 

128 format = "s" 

129 if format in ["D", "d", "h", "m", "s", "ms", "us", "ns"]: 

130 return to_datetime(col, errors="coerce", unit=format, utc=utc) 

131 elif isinstance(col.dtype, DatetimeTZDtype): 

132 # coerce to UTC timezone 

133 # GH11216 

134 return to_datetime(col, utc=True) 

135 else: 

136 return to_datetime(col, errors="coerce", format=format, utc=utc) 

137 

138 

139def _parse_date_columns(data_frame: DataFrame, parse_dates) -> DataFrame: 

140 """ 

141 Force non-datetime columns to be read as such. 

142 Supports both string formatted and integer timestamp columns. 

143 """ 

144 parse_dates = _process_parse_dates_argument(parse_dates) 

145 

146 # we want to coerce datetime64_tz dtypes for now to UTC 

147 # we could in theory do a 'nice' conversion from a FixedOffset tz 

148 # GH11216 

149 for i, (col_name, df_col) in enumerate(data_frame.items()): 

150 if isinstance(df_col.dtype, DatetimeTZDtype) or col_name in parse_dates: 

151 try: 

152 fmt = parse_dates[col_name] 

153 except (KeyError, TypeError): 

154 fmt = None 

155 data_frame.isetitem(i, _handle_date_column(df_col, format=fmt)) 

156 

157 return data_frame 

158 

159 

160def _convert_arrays_to_dataframe( 

161 data, 

162 columns, 

163 coerce_float: bool = True, 

164 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

165) -> DataFrame: 

166 content = lib.to_object_array_tuples(data) 

167 idx_len = content.shape[0] 

168 arrays = convert_object_array( 

169 list(content.T), 

170 dtype=None, 

171 coerce_float=coerce_float, 

172 dtype_backend=dtype_backend, 

173 ) 

174 if dtype_backend == "pyarrow": 

175 pa = import_optional_dependency("pyarrow") 

176 

177 result_arrays = [] 

178 for arr in arrays: 

179 pa_array = pa.array(arr, from_pandas=True) 

180 if arr.dtype == "string": 

181 # TODO: Arrow still infers strings arrays as regular strings instead 

182 # of large_string, which is what we preserver everywhere else for 

183 # dtype_backend="pyarrow". We may want to reconsider this 

184 pa_array = pa_array.cast(pa.string()) 

185 result_arrays.append(ArrowExtensionArray(pa_array)) 

186 arrays = result_arrays # type: ignore[assignment] 

187 if arrays: 

188 return DataFrame._from_arrays( 

189 arrays, columns=columns, index=range(idx_len), verify_integrity=False 

190 ) 

191 else: 

192 return DataFrame(columns=columns) 

193 

194 

195def _wrap_result( 

196 data, 

197 columns, 

198 index_col=None, 

199 coerce_float: bool = True, 

200 parse_dates=None, 

201 dtype: DtypeArg | None = None, 

202 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

203) -> DataFrame: 

204 """Wrap result set of a SQLAlchemy query in a DataFrame.""" 

205 frame = _convert_arrays_to_dataframe(data, columns, coerce_float, dtype_backend) 

206 

207 if dtype: 

208 frame = frame.astype(dtype) 

209 

210 frame = _parse_date_columns(frame, parse_dates) 

211 

212 if index_col is not None: 

213 frame = frame.set_index(index_col) 

214 

215 return frame 

216 

217 

218def _wrap_result_adbc( 

219 df: DataFrame, 

220 *, 

221 index_col=None, 

222 parse_dates=None, 

223 dtype: DtypeArg | None = None, 

224 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

225) -> DataFrame: 

226 """Wrap result set of a SQLAlchemy query in a DataFrame.""" 

227 if dtype: 

228 df = df.astype(dtype) 

229 

230 df = _parse_date_columns(df, parse_dates) 

231 

232 if index_col is not None: 

233 df = df.set_index(index_col) 

234 

235 return df 

236 

237 

238# ----------------------------------------------------------------------------- 

239# -- Read and write to DataFrames 

240 

241 

242@overload 

243def read_sql_table( # pyright: ignore[reportOverlappingOverload] 

244 table_name: str, 

245 con, 

246 schema=..., 

247 index_col: str | list[str] | None = ..., 

248 coerce_float=..., 

249 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., 

250 columns: list[str] | None = ..., 

251 chunksize: None = ..., 

252 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

253) -> DataFrame: ... 

254 

255 

256@overload 

257def read_sql_table( 

258 table_name: str, 

259 con, 

260 schema=..., 

261 index_col: str | list[str] | None = ..., 

262 coerce_float=..., 

263 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., 

264 columns: list[str] | None = ..., 

265 chunksize: int = ..., 

266 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

267) -> Iterator[DataFrame]: ... 

268 

269 

270@set_module("pandas") 

271def read_sql_table( 

272 table_name: str, 

273 con, 

274 schema: str | None = None, 

275 index_col: str | list[str] | None = None, 

276 coerce_float: bool = True, 

277 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, 

278 columns: list[str] | None = None, 

279 chunksize: int | None = None, 

280 dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, 

281) -> DataFrame | Iterator[DataFrame]: 

282 """ 

283 Read SQL database table into a DataFrame. 

284 

285 Given a table name and a SQLAlchemy connectable, returns a DataFrame. 

286 This function does not support DBAPI connections. 

287 

288 Parameters 

289 ---------- 

290 table_name : str 

291 Name of SQL table in database. 

292 con : SQLAlchemy connectable or str 

293 A database URI could be provided as str. 

294 SQLite DBAPI connection mode not supported. 

295 schema : str, default None 

296 Name of SQL schema in database to query (if database flavor 

297 supports this). Uses default schema if None (default). 

298 index_col : str or list of str, optional, default: None 

299 Column(s) to set as index(MultiIndex). 

300 coerce_float : bool, default True 

301 Attempts to convert values of non-string, non-numeric objects (like 

302 decimal.Decimal) to floating point. Can result in loss of Precision. 

303 parse_dates : list or dict, default None 

304 - List of column names to parse as dates. 

305 - Dict of ``{column_name: format string}`` where format string is 

306 strftime compatible in case of parsing string times or is one of 

307 (D, s, ns, ms, us) in case of parsing integer timestamps. 

308 - Dict of ``{column_name: arg dict}``, where the arg dict corresponds 

309 to the keyword arguments of :func:`pandas.to_datetime` 

310 Especially useful with databases without native Datetime support, 

311 such as SQLite. 

312 columns : list, default None 

313 List of column names to select from SQL table. 

314 chunksize : int, default None 

315 If specified, returns an iterator where `chunksize` is the number of 

316 rows to include in each chunk. 

317 dtype_backend : {'numpy_nullable', 'pyarrow'} 

318 Back-end data type applied to the resultant :class:`DataFrame` 

319 (still experimental). If not specified, the default behavior 

320 is to not use nullable data types. If specified, the behavior 

321 is as follows: 

322 

323 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

324 * ``"pyarrow"``: returns pyarrow-backed nullable 

325 :class:`ArrowDtype` :class:`DataFrame` 

326 

327 .. versionadded:: 2.0 

328 

329 Returns 

330 ------- 

331 DataFrame or Iterator[DataFrame] 

332 A SQL table is returned as two-dimensional data structure with labeled 

333 axes. 

334 

335 See Also 

336 -------- 

337 read_sql_query : Read SQL query into a DataFrame. 

338 read_sql : Read SQL query or database table into a DataFrame. 

339 

340 Notes 

341 ----- 

342 Any datetime values with time zone information will be converted to UTC. 

343 

344 Examples 

345 -------- 

346 >>> pd.read_sql_table("table_name", "postgres:///db_name") # doctest:+SKIP 

347 """ 

348 

349 check_dtype_backend(dtype_backend) 

350 if dtype_backend is lib.no_default: 

351 dtype_backend = "numpy" # type: ignore[assignment] 

352 assert dtype_backend is not lib.no_default 

353 

354 with pandasSQL_builder(con, schema=schema, need_transaction=True) as pandas_sql: 

355 if not pandas_sql.has_table(table_name): 

356 raise ValueError(f"Table {table_name} not found") 

357 

358 table = pandas_sql.read_table( 

359 table_name, 

360 index_col=index_col, 

361 coerce_float=coerce_float, 

362 parse_dates=parse_dates, 

363 columns=columns, 

364 chunksize=chunksize, 

365 dtype_backend=dtype_backend, 

366 ) 

367 

368 if table is not None: 

369 return table 

370 else: 

371 raise ValueError(f"Table {table_name} not found", con) 

372 

373 

374@overload 

375def read_sql_query( # pyright: ignore[reportOverlappingOverload] 

376 sql, 

377 con, 

378 index_col: str | list[str] | None = ..., 

379 coerce_float=..., 

380 params: list[Any] | Mapping[str, Any] | None = ..., 

381 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., 

382 chunksize: None = ..., 

383 dtype: DtypeArg | None = ..., 

384 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

385) -> DataFrame: ... 

386 

387 

388@overload 

389def read_sql_query( 

390 sql, 

391 con, 

392 index_col: str | list[str] | None = ..., 

393 coerce_float=..., 

394 params: list[Any] | Mapping[str, Any] | None = ..., 

395 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = ..., 

396 chunksize: int = ..., 

397 dtype: DtypeArg | None = ..., 

398 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

399) -> Iterator[DataFrame]: ... 

400 

401 

402@set_module("pandas") 

403def read_sql_query( 

404 sql, 

405 con, 

406 index_col: str | list[str] | None = None, 

407 coerce_float: bool = True, 

408 params: list[Any] | Mapping[str, Any] | None = None, 

409 parse_dates: list[str] | dict[str, str] | dict[str, dict[str, Any]] | None = None, 

410 chunksize: int | None = None, 

411 dtype: DtypeArg | None = None, 

412 dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, 

413) -> DataFrame | Iterator[DataFrame]: 

414 """ 

415 Read SQL query into a DataFrame. 

416 

417 Returns a DataFrame corresponding to the result set of the query 

418 string. Optionally provide an `index_col` parameter to use one of the 

419 columns as the index, otherwise default integer index will be used. 

420 

421 Parameters 

422 ---------- 

423 sql : str SQL query or SQLAlchemy Selectable (select or text object) 

424 SQL query to be executed. 

425 con : SQLAlchemy connectable, str, or sqlite3 connection 

426 Using SQLAlchemy makes it possible to use any DB supported by that 

427 library. If a DBAPI2 object, only sqlite3 is supported. 

428 index_col : str or list of str, optional, default: None 

429 Column(s) to set as index(MultiIndex). 

430 coerce_float : bool, default True 

431 Attempts to convert values of non-string, non-numeric objects (like 

432 decimal.Decimal) to floating point. Useful for SQL result sets. 

433 params : list, tuple or mapping, optional, default: None 

434 List of parameters to pass to execute method. The syntax used 

435 to pass parameters is database driver dependent. Check your 

436 database driver documentation for which of the five syntax styles, 

437 described in PEP 249's paramstyle, is supported. 

438 Eg. for psycopg2, uses %(name)s so use params={'name' : 'value'}. 

439 parse_dates : list or dict, default: None 

440 - List of column names to parse as dates. 

441 - Dict of ``{column_name: format string}`` where format string is 

442 strftime compatible in case of parsing string times, or is one of 

443 (D, s, ns, ms, us) in case of parsing integer timestamps. 

444 - Dict of ``{column_name: arg dict}``, where the arg dict corresponds 

445 to the keyword arguments of :func:`pandas.to_datetime` 

446 Especially useful with databases without native Datetime support, 

447 such as SQLite. 

448 chunksize : int, default None 

449 If specified, return an iterator where `chunksize` is the number of 

450 rows to include in each chunk. 

451 dtype : Type name or dict of columns 

452 Data type for data or columns. E.g. np.float64 or 

453 {'a': np.float64, 'b': np.int32, 'c': 'Int64'}. 

454 dtype_backend : {'numpy_nullable', 'pyarrow'} 

455 Back-end data type applied to the resultant :class:`DataFrame` 

456 (still experimental). If not specified, the default behavior 

457 is to not use nullable data types. If specified, the behavior 

458 is as follows: 

459 

460 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

461 * ``"pyarrow"``: returns pyarrow-backed nullable 

462 :class:`ArrowDtype` :class:`DataFrame` 

463 

464 .. versionadded:: 2.0 

465 

466 Returns 

467 ------- 

468 DataFrame or Iterator[DataFrame] 

469 Returns a DataFrame object that contains the result set of the 

470 executed SQL query, in relation to the specified database connection. 

471 

472 See Also 

473 -------- 

474 read_sql_table : Read SQL database table into a DataFrame. 

475 read_sql : Read SQL query or database table into a DataFrame. 

476 

477 Notes 

478 ----- 

479 Any datetime values with time zone information parsed via the `parse_dates` 

480 parameter will be converted to UTC. 

481 

482 Examples 

483 -------- 

484 >>> from sqlalchemy import create_engine # doctest: +SKIP 

485 >>> engine = create_engine("sqlite:///database.db") # doctest: +SKIP 

486 >>> sql_query = "SELECT int_column FROM test_data" # doctest: +SKIP 

487 >>> with engine.connect() as conn, conn.begin(): # doctest: +SKIP 

488 ... data = pd.read_sql_query(sql_query, conn) # doctest: +SKIP 

489 """ 

490 

491 check_dtype_backend(dtype_backend) 

492 if dtype_backend is lib.no_default: 

493 dtype_backend = "numpy" # type: ignore[assignment] 

494 assert dtype_backend is not lib.no_default 

495 

496 with pandasSQL_builder(con) as pandas_sql: 

497 return pandas_sql.read_query( 

498 sql, 

499 index_col=index_col, 

500 params=params, 

501 coerce_float=coerce_float, 

502 parse_dates=parse_dates, 

503 chunksize=chunksize, 

504 dtype=dtype, 

505 dtype_backend=dtype_backend, 

506 ) 

507 

508 

509@overload 

510def read_sql( # pyright: ignore[reportOverlappingOverload] 

511 sql, 

512 con, 

513 index_col: str | list[str] | None = ..., 

514 coerce_float=..., 

515 params=..., 

516 parse_dates=..., 

517 columns: list[str] = ..., 

518 chunksize: None = ..., 

519 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

520 dtype: DtypeArg | None = None, 

521) -> DataFrame: ... 

522 

523 

524@overload 

525def read_sql( 

526 sql, 

527 con, 

528 index_col: str | list[str] | None = ..., 

529 coerce_float=..., 

530 params=..., 

531 parse_dates=..., 

532 columns: list[str] = ..., 

533 chunksize: int = ..., 

534 dtype_backend: DtypeBackend | lib.NoDefault = ..., 

535 dtype: DtypeArg | None = None, 

536) -> Iterator[DataFrame]: ... 

537 

538 

539@set_module("pandas") 

540def read_sql( 

541 sql, 

542 con, 

543 index_col: str | list[str] | None = None, 

544 coerce_float: bool = True, 

545 params=None, 

546 parse_dates=None, 

547 columns: list[str] | None = None, 

548 chunksize: int | None = None, 

549 dtype_backend: DtypeBackend | lib.NoDefault = lib.no_default, 

550 dtype: DtypeArg | None = None, 

551) -> DataFrame | Iterator[DataFrame]: 

552 """ 

553 Read SQL query or database table into a DataFrame. 

554 

555 This function is a convenience wrapper around ``read_sql_table`` and 

556 ``read_sql_query`` (for backward compatibility). It will delegate 

557 to the specific function depending on the provided input. A SQL query 

558 will be routed to ``read_sql_query``, while a database table name will 

559 be routed to ``read_sql_table``. Note that the delegated function might 

560 have more specific notes about their functionality not listed here. 

561 

562 Parameters 

563 ---------- 

564 sql : str or SQLAlchemy Selectable (select or text object) 

565 SQL query to be executed or a table name. 

566 con : ADBC Connection, SQLAlchemy connectable, str, or sqlite3 connection 

567 ADBC provides high performance I/O with native type support, where available. 

568 Using SQLAlchemy makes it possible to use any DB supported by that 

569 library. If a DBAPI2 object, only sqlite3 is supported. The user is responsible 

570 for engine disposal and connection closure for the ADBC connection and 

571 SQLAlchemy connectable; str connections are closed automatically. See 

572 `here <https://docs.sqlalchemy.org/en/20/core/connections.html>`_. 

573 index_col : str or list of str, optional, default: None 

574 Column(s) to set as index(MultiIndex). 

575 coerce_float : bool, default True 

576 Attempts to convert values of non-string, non-numeric objects (like 

577 decimal.Decimal) to floating point, useful for SQL result sets. 

578 params : list, tuple or dict, optional, default: None 

579 List of parameters to pass to execute method. The syntax used 

580 to pass parameters is database driver dependent. Check your 

581 database driver documentation for which of the five syntax styles, 

582 described in PEP 249's paramstyle, is supported. 

583 Eg. for psycopg2, uses %(name)s so use params={'name' : 'value'}. 

584 parse_dates : list or dict, default: None 

585 - List of column names to parse as dates. 

586 - Dict of ``{column_name: format string}`` where format string is 

587 strftime compatible in case of parsing string times, or is one of 

588 (D, s, ns, ms, us) in case of parsing integer timestamps. 

589 - Dict of ``{column_name: arg dict}``, where the arg dict corresponds 

590 to the keyword arguments of :func:`pandas.to_datetime` 

591 Especially useful with databases without native Datetime support, 

592 such as SQLite. 

593 columns : list, default: None 

594 List of column names to select from SQL table (only used when reading 

595 a table). 

596 chunksize : int, default None 

597 If specified, return an iterator where `chunksize` is the 

598 number of rows to include in each chunk. 

599 dtype_backend : {'numpy_nullable', 'pyarrow'} 

600 Back-end data type applied to the resultant :class:`DataFrame` 

601 (still experimental). If not specified, the default behavior 

602 is to not use nullable data types. If specified, the behavior 

603 is as follows: 

604 

605 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

606 * ``"pyarrow"``: returns pyarrow-backed nullable 

607 :class:`ArrowDtype` :class:`DataFrame` 

608 

609 .. versionadded:: 2.0 

610 dtype : Type name or dict of columns 

611 Data type for data or columns. E.g. np.float64 or 

612 {'a': np.float64, 'b': np.int32, 'c': 'Int64'}. 

613 The argument is ignored if a table is passed instead of a query. 

614 

615 .. versionadded:: 2.0.0 

616 

617 Returns 

618 ------- 

619 DataFrame or Iterator[DataFrame] 

620 Returns a DataFrame object that contains the result set of the 

621 executed SQL query or an SQL Table based on the provided input, 

622 in relation to the specified database connection. 

623 

624 See Also 

625 -------- 

626 read_sql_table : Read SQL database table into a DataFrame. 

627 read_sql_query : Read SQL query into a DataFrame. 

628 

629 Notes 

630 ----- 

631 ``pandas`` does not attempt to sanitize SQL statements; 

632 instead it simply forwards the statement you are executing 

633 to the underlying driver, which may or may not sanitize from there. 

634 Please refer to the underlying driver documentation for any details. 

635 Generally, be wary when accepting statements from arbitrary sources. 

636 

637 Examples 

638 -------- 

639 Read data from SQL via either a SQL query or a SQL tablename. 

640 When using a SQLite database only SQL queries are accepted, 

641 providing only the SQL tablename will result in an error. 

642 

643 >>> from sqlite3 import connect 

644 >>> conn = connect(":memory:") 

645 >>> df = pd.DataFrame( 

646 ... data=[[0, "10/11/12"], [1, "12/11/10"]], 

647 ... columns=["int_column", "date_column"], 

648 ... ) 

649 >>> df.to_sql(name="test_data", con=conn) 

650 2 

651 

652 >>> pd.read_sql("SELECT int_column, date_column FROM test_data", conn) 

653 int_column date_column 

654 0 0 10/11/12 

655 1 1 12/11/10 

656 

657 >>> pd.read_sql("test_data", "postgres:///db_name") # doctest:+SKIP 

658 

659 For parameterized query, using ``params`` is recommended over string interpolation. 

660 

661 >>> from sqlalchemy import text 

662 >>> sql = text( 

663 ... "SELECT int_column, date_column FROM test_data WHERE int_column=:int_val" 

664 ... ) 

665 >>> pd.read_sql(sql, conn, params={"int_val": 1}) # doctest:+SKIP 

666 int_column date_column 

667 0 1 12/11/10 

668 

669 Apply date parsing to columns through the ``parse_dates`` argument 

670 The ``parse_dates`` argument calls ``pd.to_datetime`` on the provided columns. 

671 Custom argument values for applying ``pd.to_datetime`` on a column are specified 

672 via a dictionary format: 

673 

674 >>> pd.read_sql( 

675 ... "SELECT int_column, date_column FROM test_data", 

676 ... conn, 

677 ... parse_dates={"date_column": {"format": "%d/%m/%y"}}, 

678 ... ) 

679 int_column date_column 

680 0 0 2012-11-10 

681 1 1 2010-11-12 

682 

683 .. versionadded:: 2.2.0 

684 

685 pandas now supports reading via ADBC drivers 

686 

687 >>> from adbc_driver_postgresql import dbapi # doctest:+SKIP 

688 >>> with dbapi.connect("postgres:///db_name") as conn: # doctest:+SKIP 

689 ... pd.read_sql("SELECT int_column FROM test_data", conn) 

690 int_column 

691 0 0 

692 1 1 

693 """ 

694 

695 check_dtype_backend(dtype_backend) 

696 if dtype_backend is lib.no_default: 

697 dtype_backend = "numpy" # type: ignore[assignment] 

698 assert dtype_backend is not lib.no_default 

699 

700 with pandasSQL_builder(con) as pandas_sql: 

701 if isinstance(pandas_sql, SQLiteDatabase): 

702 return pandas_sql.read_query( 

703 sql, 

704 index_col=index_col, 

705 params=params, 

706 coerce_float=coerce_float, 

707 parse_dates=parse_dates, 

708 chunksize=chunksize, 

709 dtype_backend=dtype_backend, 

710 dtype=dtype, 

711 ) 

712 

713 try: 

714 _is_table_name = pandas_sql.has_table(sql) 

715 except Exception: 

716 # using generic exception to catch errors from sql drivers (GH24988) 

717 _is_table_name = False 

718 

719 if _is_table_name: 

720 return pandas_sql.read_table( 

721 sql, 

722 index_col=index_col, 

723 coerce_float=coerce_float, 

724 parse_dates=parse_dates, 

725 columns=columns, 

726 chunksize=chunksize, 

727 dtype_backend=dtype_backend, 

728 ) 

729 else: 

730 return pandas_sql.read_query( 

731 sql, 

732 index_col=index_col, 

733 params=params, 

734 coerce_float=coerce_float, 

735 parse_dates=parse_dates, 

736 chunksize=chunksize, 

737 dtype_backend=dtype_backend, 

738 dtype=dtype, 

739 ) 

740 

741 

742def to_sql( 

743 frame, 

744 name: str, 

745 con, 

746 schema: str | None = None, 

747 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

748 index: bool = True, 

749 index_label: IndexLabel | None = None, 

750 chunksize: int | None = None, 

751 dtype: DtypeArg | None = None, 

752 method: Literal["multi"] | Callable | None = None, 

753 engine: str = "auto", 

754 **engine_kwargs, 

755) -> int | None: 

756 """ 

757 Write records stored in a DataFrame to a SQL database. 

758 

759 .. warning:: 

760 The pandas library does not attempt to sanitize inputs provided via a to_sql call. 

761 Please refer to the documentation for the underlying database driver to see if it 

762 will properly prevent injection, or alternatively be advised of a security risk when 

763 executing arbitrary commands in a to_sql call. 

764 

765 Parameters 

766 ---------- 

767 frame : DataFrame, Series 

768 name : str 

769 Name of SQL table. 

770 con : ADBC Connection, SQLAlchemy connectable, str, or sqlite3 connection 

771 or sqlite3 DBAPI2 connection 

772 ADBC provides high performance I/O with native type support, where available. 

773 Using SQLAlchemy makes it possible to use any DB supported by that 

774 library. 

775 If a DBAPI2 object, only sqlite3 is supported. 

776 schema : str, optional 

777 Name of SQL schema in database to write to (if database flavor 

778 supports this). If None, use default schema (default). 

779 if_exists : {'fail', 'replace', 'append', 'delete_rows'}, default 'fail' 

780 - fail: If table exists, do nothing. 

781 - replace: If table exists, drop it, recreate it, and insert data. 

782 - append: If table exists, insert data. Create if does not exist. 

783 - delete_rows: If a table exists, delete all records and insert data. 

784 index : bool, default True 

785 Write DataFrame index as a column. 

786 index_label : str or sequence, optional 

787 Column label for index column(s). If None is given (default) and 

788 `index` is True, then the index names are used. 

789 A sequence should be given if the DataFrame uses MultiIndex. 

790 chunksize : int, optional 

791 Specify the number of rows in each batch to be written at a time. 

792 By default, all rows will be written at once. 

793 dtype : dict or scalar, optional 

794 Specifying the datatype for columns. If a dictionary is used, the 

795 keys should be the column names and the values should be the 

796 SQLAlchemy types or strings for the sqlite3 fallback mode. If a 

797 scalar is provided, it will be applied to all columns. 

798 method : {None, 'multi', callable}, optional 

799 Controls the SQL insertion clause used: 

800 

801 - None : Uses standard SQL ``INSERT`` clause (one per row). 

802 - ``'multi'``: Pass multiple values in a single ``INSERT`` clause. 

803 - callable with signature ``(pd_table, conn, keys, data_iter) -> int | None``. 

804 

805 Details and a sample callable implementation can be found in the 

806 section :ref:`insert method <io.sql.method>`. 

807 engine : {'auto', 'sqlalchemy'}, default 'auto' 

808 SQL engine library to use. If 'auto', then the option 

809 ``io.sql.engine`` is used. The default ``io.sql.engine`` 

810 behavior is 'sqlalchemy' 

811 

812 **engine_kwargs 

813 Any additional kwargs are passed to the engine. 

814 

815 Returns 

816 ------- 

817 None or int 

818 Number of rows affected by to_sql. None is returned if the callable 

819 passed into ``method`` does not return an integer number of rows. 

820 

821 Notes 

822 ----- 

823 The returned rows affected is the sum of the ``rowcount`` attribute of ``sqlite3.Cursor`` 

824 or SQLAlchemy connectable. If using ADBC the returned rows are the result 

825 of ``Cursor.adbc_ingest``. The returned value may not reflect the exact number of written 

826 rows as stipulated in the 

827 `sqlite3 <https://docs.python.org/3/library/sqlite3.html#sqlite3.Cursor.rowcount>`__ or 

828 `SQLAlchemy <https://docs.sqlalchemy.org/en/14/core/connections.html#sqlalchemy.engine.BaseCursorResult.rowcount>`__ 

829 """ # noqa: E501 

830 if if_exists not in ("fail", "replace", "append", "delete_rows"): 

831 raise ValueError(f"'{if_exists}' is not valid for if_exists") 

832 

833 if isinstance(frame, Series): 

834 frame = frame.to_frame() 

835 elif not isinstance(frame, DataFrame): 

836 raise NotImplementedError( 

837 "'frame' argument should be either a Series or a DataFrame" 

838 ) 

839 

840 with pandasSQL_builder(con, schema=schema, need_transaction=True) as pandas_sql: 

841 return pandas_sql.to_sql( 

842 frame, 

843 name, 

844 if_exists=if_exists, 

845 index=index, 

846 index_label=index_label, 

847 schema=schema, 

848 chunksize=chunksize, 

849 dtype=dtype, 

850 method=method, 

851 engine=engine, 

852 **engine_kwargs, 

853 ) 

854 

855 

856def has_table(table_name: str, con, schema: str | None = None) -> bool: 

857 """ 

858 Check if DataBase has named table. 

859 

860 Parameters 

861 ---------- 

862 table_name: string 

863 Name of SQL table. 

864 con: ADBC Connection, SQLAlchemy connectable, str, or sqlite3 connection 

865 ADBC provides high performance I/O with native type support, where available. 

866 Using SQLAlchemy makes it possible to use any DB supported by that 

867 library. 

868 If a DBAPI2 object, only sqlite3 is supported. 

869 schema : string, default None 

870 Name of SQL schema in database to write to (if database flavor supports 

871 this). If None, use default schema (default). 

872 

873 Returns 

874 ------- 

875 boolean 

876 """ 

877 with pandasSQL_builder(con, schema=schema) as pandas_sql: 

878 return pandas_sql.has_table(table_name) 

879 

880 

881table_exists = has_table 

882 

883 

884def pandasSQL_builder( 

885 con, 

886 schema: str | None = None, 

887 need_transaction: bool = False, 

888) -> PandasSQL: 

889 """ 

890 Convenience function to return the correct PandasSQL subclass based on the 

891 provided parameters. Also creates a sqlalchemy connection and transaction 

892 if necessary. 

893 """ 

894 import sqlite3 

895 

896 if isinstance(con, sqlite3.Connection) or con is None: 

897 return SQLiteDatabase(con) 

898 

899 sqlalchemy = import_optional_dependency("sqlalchemy", errors="ignore") 

900 

901 if isinstance(con, str) and sqlalchemy is None: 

902 raise ImportError( 

903 f"Using URI string without version '{VERSIONS['sqlalchemy']}' or newer " 

904 "of 'sqlalchemy' installed." 

905 ) 

906 

907 if sqlalchemy is not None and isinstance(con, (str, sqlalchemy.engine.Connectable)): 

908 return SQLDatabase(con, schema, need_transaction) 

909 

910 adbc = import_optional_dependency("adbc_driver_manager.dbapi", errors="ignore") 

911 if adbc and isinstance(con, adbc.Connection): 

912 return ADBCDatabase(con) 

913 

914 warnings.warn( 

915 "pandas only supports SQLAlchemy connectable (engine/connection) or " 

916 "database string URI or sqlite3 DBAPI2 connection. Other DBAPI2 " 

917 "objects are not tested. Please consider using SQLAlchemy.", 

918 UserWarning, 

919 stacklevel=find_stack_level(), 

920 ) 

921 return SQLiteDatabase(con) 

922 

923 

924class SQLTable(PandasObject): 

925 """ 

926 For mapping Pandas tables to SQL tables. 

927 Uses fact that table is reflected by SQLAlchemy to 

928 do better type conversions. 

929 Also holds various flags needed to avoid having to 

930 pass them between functions all the time. 

931 """ 

932 

933 # TODO: support for multiIndex 

934 

935 def __init__( 

936 self, 

937 name: str, 

938 pandas_sql_engine, 

939 frame=None, 

940 index: bool | str | list[str] | None = True, 

941 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

942 prefix: str = "pandas", 

943 index_label=None, 

944 schema=None, 

945 keys=None, 

946 dtype: DtypeArg | None = None, 

947 ) -> None: 

948 self.name = name 

949 self.pd_sql = pandas_sql_engine 

950 self.prefix = prefix 

951 self.frame = frame 

952 self.index = self._index_name(index, index_label) 

953 self.schema = schema 

954 self.if_exists = if_exists 

955 self.keys = keys 

956 self.dtype = dtype 

957 

958 if frame is not None: 

959 # We want to initialize based on a dataframe 

960 self.table = self._create_table_setup() 

961 else: 

962 # no data provided, read-only mode 

963 self.table = self.pd_sql.get_table(self.name, self.schema) 

964 

965 if self.table is None: 

966 raise ValueError(f"Could not init table '{name}'") 

967 

968 if not len(self.name): 

969 raise ValueError("Empty table name specified") 

970 

971 def exists(self): 

972 return self.pd_sql.has_table(self.name, self.schema) 

973 

974 def sql_schema(self) -> str: 

975 from sqlalchemy.schema import CreateTable 

976 

977 return str(CreateTable(self.table).compile(self.pd_sql.con)) 

978 

979 def _execute_create(self) -> None: 

980 # Inserting table into database, add to MetaData object 

981 self.table = self.table.to_metadata(self.pd_sql.meta) 

982 with self.pd_sql.run_transaction(): 

983 self.table.create(bind=self.pd_sql.con) 

984 

985 def create(self) -> None: 

986 if self.exists(): 

987 if self.if_exists == "fail": 

988 raise ValueError(f"Table '{self.name}' already exists.") 

989 elif self.if_exists == "replace": 

990 self.pd_sql.drop_table(self.name, self.schema) 

991 self._execute_create() 

992 elif self.if_exists == "append": 

993 pass 

994 elif self.if_exists == "delete_rows": 

995 self.pd_sql.delete_rows(self.name, self.schema) 

996 else: 

997 raise ValueError(f"'{self.if_exists}' is not valid for if_exists") 

998 else: 

999 self._execute_create() 

1000 

1001 def _execute_insert(self, conn, keys: list[str], data_iter) -> int: 

1002 """ 

1003 Execute SQL statement inserting data 

1004 

1005 Parameters 

1006 ---------- 

1007 conn : sqlalchemy.engine.Engine or sqlalchemy.engine.Connection 

1008 keys : list of str 

1009 Column names 

1010 data_iter : generator of list 

1011 Each item contains a list of values to be inserted 

1012 """ 

1013 data = [dict(zip(keys, row, strict=True)) for row in data_iter] 

1014 result = self.pd_sql.execute(self.table.insert(), data) 

1015 return result.rowcount 

1016 

1017 def _execute_insert_multi(self, conn, keys: list[str], data_iter) -> int: 

1018 """ 

1019 Alternative to _execute_insert for DBs support multi-value INSERT. 

1020 

1021 Note: multi-value insert is usually faster for analytics DBs 

1022 and tables containing a few columns 

1023 but performance degrades quickly with increase of columns. 

1024 

1025 """ 

1026 

1027 from sqlalchemy import insert 

1028 

1029 data = [dict(zip(keys, row, strict=True)) for row in data_iter] 

1030 stmt = insert(self.table).values(data) 

1031 result = self.pd_sql.execute(stmt) 

1032 return result.rowcount 

1033 

1034 def insert_data(self) -> tuple[list[str], list[np.ndarray]]: 

1035 if self.index is not None: 

1036 temp = self.frame.copy(deep=False) 

1037 temp.index.names = self.index 

1038 try: 

1039 temp.reset_index(inplace=True) 

1040 except ValueError as err: 

1041 raise ValueError(f"duplicate name in index/columns: {err}") from err 

1042 else: 

1043 temp = self.frame 

1044 

1045 column_names = list(map(str, temp.columns)) 

1046 ncols = len(column_names) 

1047 # this just pre-allocates the list: None's will be replaced with ndarrays 

1048 # error: List item 0 has incompatible type "None"; expected "ndarray" 

1049 data_list: list[np.ndarray] = [None] * ncols # type: ignore[list-item] 

1050 

1051 for i, (_, ser) in enumerate(temp.items()): 

1052 if ser.dtype.kind == "M": 

1053 if isinstance(ser._values, ArrowExtensionArray): 

1054 import pyarrow as pa 

1055 

1056 if pa.types.is_date(ser.dtype.pyarrow_dtype): 

1057 # GH#53854 to_pydatetime not supported for pyarrow date dtypes 

1058 d = ser._values.to_numpy(dtype=object) 

1059 else: 

1060 d = ser.dt.to_pydatetime()._values 

1061 else: 

1062 d = ser._values.to_pydatetime() 

1063 elif ser.dtype.kind == "m": 

1064 vals = ser._values 

1065 if isinstance(vals, ArrowExtensionArray): 

1066 vals = vals.to_numpy(dtype=np.dtype("m8[ns]")) 

1067 # store as integers, see GH#6921, GH#7076 

1068 d = vals.view("i8").astype(object) 

1069 else: 

1070 d = ser._values.astype(object) 

1071 

1072 assert isinstance(d, np.ndarray), type(d) 

1073 

1074 if ser._can_hold_na: 

1075 # Note: this will miss timedeltas since they are converted to int 

1076 mask = isna(d) 

1077 d[mask] = None 

1078 

1079 data_list[i] = d 

1080 

1081 return column_names, data_list 

1082 

1083 def insert( 

1084 self, 

1085 chunksize: int | None = None, 

1086 method: Literal["multi"] | Callable | None = None, 

1087 ) -> int | None: 

1088 # set insert method 

1089 if method is None: 

1090 exec_insert = self._execute_insert 

1091 elif method == "multi": 

1092 exec_insert = self._execute_insert_multi 

1093 elif callable(method): 

1094 exec_insert = partial(method, self) 

1095 else: 

1096 raise ValueError(f"Invalid parameter `method`: {method}") 

1097 

1098 keys, data_list = self.insert_data() 

1099 

1100 nrows = len(self.frame) 

1101 

1102 if nrows == 0: 

1103 return 0 

1104 

1105 if chunksize is None: 

1106 chunksize = nrows 

1107 elif chunksize == 0: 

1108 raise ValueError("chunksize argument should be non-zero") 

1109 

1110 chunks = (nrows // chunksize) + 1 

1111 total_inserted = None 

1112 with self.pd_sql.run_transaction() as conn: 

1113 for i in range(chunks): 

1114 start_i = i * chunksize 

1115 end_i = min((i + 1) * chunksize, nrows) 

1116 if start_i >= end_i: 

1117 break 

1118 

1119 chunk_iter = zip( 

1120 *(arr[start_i:end_i] for arr in data_list), strict=True 

1121 ) 

1122 num_inserted = exec_insert(conn, keys, chunk_iter) 

1123 # GH 46891 

1124 if num_inserted is not None: 

1125 if total_inserted is None: 

1126 total_inserted = num_inserted 

1127 else: 

1128 total_inserted += num_inserted 

1129 return total_inserted 

1130 

1131 def _query_iterator( 

1132 self, 

1133 result, 

1134 exit_stack: ExitStack, 

1135 chunksize: int | None, 

1136 columns, 

1137 coerce_float: bool = True, 

1138 parse_dates=None, 

1139 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1140 ) -> Generator[DataFrame]: 

1141 """Return generator through chunked result set.""" 

1142 has_read_data = False 

1143 with exit_stack: 

1144 while True: 

1145 data = result.fetchmany(chunksize) 

1146 if not data: 

1147 if not has_read_data: 

1148 yield DataFrame.from_records( 

1149 [], columns=columns, coerce_float=coerce_float 

1150 ) 

1151 break 

1152 

1153 has_read_data = True 

1154 self.frame = _convert_arrays_to_dataframe( 

1155 data, columns, coerce_float, dtype_backend 

1156 ) 

1157 

1158 self._harmonize_columns( 

1159 parse_dates=parse_dates, dtype_backend=dtype_backend 

1160 ) 

1161 

1162 if self.index is not None: 

1163 self.frame.set_index(self.index, inplace=True) 

1164 

1165 yield self.frame 

1166 

1167 def read( 

1168 self, 

1169 exit_stack: ExitStack, 

1170 coerce_float: bool = True, 

1171 parse_dates=None, 

1172 columns=None, 

1173 chunksize: int | None = None, 

1174 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1175 ) -> DataFrame | Iterator[DataFrame]: 

1176 from sqlalchemy import select 

1177 

1178 if columns is not None and len(columns) > 0: 

1179 cols = [self.table.c[n] for n in columns] 

1180 if self.index is not None: 

1181 for idx in self.index[::-1]: 

1182 cols.insert(0, self.table.c[idx]) 

1183 sql_select = select(*cols) 

1184 else: 

1185 sql_select = select(self.table) 

1186 result = self.pd_sql.execute(sql_select) 

1187 column_names = result.keys() 

1188 

1189 if chunksize is not None: 

1190 return self._query_iterator( 

1191 result, 

1192 exit_stack, 

1193 chunksize, 

1194 column_names, 

1195 coerce_float=coerce_float, 

1196 parse_dates=parse_dates, 

1197 dtype_backend=dtype_backend, 

1198 ) 

1199 else: 

1200 data = result.fetchall() 

1201 self.frame = _convert_arrays_to_dataframe( 

1202 data, column_names, coerce_float, dtype_backend 

1203 ) 

1204 

1205 self._harmonize_columns( 

1206 parse_dates=parse_dates, dtype_backend=dtype_backend 

1207 ) 

1208 

1209 if self.index is not None: 

1210 self.frame.set_index(self.index, inplace=True) 

1211 

1212 return self.frame 

1213 

1214 def _index_name(self, index, index_label): 

1215 # for writing: index=True to include index in sql table 

1216 if index is True: 

1217 nlevels = self.frame.index.nlevels 

1218 # if index_label is specified, set this as index name(s) 

1219 if index_label is not None: 

1220 if not isinstance(index_label, list): 

1221 index_label = [index_label] 

1222 if len(index_label) != nlevels: 

1223 raise ValueError( 

1224 "Length of 'index_label' should match number of " 

1225 f"levels, which is {nlevels}" 

1226 ) 

1227 return index_label 

1228 # return the used column labels for the index columns 

1229 if ( 

1230 nlevels == 1 

1231 and "index" not in self.frame.columns 

1232 and self.frame.index.name is None 

1233 ): 

1234 return ["index"] 

1235 else: 

1236 return com.fill_missing_names(self.frame.index.names) 

1237 

1238 # for reading: index=(list of) string to specify column to set as index 

1239 elif isinstance(index, str): 

1240 return [index] 

1241 elif isinstance(index, list): 

1242 return index 

1243 else: 

1244 return None 

1245 

1246 def _get_column_names_and_types(self, dtype_mapper): 

1247 column_names_and_types = [] 

1248 if self.index is not None: 

1249 for i, idx_label in enumerate(self.index): 

1250 idx_type = dtype_mapper(self.frame.index._get_level_values(i)) 

1251 column_names_and_types.append((str(idx_label), idx_type, True)) 

1252 

1253 column_names_and_types += [ 

1254 (str(self.frame.columns[i]), dtype_mapper(self.frame.iloc[:, i]), False) 

1255 for i in range(len(self.frame.columns)) 

1256 ] 

1257 

1258 return column_names_and_types 

1259 

1260 def _create_table_setup(self): 

1261 from sqlalchemy import ( 

1262 Column, 

1263 PrimaryKeyConstraint, 

1264 Table, 

1265 ) 

1266 from sqlalchemy.schema import MetaData 

1267 

1268 column_names_and_types = self._get_column_names_and_types(self._sqlalchemy_type) 

1269 

1270 columns: list[Any] = [ 

1271 Column(name, typ, index=is_index) 

1272 for name, typ, is_index in column_names_and_types 

1273 ] 

1274 

1275 if self.keys is not None: 

1276 if not is_list_like(self.keys): 

1277 keys = [self.keys] 

1278 else: 

1279 keys = self.keys 

1280 pkc = PrimaryKeyConstraint(*keys, name=self.name + "_pk") 

1281 columns.append(pkc) 

1282 

1283 schema = self.schema or self.pd_sql.meta.schema 

1284 

1285 # At this point, attach to new metadata, only attach to self.meta 

1286 # once table is created. 

1287 meta = MetaData() 

1288 return Table(self.name, meta, *columns, schema=schema) 

1289 

1290 def _harmonize_columns( 

1291 self, 

1292 parse_dates=None, 

1293 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1294 ) -> None: 

1295 """ 

1296 Make the DataFrame's column types align with the SQL table 

1297 column types. 

1298 Need to work around limited NA value support. Floats are always 

1299 fine, ints must always be floats if there are Null values. 

1300 Booleans are hard because converting bool column with None replaces 

1301 all Nones with false. Therefore only convert bool if there are no 

1302 NA values. 

1303 Datetimes should already be converted to np.datetime64 if supported, 

1304 but here we also force conversion if required. 

1305 """ 

1306 parse_dates = _process_parse_dates_argument(parse_dates) 

1307 

1308 for sql_col in self.table.columns: 

1309 col_name = sql_col.name 

1310 try: 

1311 df_col = self.frame[col_name] 

1312 

1313 # Handle date parsing upfront; don't try to convert columns 

1314 # twice 

1315 if col_name in parse_dates: 

1316 try: 

1317 fmt = parse_dates[col_name] 

1318 except TypeError: 

1319 fmt = None 

1320 self.frame[col_name] = _handle_date_column(df_col, format=fmt) 

1321 continue 

1322 

1323 # the type the dataframe column should have 

1324 col_type = self._get_dtype(sql_col.type) 

1325 

1326 if ( 

1327 col_type is datetime 

1328 or col_type is date 

1329 or col_type is DatetimeTZDtype 

1330 ): 

1331 # Convert tz-aware Datetime SQL columns to UTC 

1332 utc = col_type is DatetimeTZDtype 

1333 self.frame[col_name] = _handle_date_column(df_col, utc=utc) 

1334 elif dtype_backend == "numpy" and col_type is float: 

1335 # floats support NA, can always convert! 

1336 self.frame[col_name] = df_col.astype(col_type) 

1337 elif ( 

1338 using_string_dtype() 

1339 and is_string_dtype(col_type) 

1340 and is_object_dtype(self.frame[col_name]) 

1341 ): 

1342 self.frame[col_name] = df_col.astype(col_type) 

1343 elif dtype_backend == "numpy" and len(df_col) == df_col.count(): 

1344 # No NA values, can convert ints and bools 

1345 if col_type is np.dtype("int64") or col_type is bool: 

1346 self.frame[col_name] = df_col.astype(col_type) 

1347 except KeyError: 

1348 pass # this column not in results 

1349 

1350 def _sqlalchemy_type(self, col: Index | Series): 

1351 dtype: DtypeArg = self.dtype or {} 

1352 if is_dict_like(dtype): 

1353 dtype = cast(dict, dtype) 

1354 if col.name in dtype: 

1355 return dtype[col.name] 

1356 

1357 # Infer type of column, while ignoring missing values. 

1358 # Needed for inserting typed data containing NULLs, GH 8778. 

1359 col_type = lib.infer_dtype(col, skipna=True) 

1360 

1361 from sqlalchemy.types import ( 

1362 TIMESTAMP, 

1363 BigInteger, 

1364 Boolean, 

1365 Date, 

1366 DateTime, 

1367 Float, 

1368 Integer, 

1369 SmallInteger, 

1370 Text, 

1371 Time, 

1372 ) 

1373 

1374 if col_type in ("datetime64", "datetime"): 

1375 # GH 9086: TIMESTAMP is the suggested type if the column contains 

1376 # timezone information 

1377 try: 

1378 # error: Item "Index" of "Union[Index, Series]" has no attribute "dt" 

1379 if col.dt.tz is not None: # type: ignore[union-attr] 

1380 return TIMESTAMP(timezone=True) 

1381 except AttributeError: 

1382 # The column is actually a DatetimeIndex 

1383 # GH 26761 or an Index with date-like data e.g. 9999-01-01 

1384 if getattr(col, "tz", None) is not None: 

1385 return TIMESTAMP(timezone=True) 

1386 return DateTime 

1387 if col_type == "timedelta64": 

1388 warnings.warn( 

1389 "the 'timedelta' type is not supported, and will be " 

1390 "written as integer values (ns frequency) to the database.", 

1391 UserWarning, 

1392 stacklevel=find_stack_level(), 

1393 ) 

1394 return BigInteger 

1395 elif col_type == "floating": 

1396 if col.dtype == "float32": 

1397 return Float(precision=23) 

1398 else: 

1399 return Float(precision=53) 

1400 elif col_type == "integer": 

1401 # GH35076 Map pandas integer to optimal SQLAlchemy integer type 

1402 if col.dtype.name.lower() in ("int8", "uint8", "int16"): 

1403 return SmallInteger 

1404 elif col.dtype.name.lower() in ("uint16", "int32"): 

1405 return Integer 

1406 elif col.dtype.name.lower() == "uint64": 

1407 raise ValueError("Unsigned 64 bit integer datatype is not supported") 

1408 else: 

1409 return BigInteger 

1410 elif col_type == "boolean": 

1411 return Boolean 

1412 elif col_type == "date": 

1413 return Date 

1414 elif col_type == "time": 

1415 return Time 

1416 elif col_type == "complex": 

1417 raise ValueError("Complex datatypes not supported") 

1418 

1419 return Text 

1420 

1421 def _get_dtype(self, sqltype): 

1422 from sqlalchemy.types import ( 

1423 TIMESTAMP, 

1424 Boolean, 

1425 Date, 

1426 DateTime, 

1427 Float, 

1428 Integer, 

1429 String, 

1430 ) 

1431 

1432 if isinstance(sqltype, Float): 

1433 return float 

1434 elif isinstance(sqltype, Integer): 

1435 # TODO: Refine integer size. 

1436 return np.dtype("int64") 

1437 elif isinstance(sqltype, TIMESTAMP): 

1438 # we have a timezone capable type 

1439 if not sqltype.timezone: 

1440 return datetime 

1441 return DatetimeTZDtype 

1442 elif isinstance(sqltype, DateTime): 

1443 # Caution: np.datetime64 is also a subclass of np.number. 

1444 return datetime 

1445 elif isinstance(sqltype, Date): 

1446 return date 

1447 elif isinstance(sqltype, Boolean): 

1448 return bool 

1449 elif isinstance(sqltype, String): 

1450 if using_string_dtype(): 

1451 return StringDtype(na_value=np.nan) 

1452 

1453 return object 

1454 

1455 

1456class PandasSQL(PandasObject, ABC): 

1457 """ 

1458 Subclasses Should define read_query and to_sql. 

1459 """ 

1460 

1461 def __enter__(self) -> Self: 

1462 return self 

1463 

1464 def __exit__(self, *args) -> None: 

1465 pass 

1466 

1467 def read_table( 

1468 self, 

1469 table_name: str, 

1470 index_col: str | list[str] | None = None, 

1471 coerce_float: bool = True, 

1472 parse_dates=None, 

1473 columns=None, 

1474 schema: str | None = None, 

1475 chunksize: int | None = None, 

1476 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1477 ) -> DataFrame | Iterator[DataFrame]: 

1478 raise NotImplementedError 

1479 

1480 @abstractmethod 

1481 def read_query( 

1482 self, 

1483 sql: str, 

1484 index_col: str | list[str] | None = None, 

1485 coerce_float: bool = True, 

1486 parse_dates=None, 

1487 params=None, 

1488 chunksize: int | None = None, 

1489 dtype: DtypeArg | None = None, 

1490 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1491 ) -> DataFrame | Iterator[DataFrame]: 

1492 pass 

1493 

1494 @abstractmethod 

1495 def to_sql( 

1496 self, 

1497 frame, 

1498 name: str, 

1499 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

1500 index: bool = True, 

1501 index_label=None, 

1502 schema=None, 

1503 chunksize: int | None = None, 

1504 dtype: DtypeArg | None = None, 

1505 method: Literal["multi"] | Callable | None = None, 

1506 engine: str = "auto", 

1507 **engine_kwargs, 

1508 ) -> int | None: 

1509 pass 

1510 

1511 @abstractmethod 

1512 def execute(self, sql: str | Select | TextClause, params=None): 

1513 pass 

1514 

1515 @abstractmethod 

1516 def has_table(self, name: str, schema: str | None = None) -> bool: 

1517 pass 

1518 

1519 @abstractmethod 

1520 def _create_sql_schema( 

1521 self, 

1522 frame: DataFrame, 

1523 table_name: str, 

1524 keys: list[str] | None = None, 

1525 dtype: DtypeArg | None = None, 

1526 schema: str | None = None, 

1527 ) -> str: 

1528 pass 

1529 

1530 

1531class BaseEngine: 

1532 def insert_records( 

1533 self, 

1534 table: SQLTable, 

1535 con, 

1536 frame, 

1537 name: str, 

1538 index: bool | str | list[str] | None = True, 

1539 schema=None, 

1540 chunksize: int | None = None, 

1541 method=None, 

1542 **engine_kwargs, 

1543 ) -> int | None: 

1544 """ 

1545 Inserts data into already-prepared table 

1546 """ 

1547 raise AbstractMethodError(self) 

1548 

1549 

1550class SQLAlchemyEngine(BaseEngine): 

1551 def __init__(self) -> None: 

1552 import_optional_dependency( 

1553 "sqlalchemy", extra="sqlalchemy is required for SQL support." 

1554 ) 

1555 

1556 def insert_records( 

1557 self, 

1558 table: SQLTable, 

1559 con, 

1560 frame, 

1561 name: str, 

1562 index: bool | str | list[str] | None = True, 

1563 schema=None, 

1564 chunksize: int | None = None, 

1565 method=None, 

1566 **engine_kwargs, 

1567 ) -> int | None: 

1568 from sqlalchemy import exc 

1569 

1570 try: 

1571 return table.insert(chunksize=chunksize, method=method) 

1572 except exc.StatementError as err: 

1573 # GH34431 

1574 # https://stackoverflow.com/a/67358288/6067848 

1575 msg = r"""(\(1054, "Unknown column 'inf(e0)?' in 'field list'"\))(?# 

1576 )|inf can not be used with MySQL""" 

1577 err_text = str(err.orig) 

1578 if re.search(msg, err_text): 

1579 raise ValueError("inf cannot be used with MySQL") from err 

1580 raise err 

1581 

1582 

1583def get_engine(engine: str) -> BaseEngine: 

1584 """return our implementation""" 

1585 if engine == "auto": 

1586 engine = get_option("io.sql.engine") 

1587 

1588 if engine == "auto": 

1589 # try engines in this order 

1590 engine_classes = [SQLAlchemyEngine] 

1591 

1592 error_msgs = "" 

1593 for engine_class in engine_classes: 

1594 try: 

1595 return engine_class() 

1596 except ImportError as err: 

1597 error_msgs += "\n - " + str(err) 

1598 

1599 raise ImportError( 

1600 "Unable to find a usable engine; " 

1601 "tried using: 'sqlalchemy'.\n" 

1602 "A suitable version of " 

1603 "sqlalchemy is required for sql I/O " 

1604 "support.\n" 

1605 "Trying to import the above resulted in these errors:" 

1606 f"{error_msgs}" 

1607 ) 

1608 

1609 if engine == "sqlalchemy": 

1610 return SQLAlchemyEngine() 

1611 

1612 raise ValueError("engine must be one of 'auto', 'sqlalchemy'") 

1613 

1614 

1615class SQLDatabase(PandasSQL): 

1616 """ 

1617 This class enables conversion between DataFrame and SQL databases 

1618 using SQLAlchemy to handle DataBase abstraction. 

1619 

1620 Parameters 

1621 ---------- 

1622 con : SQLAlchemy Connectable or URI string. 

1623 Connectable to connect with the database. Using SQLAlchemy makes it 

1624 possible to use any DB supported by that library. 

1625 schema : string, default None 

1626 Name of SQL schema in database to write to (if database flavor 

1627 supports this). If None, use default schema (default). 

1628 need_transaction : bool, default False 

1629 If True, SQLDatabase will create a transaction. 

1630 

1631 """ 

1632 

1633 def __init__( 

1634 self, con, schema: str | None = None, need_transaction: bool = False 

1635 ) -> None: 

1636 from sqlalchemy import create_engine 

1637 from sqlalchemy.engine import Engine 

1638 from sqlalchemy.schema import MetaData 

1639 

1640 # self.exit_stack cleans up the Engine and Connection and commits the 

1641 # transaction if any of those objects was created below. 

1642 # Cleanup happens either in self.__exit__ or at the end of the iterator 

1643 # returned by read_sql when chunksize is not None. 

1644 self.exit_stack = ExitStack() 

1645 if isinstance(con, str): 

1646 con = create_engine(con) 

1647 self.exit_stack.callback(con.dispose) 

1648 if isinstance(con, Engine): 

1649 con = self.exit_stack.enter_context(con.connect()) 

1650 if need_transaction and not con.in_transaction(): 

1651 self.exit_stack.enter_context(con.begin()) 

1652 self.con = con 

1653 self.meta = MetaData(schema=schema) 

1654 self.returns_generator = False 

1655 

1656 def __exit__(self, *args) -> None: 

1657 if not self.returns_generator: 

1658 self.exit_stack.close() 

1659 

1660 @contextmanager 

1661 def run_transaction(self): 

1662 if not self.con.in_transaction(): 

1663 with self.con.begin(): 

1664 yield self.con 

1665 else: 

1666 yield self.con 

1667 

1668 def execute(self, sql: str | Select | TextClause | Delete, params=None): 

1669 """Simple passthrough to SQLAlchemy connectable""" 

1670 from sqlalchemy.exc import SQLAlchemyError 

1671 

1672 args = [] if params is None else [params] 

1673 if isinstance(sql, str): 

1674 execute_function = self.con.exec_driver_sql 

1675 else: 

1676 execute_function = self.con.execute 

1677 

1678 try: 

1679 return execute_function(sql, *args) 

1680 except SQLAlchemyError as exc: 

1681 raise DatabaseError(f"Execution failed on sql '{sql}': {exc}") from exc 

1682 

1683 def read_table( 

1684 self, 

1685 table_name: str, 

1686 index_col: str | list[str] | None = None, 

1687 coerce_float: bool = True, 

1688 parse_dates=None, 

1689 columns=None, 

1690 schema: str | None = None, 

1691 chunksize: int | None = None, 

1692 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1693 ) -> DataFrame | Iterator[DataFrame]: 

1694 """ 

1695 Read SQL database table into a DataFrame. 

1696 

1697 Parameters 

1698 ---------- 

1699 table_name : str 

1700 Name of SQL table in database. 

1701 index_col : string, optional, default: None 

1702 Column to set as index. 

1703 coerce_float : bool, default True 

1704 Attempts to convert values of non-string, non-numeric objects 

1705 (like decimal.Decimal) to floating point. This can result in 

1706 loss of precision. 

1707 parse_dates : list or dict, default: None 

1708 - List of column names to parse as dates. 

1709 - Dict of ``{column_name: format string}`` where format string is 

1710 strftime compatible in case of parsing string times, or is one of 

1711 (D, s, ns, ms, us) in case of parsing integer timestamps. 

1712 - Dict of ``{column_name: arg}``, where the arg corresponds 

1713 to the keyword arguments of :func:`pandas.to_datetime`. 

1714 Especially useful with databases without native Datetime support, 

1715 such as SQLite. 

1716 columns : list, default: None 

1717 List of column names to select from SQL table. 

1718 schema : string, default None 

1719 Name of SQL schema in database to query (if database flavor 

1720 supports this). If specified, this overwrites the default 

1721 schema of the SQL database object. 

1722 chunksize : int, default None 

1723 If specified, return an iterator where `chunksize` is the number 

1724 of rows to include in each chunk. 

1725 dtype_backend : {'numpy_nullable', 'pyarrow'} 

1726 Back-end data type applied to the resultant :class:`DataFrame` 

1727 (still experimental). If not specified, the default behavior 

1728 is to not use nullable data types. If specified, the behavior 

1729 is as follows: 

1730 

1731 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

1732 * ``"pyarrow"``: returns pyarrow-backed nullable 

1733 :class:`ArrowDtype` :class:`DataFrame` 

1734 

1735 .. versionadded:: 2.0 

1736 

1737 Returns 

1738 ------- 

1739 DataFrame 

1740 

1741 See Also 

1742 -------- 

1743 pandas.read_sql_table 

1744 SQLDatabase.read_query 

1745 

1746 """ 

1747 self.meta.reflect(bind=self.con, only=[table_name], views=True) 

1748 table = SQLTable(table_name, self, index=index_col, schema=schema) 

1749 if chunksize is not None: 

1750 self.returns_generator = True 

1751 return table.read( 

1752 self.exit_stack, 

1753 coerce_float=coerce_float, 

1754 parse_dates=parse_dates, 

1755 columns=columns, 

1756 chunksize=chunksize, 

1757 dtype_backend=dtype_backend, 

1758 ) 

1759 

1760 @staticmethod 

1761 def _query_iterator( 

1762 result, 

1763 exit_stack: ExitStack, 

1764 chunksize: int, 

1765 columns, 

1766 index_col=None, 

1767 coerce_float: bool = True, 

1768 parse_dates=None, 

1769 dtype: DtypeArg | None = None, 

1770 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1771 ) -> Generator[DataFrame]: 

1772 """Return generator through chunked result set""" 

1773 has_read_data = False 

1774 with exit_stack: 

1775 while True: 

1776 data = result.fetchmany(chunksize) 

1777 if not data: 

1778 if not has_read_data: 

1779 yield _wrap_result( 

1780 [], 

1781 columns, 

1782 index_col=index_col, 

1783 coerce_float=coerce_float, 

1784 parse_dates=parse_dates, 

1785 dtype=dtype, 

1786 dtype_backend=dtype_backend, 

1787 ) 

1788 break 

1789 

1790 has_read_data = True 

1791 yield _wrap_result( 

1792 data, 

1793 columns, 

1794 index_col=index_col, 

1795 coerce_float=coerce_float, 

1796 parse_dates=parse_dates, 

1797 dtype=dtype, 

1798 dtype_backend=dtype_backend, 

1799 ) 

1800 

1801 def read_query( 

1802 self, 

1803 sql: str, 

1804 index_col: str | list[str] | None = None, 

1805 coerce_float: bool = True, 

1806 parse_dates=None, 

1807 params=None, 

1808 chunksize: int | None = None, 

1809 dtype: DtypeArg | None = None, 

1810 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

1811 ) -> DataFrame | Iterator[DataFrame]: 

1812 """ 

1813 Read SQL query into a DataFrame. 

1814 

1815 Parameters 

1816 ---------- 

1817 sql : str 

1818 SQL query to be executed. 

1819 index_col : string, optional, default: None 

1820 Column name to use as index for the returned DataFrame object. 

1821 coerce_float : bool, default True 

1822 Attempt to convert values of non-string, non-numeric objects (like 

1823 decimal.Decimal) to floating point, useful for SQL result sets. 

1824 params : list, tuple or dict, optional, default: None 

1825 List of parameters to pass to execute method. The syntax used 

1826 to pass parameters is database driver dependent. Check your 

1827 database driver documentation for which of the five syntax styles, 

1828 described in PEP 249's paramstyle, is supported. 

1829 Eg. for psycopg2, uses %(name)s so use params={'name' : 'value'} 

1830 parse_dates : list or dict, default: None 

1831 - List of column names to parse as dates. 

1832 - Dict of ``{column_name: format string}`` where format string is 

1833 strftime compatible in case of parsing string times, or is one of 

1834 (D, s, ns, ms, us) in case of parsing integer timestamps. 

1835 - Dict of ``{column_name: arg dict}``, where the arg dict 

1836 corresponds to the keyword arguments of 

1837 :func:`pandas.to_datetime` Especially useful with databases 

1838 without native Datetime support, such as SQLite. 

1839 chunksize : int, default None 

1840 If specified, return an iterator where `chunksize` is the number 

1841 of rows to include in each chunk. 

1842 dtype : Type name or dict of columns 

1843 Data type for data or columns. E.g. np.float64 or 

1844 {'a': np.float64, 'b': np.int32, 'c': 'Int64'} 

1845 

1846 Returns 

1847 ------- 

1848 DataFrame 

1849 

1850 See Also 

1851 -------- 

1852 read_sql_table : Read SQL database table into a DataFrame. 

1853 read_sql 

1854 

1855 """ 

1856 result = self.execute(sql, params) 

1857 columns = result.keys() 

1858 

1859 if chunksize is not None: 

1860 self.returns_generator = True 

1861 return self._query_iterator( 

1862 result, 

1863 self.exit_stack, 

1864 chunksize, 

1865 columns, 

1866 index_col=index_col, 

1867 coerce_float=coerce_float, 

1868 parse_dates=parse_dates, 

1869 dtype=dtype, 

1870 dtype_backend=dtype_backend, 

1871 ) 

1872 else: 

1873 data = result.fetchall() 

1874 frame = _wrap_result( 

1875 data, 

1876 columns, 

1877 index_col=index_col, 

1878 coerce_float=coerce_float, 

1879 parse_dates=parse_dates, 

1880 dtype=dtype, 

1881 dtype_backend=dtype_backend, 

1882 ) 

1883 return frame 

1884 

1885 read_sql = read_query 

1886 

1887 def prep_table( 

1888 self, 

1889 frame, 

1890 name: str, 

1891 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

1892 index: bool | str | list[str] | None = True, 

1893 index_label=None, 

1894 schema=None, 

1895 dtype: DtypeArg | None = None, 

1896 ) -> SQLTable: 

1897 """ 

1898 Prepares table in the database for data insertion. Creates it if needed, etc. 

1899 """ 

1900 if dtype: 

1901 if not is_dict_like(dtype): 

1902 # error: Value expression in dictionary comprehension has incompatible 

1903 # type "Union[ExtensionDtype, str, dtype[Any], Type[object], 

1904 # Dict[Hashable, Union[ExtensionDtype, Union[str, dtype[Any]], 

1905 # Type[str], Type[float], Type[int], Type[complex], Type[bool], 

1906 # Type[object]]]]"; expected type "Union[ExtensionDtype, str, 

1907 # dtype[Any], Type[object]]" 

1908 dtype = dict.fromkeys(frame, dtype) # type: ignore[arg-type] 

1909 else: 

1910 dtype = cast(dict, dtype) 

1911 

1912 from sqlalchemy.types import TypeEngine 

1913 

1914 for col, my_type in dtype.items(): 

1915 if isinstance(my_type, type) and issubclass(my_type, TypeEngine): 

1916 pass 

1917 elif isinstance(my_type, TypeEngine): 

1918 pass 

1919 else: 

1920 raise ValueError(f"The type of {col} is not a SQLAlchemy type") 

1921 

1922 table = SQLTable( 

1923 name, 

1924 self, 

1925 frame=frame, 

1926 index=index, 

1927 if_exists=if_exists, 

1928 index_label=index_label, 

1929 schema=schema, 

1930 dtype=dtype, 

1931 ) 

1932 table.create() 

1933 return table 

1934 

1935 def check_case_sensitive( 

1936 self, 

1937 name: str, 

1938 schema: str | None, 

1939 ) -> None: 

1940 """ 

1941 Checks table name for issues with case-sensitivity. 

1942 Method is called after data is inserted. 

1943 """ 

1944 if not name.isdigit() and not name.islower(): 

1945 # check for potentially case sensitivity issues (GH7815) 

1946 # Only check when name is not a number and name is not lower case 

1947 from sqlalchemy import inspect as sqlalchemy_inspect 

1948 

1949 insp = sqlalchemy_inspect(self.con) 

1950 table_names = insp.get_table_names(schema=schema or self.meta.schema) 

1951 if name not in table_names: 

1952 msg = ( 

1953 f"The provided table name '{name}' is not found exactly as " 

1954 "such in the database after writing the table, possibly " 

1955 "due to case sensitivity issues. Consider using lower " 

1956 "case table names." 

1957 ) 

1958 warnings.warn( 

1959 msg, 

1960 UserWarning, 

1961 stacklevel=find_stack_level(), 

1962 ) 

1963 

1964 def to_sql( 

1965 self, 

1966 frame, 

1967 name: str, 

1968 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

1969 index: bool = True, 

1970 index_label=None, 

1971 schema: str | None = None, 

1972 chunksize: int | None = None, 

1973 dtype: DtypeArg | None = None, 

1974 method: Literal["multi"] | Callable | None = None, 

1975 engine: str = "auto", 

1976 **engine_kwargs, 

1977 ) -> int | None: 

1978 """ 

1979 Write records stored in a DataFrame to a SQL database. 

1980 

1981 Parameters 

1982 ---------- 

1983 frame : DataFrame 

1984 name : string 

1985 Name of SQL table. 

1986 if_exists : {'fail', 'replace', 'append', 'delete_rows'}, default 'fail' 

1987 - fail: If table exists, do nothing. 

1988 - replace: If table exists, drop it, recreate it, and insert data. 

1989 - append: If table exists, insert data. Create if does not exist. 

1990 - delete_rows: If a table exists, delete all records and insert data. 

1991 index : boolean, default True 

1992 Write DataFrame index as a column. 

1993 index_label : string or sequence, default None 

1994 Column label for index column(s). If None is given (default) and 

1995 `index` is True, then the index names are used. 

1996 A sequence should be given if the DataFrame uses MultiIndex. 

1997 schema : string, default None 

1998 Name of SQL schema in database to write to (if database flavor 

1999 supports this). If specified, this overwrites the default 

2000 schema of the SQLDatabase object. 

2001 chunksize : int, default None 

2002 If not None, then rows will be written in batches of this size at a 

2003 time. If None, all rows will be written at once. 

2004 dtype : single type or dict of column name to SQL type, default None 

2005 Optional specifying the datatype for columns. The SQL type should 

2006 be a SQLAlchemy type. If all columns are of the same type, one 

2007 single value can be used. 

2008 method : {None', 'multi', callable}, default None 

2009 Controls the SQL insertion clause used: 

2010 

2011 * None : Uses standard SQL ``INSERT`` clause (one per row). 

2012 * 'multi': Pass multiple values in a single ``INSERT`` clause. 

2013 * callable with signature ``(pd_table, conn, keys, data_iter)``. 

2014 

2015 Details and a sample callable implementation can be found in the 

2016 section :ref:`insert method <io.sql.method>`. 

2017 engine : {'auto', 'sqlalchemy'}, default 'auto' 

2018 SQL engine library to use. If 'auto', then the option 

2019 ``io.sql.engine`` is used. The default ``io.sql.engine`` 

2020 behavior is 'sqlalchemy' 

2021 

2022 **engine_kwargs 

2023 Any additional kwargs are passed to the engine. 

2024 """ 

2025 sql_engine = get_engine(engine) 

2026 

2027 table = self.prep_table( 

2028 frame=frame, 

2029 name=name, 

2030 if_exists=if_exists, 

2031 index=index, 

2032 index_label=index_label, 

2033 schema=schema, 

2034 dtype=dtype, 

2035 ) 

2036 

2037 total_inserted = sql_engine.insert_records( 

2038 table=table, 

2039 con=self.con, 

2040 frame=frame, 

2041 name=name, 

2042 index=index, 

2043 schema=schema, 

2044 chunksize=chunksize, 

2045 method=method, 

2046 **engine_kwargs, 

2047 ) 

2048 

2049 self.check_case_sensitive(name=name, schema=schema) 

2050 return total_inserted 

2051 

2052 @property 

2053 def tables(self): 

2054 return self.meta.tables 

2055 

2056 def has_table(self, name: str, schema: str | None = None) -> bool: 

2057 from sqlalchemy import inspect as sqlalchemy_inspect 

2058 

2059 insp = sqlalchemy_inspect(self.con) 

2060 return insp.has_table(name, schema or self.meta.schema) 

2061 

2062 def get_table(self, table_name: str, schema: str | None = None) -> Table: 

2063 from sqlalchemy import ( 

2064 Numeric, 

2065 Table, 

2066 ) 

2067 

2068 schema = schema or self.meta.schema 

2069 tbl = Table(table_name, self.meta, autoload_with=self.con, schema=schema) 

2070 for column in tbl.columns: 

2071 if isinstance(column.type, Numeric): 

2072 column.type.asdecimal = False 

2073 return tbl 

2074 

2075 def drop_table(self, table_name: str, schema: str | None = None) -> None: 

2076 schema = schema or self.meta.schema 

2077 if self.has_table(table_name, schema): 

2078 self.meta.reflect( 

2079 bind=self.con, only=[table_name], schema=schema, views=True 

2080 ) 

2081 with self.run_transaction(): 

2082 self.get_table(table_name, schema).drop(bind=self.con) 

2083 self.meta.clear() 

2084 

2085 def delete_rows(self, table_name: str, schema: str | None = None) -> None: 

2086 schema = schema or self.meta.schema 

2087 if self.has_table(table_name, schema): 

2088 self.meta.reflect( 

2089 bind=self.con, only=[table_name], schema=schema, views=True 

2090 ) 

2091 table = self.get_table(table_name, schema) 

2092 self.execute(table.delete()).close() 

2093 self.meta.clear() 

2094 

2095 def _create_sql_schema( 

2096 self, 

2097 frame: DataFrame, 

2098 table_name: str, 

2099 keys: list[str] | None = None, 

2100 dtype: DtypeArg | None = None, 

2101 schema: str | None = None, 

2102 ) -> str: 

2103 table = SQLTable( 

2104 table_name, 

2105 self, 

2106 frame=frame, 

2107 index=False, 

2108 keys=keys, 

2109 dtype=dtype, 

2110 schema=schema, 

2111 ) 

2112 return str(table.sql_schema()) 

2113 

2114 

2115# ---- SQL without SQLAlchemy --- 

2116 

2117 

2118class ADBCDatabase(PandasSQL): 

2119 """ 

2120 This class enables conversion between DataFrame and SQL databases 

2121 using ADBC to handle DataBase abstraction. 

2122 

2123 Parameters 

2124 ---------- 

2125 con : adbc_driver_manager.dbapi.Connection 

2126 """ 

2127 

2128 def __init__(self, con) -> None: 

2129 self.con = con 

2130 

2131 @contextmanager 

2132 def run_transaction(self): 

2133 with self.con.cursor() as cur: 

2134 try: 

2135 yield cur 

2136 except Exception: 

2137 self.con.rollback() 

2138 raise 

2139 self.con.commit() 

2140 

2141 def execute(self, sql: str | Select | TextClause, params=None): 

2142 from adbc_driver_manager import Error 

2143 

2144 if not isinstance(sql, str): 

2145 raise TypeError("Query must be a string unless using sqlalchemy.") 

2146 args = [] if params is None else [params] 

2147 cur = self.con.cursor() 

2148 try: 

2149 cur.execute(sql, *args) 

2150 return cur 

2151 except Error as exc: 

2152 try: 

2153 self.con.rollback() 

2154 except Error as inner_exc: # pragma: no cover 

2155 ex = DatabaseError( 

2156 f"Execution failed on sql: {sql}\n{exc}\nunable to rollback" 

2157 ) 

2158 raise ex from inner_exc 

2159 

2160 ex = DatabaseError(f"Execution failed on sql '{sql}': {exc}") 

2161 raise ex from exc 

2162 

2163 def read_table( 

2164 self, 

2165 table_name: str, 

2166 index_col: str | list[str] | None = None, 

2167 coerce_float: bool = True, 

2168 parse_dates=None, 

2169 columns=None, 

2170 schema: str | None = None, 

2171 chunksize: int | None = None, 

2172 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

2173 ) -> DataFrame | Iterator[DataFrame]: 

2174 """ 

2175 Read SQL database table into a DataFrame. 

2176 

2177 Parameters 

2178 ---------- 

2179 table_name : str 

2180 Name of SQL table in database. 

2181 coerce_float : bool, default True 

2182 Raises NotImplementedError 

2183 parse_dates : list or dict, default: None 

2184 - List of column names to parse as dates. 

2185 - Dict of ``{column_name: format string}`` where format string is 

2186 strftime compatible in case of parsing string times, or is one of 

2187 (D, s, ns, ms, us) in case of parsing integer timestamps. 

2188 - Dict of ``{column_name: arg}``, where the arg corresponds 

2189 to the keyword arguments of :func:`pandas.to_datetime`. 

2190 Especially useful with databases without native Datetime support, 

2191 such as SQLite. 

2192 columns : list, default: None 

2193 List of column names to select from SQL table. 

2194 schema : string, default None 

2195 Name of SQL schema in database to query (if database flavor 

2196 supports this). If specified, this overwrites the default 

2197 schema of the SQL database object. 

2198 chunksize : int, default None 

2199 Raises NotImplementedError 

2200 dtype_backend : {'numpy_nullable', 'pyarrow'} 

2201 Back-end data type applied to the resultant :class:`DataFrame` 

2202 (still experimental). If not specified, the default behavior 

2203 is to not use nullable data types. If specified, the behavior 

2204 is as follows: 

2205 

2206 * ``"numpy_nullable"``: returns nullable-dtype-backed :class:`DataFrame` 

2207 * ``"pyarrow"``: returns pyarrow-backed nullable 

2208 :class:`ArrowDtype` :class:`DataFrame` 

2209 

2210 .. versionadded:: 2.0 

2211 

2212 Returns 

2213 ------- 

2214 DataFrame 

2215 

2216 See Also 

2217 -------- 

2218 pandas.read_sql_table 

2219 SQLDatabase.read_query 

2220 

2221 """ 

2222 if coerce_float is not True: 

2223 raise NotImplementedError( 

2224 "'coerce_float' is not implemented for ADBC drivers" 

2225 ) 

2226 if chunksize: 

2227 raise NotImplementedError("'chunksize' is not implemented for ADBC drivers") 

2228 

2229 if columns: 

2230 if index_col: 

2231 index_select = maybe_make_list(index_col) 

2232 else: 

2233 index_select = [] 

2234 to_select = index_select + columns 

2235 select_list = ", ".join(_quote_identifier(x) for x in to_select) 

2236 else: 

2237 select_list = "*" 

2238 quoted_table_name = _quote_identifier(table_name) 

2239 if schema: 

2240 quoted_schema = _quote_identifier(schema) 

2241 stmt = f"SELECT {select_list} FROM {quoted_schema}.{quoted_table_name}" 

2242 else: 

2243 stmt = f"SELECT {select_list} FROM {quoted_table_name}" 

2244 

2245 with self.execute(stmt) as cur: 

2246 pa_table = cur.fetch_arrow_table() 

2247 df = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) 

2248 

2249 return _wrap_result_adbc( 

2250 df, 

2251 index_col=index_col, 

2252 parse_dates=parse_dates, 

2253 ) 

2254 

2255 def read_query( 

2256 self, 

2257 sql: str, 

2258 index_col: str | list[str] | None = None, 

2259 coerce_float: bool = True, 

2260 parse_dates=None, 

2261 params=None, 

2262 chunksize: int | None = None, 

2263 dtype: DtypeArg | None = None, 

2264 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

2265 ) -> DataFrame | Iterator[DataFrame]: 

2266 """ 

2267 Read SQL query into a DataFrame. 

2268 

2269 Parameters 

2270 ---------- 

2271 sql : str 

2272 SQL query to be executed. 

2273 index_col : string, optional, default: None 

2274 Column name to use as index for the returned DataFrame object. 

2275 coerce_float : bool, default True 

2276 Raises NotImplementedError 

2277 params : list, tuple or dict, optional, default: None 

2278 Raises NotImplementedError 

2279 parse_dates : list or dict, default: None 

2280 - List of column names to parse as dates. 

2281 - Dict of ``{column_name: format string}`` where format string is 

2282 strftime compatible in case of parsing string times, or is one of 

2283 (D, s, ns, ms, us) in case of parsing integer timestamps. 

2284 - Dict of ``{column_name: arg dict}``, where the arg dict 

2285 corresponds to the keyword arguments of 

2286 :func:`pandas.to_datetime` Especially useful with databases 

2287 without native Datetime support, such as SQLite. 

2288 chunksize : int, default None 

2289 Raises NotImplementedError 

2290 dtype : Type name or dict of columns 

2291 Data type for data or columns. E.g. np.float64 or 

2292 {'a': np.float64, 'b': np.int32, 'c': 'Int64'} 

2293 

2294 Returns 

2295 ------- 

2296 DataFrame 

2297 

2298 See Also 

2299 -------- 

2300 read_sql_table : Read SQL database table into a DataFrame. 

2301 read_sql 

2302 

2303 """ 

2304 if coerce_float is not True: 

2305 raise NotImplementedError( 

2306 "'coerce_float' is not implemented for ADBC drivers" 

2307 ) 

2308 if params: 

2309 raise NotImplementedError("'params' is not implemented for ADBC drivers") 

2310 if chunksize: 

2311 raise NotImplementedError("'chunksize' is not implemented for ADBC drivers") 

2312 

2313 with self.execute(sql) as cur: 

2314 pa_table = cur.fetch_arrow_table() 

2315 df = arrow_table_to_pandas(pa_table, dtype_backend=dtype_backend) 

2316 

2317 return _wrap_result_adbc( 

2318 df, 

2319 index_col=index_col, 

2320 parse_dates=parse_dates, 

2321 dtype=dtype, 

2322 ) 

2323 

2324 read_sql = read_query 

2325 

2326 def to_sql( 

2327 self, 

2328 frame, 

2329 name: str, 

2330 if_exists: Literal["fail", "replace", "append", "delete_rows"] = "fail", 

2331 index: bool = True, 

2332 index_label=None, 

2333 schema: str | None = None, 

2334 chunksize: int | None = None, 

2335 dtype: DtypeArg | None = None, 

2336 method: Literal["multi"] | Callable | None = None, 

2337 engine: str = "auto", 

2338 **engine_kwargs, 

2339 ) -> int | None: 

2340 """ 

2341 Write records stored in a DataFrame to a SQL database. 

2342 

2343 Parameters 

2344 ---------- 

2345 frame : DataFrame 

2346 name : string 

2347 Name of SQL table. 

2348 if_exists : {'fail', 'replace', 'append'}, default 'fail' 

2349 - fail: If table exists, do nothing. 

2350 - replace: If table exists, drop it, recreate it, and insert data. 

2351 - append: If table exists, insert data. Create if does not exist. 

2352 - delete_rows: If a table exists, delete all records and insert data. 

2353 index : boolean, default True 

2354 Write DataFrame index as a column. 

2355 index_label : string or sequence, default None 

2356 Raises NotImplementedError 

2357 schema : string, default None 

2358 Name of SQL schema in database to write to (if database flavor 

2359 supports this). If specified, this overwrites the default 

2360 schema of the SQLDatabase object. 

2361 chunksize : int, default None 

2362 Raises NotImplementedError 

2363 dtype : single type or dict of column name to SQL type, default None 

2364 Raises NotImplementedError 

2365 method : {None', 'multi', callable}, default None 

2366 Raises NotImplementedError 

2367 engine : {'auto', 'sqlalchemy'}, default 'auto' 

2368 Raises NotImplementedError if not set to 'auto' 

2369 """ 

2370 pa = import_optional_dependency("pyarrow") 

2371 from adbc_driver_manager import Error 

2372 

2373 if index_label: 

2374 raise NotImplementedError( 

2375 "'index_label' is not implemented for ADBC drivers" 

2376 ) 

2377 if chunksize: 

2378 raise NotImplementedError("'chunksize' is not implemented for ADBC drivers") 

2379 if dtype: 

2380 raise NotImplementedError("'dtype' is not implemented for ADBC drivers") 

2381 if method: 

2382 raise NotImplementedError("'method' is not implemented for ADBC drivers") 

2383 if engine != "auto": 

2384 raise NotImplementedError( 

2385 "engine != 'auto' not implemented for ADBC drivers" 

2386 ) 

2387 

2388 quoted_name = _quote_identifier(name) 

2389 if schema: 

2390 quoted_schema = _quote_identifier(schema) 

2391 quoted_table_name = f"{quoted_schema}.{quoted_name}" 

2392 else: 

2393 quoted_table_name = quoted_name 

2394 

2395 # pandas if_exists="append" will still create the 

2396 # table if it does not exist; ADBC is more explicit with append/create 

2397 # as applicable modes, so the semantics get blurred across 

2398 # the libraries 

2399 mode = "create" 

2400 if self.has_table(name, schema): 

2401 if if_exists == "fail": 

2402 raise ValueError(f"Table '{name}' already exists.") 

2403 elif if_exists == "replace": 

2404 sql_statement = f"DROP TABLE {quoted_table_name}" 

2405 self.execute(sql_statement).close() 

2406 elif if_exists == "append": 

2407 mode = "append" 

2408 elif if_exists == "delete_rows": 

2409 mode = "append" 

2410 self.delete_rows(name, schema) 

2411 

2412 try: 

2413 tbl = pa.Table.from_pandas(frame, preserve_index=index) 

2414 except pa.ArrowNotImplementedError as exc: 

2415 raise ValueError("datatypes not supported") from exc 

2416 

2417 with self.con.cursor() as cur: 

2418 try: 

2419 total_inserted = cur.adbc_ingest( 

2420 table_name=name, data=tbl, mode=mode, db_schema_name=schema 

2421 ) 

2422 except Error as exc: 

2423 raise DatabaseError( 

2424 f"Failed to insert records on table={name} with {mode=}" 

2425 ) from exc 

2426 

2427 self.con.commit() 

2428 return total_inserted 

2429 

2430 def has_table(self, name: str, schema: str | None = None) -> bool: 

2431 meta = self.con.adbc_get_objects( 

2432 db_schema_filter=schema, table_name_filter=name 

2433 ).read_all() 

2434 

2435 for catalog_schema in meta["catalog_db_schemas"].to_pylist(): 

2436 if not catalog_schema: 

2437 continue 

2438 for schema_record in catalog_schema: 

2439 if not schema_record: 

2440 continue 

2441 

2442 for table_record in schema_record["db_schema_tables"]: 

2443 if table_record["table_name"] == name: 

2444 return True 

2445 

2446 return False 

2447 

2448 def delete_rows(self, name: str, schema: str | None = None) -> None: 

2449 quoted_table_name = _quote_identifier(name) 

2450 if schema: 

2451 quoted_schema = _quote_identifier(schema) 

2452 quoted_table_name = f"{quoted_schema}.{quoted_table_name}" 

2453 

2454 if self.has_table(name, schema): 

2455 self.execute(f"DELETE FROM {quoted_table_name}").close() 

2456 

2457 def _create_sql_schema( 

2458 self, 

2459 frame: DataFrame, 

2460 table_name: str, 

2461 keys: list[str] | None = None, 

2462 dtype: DtypeArg | None = None, 

2463 schema: str | None = None, 

2464 ) -> str: 

2465 raise NotImplementedError("not implemented for adbc") 

2466 

2467 

2468# sqlite-specific sql strings and handler class 

2469# dictionary used for readability purposes 

2470_SQL_TYPES = { 

2471 "string": "TEXT", 

2472 "floating": "REAL", 

2473 "integer": "INTEGER", 

2474 "datetime": "TIMESTAMP", 

2475 "date": "DATE", 

2476 "time": "TIME", 

2477 "boolean": "INTEGER", 

2478} 

2479 

2480 

2481def _get_unicode_name(name: object) -> str: 

2482 try: 

2483 uname = str(name).encode("utf-8", "strict").decode("utf-8") 

2484 except UnicodeError as err: 

2485 raise ValueError(f"Cannot convert identifier to UTF-8: '{name}'") from err 

2486 return uname 

2487 

2488 

2489def _get_valid_sqlite_name(name: object) -> str: 

2490 # See https://stackoverflow.com/questions/6514274/how-do-you-escape-strings\ 

2491 # -for-sqlite-table-column-names-in-python 

2492 # Ensure the string can be encoded as UTF-8. 

2493 # Ensure the string does not include any NUL characters. 

2494 # Replace all " with "". 

2495 # Wrap the entire thing in double quotes. 

2496 

2497 uname = _get_unicode_name(name) 

2498 if not len(uname): 

2499 raise ValueError("Empty table or column name specified") 

2500 

2501 nul_index = uname.find("\x00") 

2502 if nul_index >= 0: 

2503 raise ValueError("SQLite identifier cannot contain NULs") 

2504 return '"' + uname.replace('"', '""') + '"' 

2505 

2506 

2507def _quote_identifier(name: object) -> str: 

2508 """ 

2509 Escape a SQL identifier (table name or schema name) for safe use in 

2510 ADBC-generated SQL statements. 

2511 

2512 Uses ANSI SQL double-quote escaping, which is supported by PostgreSQL, 

2513 DuckDB, SQLite, and other databases targeted by ADBC drivers. 

2514 

2515 Parameters 

2516 ---------- 

2517 name : object 

2518 Identifier to escape. 

2519 

2520 Returns 

2521 ------- 

2522 str 

2523 The identifier wrapped in double quotes with any internal double 

2524 quotes doubled, e.g. ``my"table`` becomes ``"my""table"``. 

2525 

2526 Raises 

2527 ------ 

2528 ValueError 

2529 If *name* is empty or contains a NUL character. 

2530 """ 

2531 uname = _get_unicode_name(name) 

2532 if not len(uname): 

2533 raise ValueError("Empty table or column name specified") 

2534 

2535 nul_index = uname.find("\x00") 

2536 if nul_index >= 0: 

2537 raise ValueError("SQL identifier cannot contain NUL characters") 

2538 

2539 return '"' + uname.replace('"', '""') + '"' 

2540 

2541 

2542class SQLiteTable(SQLTable): 

2543 """ 

2544 Patch the SQLTable for fallback support. 

2545 Instead of a table variable just use the Create Table statement. 

2546 """ 

2547 

2548 def __init__(self, *args, **kwargs) -> None: 

2549 super().__init__(*args, **kwargs) 

2550 

2551 self._register_date_adapters() 

2552 

2553 def _register_date_adapters(self) -> None: 

2554 # GH 8341 

2555 # register an adapter callable for datetime.time object 

2556 import sqlite3 

2557 

2558 # this will transform time(12,34,56,789) into '12:34:56.000789' 

2559 # (this is what sqlalchemy does) 

2560 def _adapt_time(t) -> str: 

2561 # This is faster than strftime 

2562 return f"{t.hour:02d}:{t.minute:02d}:{t.second:02d}.{t.microsecond:06d}" 

2563 

2564 # Also register adapters for date/datetime and co 

2565 # xref https://docs.python.org/3.12/library/sqlite3.html#adapter-and-converter-recipes 

2566 # Python 3.12+ doesn't auto-register adapters for us anymore 

2567 

2568 adapt_date_iso = lambda val: val.isoformat() 

2569 adapt_datetime_iso = lambda val: val.isoformat(" ") 

2570 

2571 sqlite3.register_adapter(time, _adapt_time) 

2572 

2573 sqlite3.register_adapter(date, adapt_date_iso) 

2574 sqlite3.register_adapter(datetime, adapt_datetime_iso) 

2575 

2576 convert_date = lambda val: date.fromisoformat(val.decode()) 

2577 convert_timestamp = lambda val: datetime.fromisoformat(val.decode()) 

2578 

2579 sqlite3.register_converter("date", convert_date) 

2580 sqlite3.register_converter("timestamp", convert_timestamp) 

2581 

2582 def sql_schema(self) -> str: 

2583 return str(";\n".join(self.table)) 

2584 

2585 def _execute_create(self) -> None: 

2586 with self.pd_sql.run_transaction() as cur: 

2587 for stmt in self.table: 

2588 cur.execute(stmt) 

2589 

2590 def insert_statement(self, *, num_rows: int) -> str: 

2591 names = list(map(str, self.frame.columns)) 

2592 wld = "?" # wildcard char 

2593 escape = _get_valid_sqlite_name 

2594 

2595 if self.index is not None: 

2596 for idx in self.index[::-1]: 

2597 names.insert(0, idx) 

2598 

2599 bracketed_names = [escape(column) for column in names] 

2600 col_names = ",".join(bracketed_names) 

2601 

2602 row_wildcards = ",".join([wld] * len(names)) 

2603 wildcards = ",".join([f"({row_wildcards})" for _ in range(num_rows)]) 

2604 insert_statement = ( 

2605 f"INSERT INTO {escape(self.name)} ({col_names}) VALUES {wildcards}" 

2606 ) 

2607 return insert_statement 

2608 

2609 def _execute_insert(self, conn, keys, data_iter) -> int: 

2610 from sqlite3 import Error 

2611 

2612 data_list = list(data_iter) 

2613 try: 

2614 conn.executemany(self.insert_statement(num_rows=1), data_list) 

2615 except Error as exc: 

2616 raise DatabaseError("Execution failed") from exc 

2617 return conn.rowcount 

2618 

2619 def _execute_insert_multi(self, conn, keys, data_iter) -> int: 

2620 data_list = list(data_iter) 

2621 flattened_data = [x for row in data_list for x in row] 

2622 conn.execute(self.insert_statement(num_rows=len(data_list)), flattened_data) 

2623 return conn.rowcount 

2624 

2625 def _create_table_setup(self): 

2626 """ 

2627 Return a list of SQL statements that creates a table reflecting the 

2628 structure of a DataFrame. The first entry will be a CREATE TABLE 

2629 statement while the rest will be CREATE INDEX statements. 

2630 """ 

2631 column_names_and_types = self._get_column_names_and_types(self._sql_type_name) 

2632 escape = _get_valid_sqlite_name 

2633 

2634 create_tbl_stmts = [ 

2635 escape(cname) + " " + ctype for cname, ctype, _ in column_names_and_types 

2636 ] 

2637 

2638 if self.keys is not None and len(self.keys): 

2639 if not is_list_like(self.keys): 

2640 keys = [self.keys] 

2641 else: 

2642 keys = self.keys 

2643 cnames_br = ", ".join([escape(c) for c in keys]) 

2644 create_tbl_stmts.append( 

2645 f"CONSTRAINT {self.name}_pk PRIMARY KEY ({cnames_br})" 

2646 ) 

2647 if self.schema: 

2648 schema_name = self.schema + "." 

2649 else: 

2650 schema_name = "" 

2651 create_stmts = [ 

2652 "CREATE TABLE " 

2653 + schema_name 

2654 + escape(self.name) 

2655 + " (\n" 

2656 + ",\n ".join(create_tbl_stmts) 

2657 + "\n)" 

2658 ] 

2659 

2660 ix_cols = [cname for cname, _, is_index in column_names_and_types if is_index] 

2661 if ix_cols: 

2662 cnames = "_".join(ix_cols) 

2663 cnames_br = ",".join([escape(c) for c in ix_cols]) 

2664 create_stmts.append( 

2665 "CREATE INDEX " 

2666 + escape("ix_" + self.name + "_" + cnames) 

2667 + "ON " 

2668 + escape(self.name) 

2669 + " (" 

2670 + cnames_br 

2671 + ")" 

2672 ) 

2673 

2674 return create_stmts 

2675 

2676 def _sql_type_name(self, col): 

2677 dtype: DtypeArg = self.dtype or {} 

2678 if is_dict_like(dtype): 

2679 dtype = cast(dict, dtype) 

2680 if col.name in dtype: 

2681 return dtype[col.name] 

2682 

2683 # Infer type of column, while ignoring missing values. 

2684 # Needed for inserting typed data containing NULLs, GH 8778. 

2685 col_type = lib.infer_dtype(col, skipna=True) 

2686 

2687 if col_type == "timedelta64": 

2688 warnings.warn( 

2689 "the 'timedelta' type is not supported, and will be " 

2690 "written as integer values (ns frequency) to the database.", 

2691 UserWarning, 

2692 stacklevel=find_stack_level(), 

2693 ) 

2694 col_type = "integer" 

2695 

2696 elif col_type == "datetime64": 

2697 col_type = "datetime" 

2698 

2699 elif col_type == "empty": 

2700 col_type = "string" 

2701 

2702 elif col_type == "complex": 

2703 raise ValueError("Complex datatypes not supported") 

2704 

2705 if col_type not in _SQL_TYPES: 

2706 col_type = "string" 

2707 

2708 return _SQL_TYPES[col_type] 

2709 

2710 

2711class SQLiteDatabase(PandasSQL): 

2712 """ 

2713 Version of SQLDatabase to support SQLite connections (fallback without 

2714 SQLAlchemy). This should only be used internally. 

2715 

2716 Parameters 

2717 ---------- 

2718 con : sqlite connection object 

2719 

2720 """ 

2721 

2722 def __init__(self, con) -> None: 

2723 self.con = con 

2724 

2725 @contextmanager 

2726 def run_transaction(self): 

2727 cur = self.con.cursor() 

2728 try: 

2729 yield cur 

2730 self.con.commit() 

2731 except Exception: 

2732 self.con.rollback() 

2733 raise 

2734 finally: 

2735 cur.close() 

2736 

2737 def execute(self, sql: str | Select | TextClause, params=None): 

2738 from sqlite3 import Error 

2739 

2740 if not isinstance(sql, str): 

2741 raise TypeError("Query must be a string unless using sqlalchemy.") 

2742 args = [] if params is None else [params] 

2743 cur = self.con.cursor() 

2744 try: 

2745 cur.execute(sql, *args) 

2746 return cur 

2747 except Error as exc: 

2748 try: 

2749 self.con.rollback() 

2750 except Error as inner_exc: # pragma: no cover 

2751 ex = DatabaseError( 

2752 f"Execution failed on sql: {sql}\n{exc}\nunable to rollback" 

2753 ) 

2754 raise ex from inner_exc 

2755 

2756 ex = DatabaseError(f"Execution failed on sql '{sql}': {exc}") 

2757 raise ex from exc 

2758 

2759 @staticmethod 

2760 def _query_iterator( 

2761 cursor, 

2762 chunksize: int, 

2763 columns, 

2764 index_col=None, 

2765 coerce_float: bool = True, 

2766 parse_dates=None, 

2767 dtype: DtypeArg | None = None, 

2768 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

2769 ) -> Generator[DataFrame]: 

2770 """Return generator through chunked result set""" 

2771 has_read_data = False 

2772 while True: 

2773 data = cursor.fetchmany(chunksize) 

2774 if type(data) == tuple: 

2775 data = list(data) 

2776 if not data: 

2777 cursor.close() 

2778 if not has_read_data: 

2779 result = DataFrame.from_records( 

2780 [], columns=columns, coerce_float=coerce_float 

2781 ) 

2782 if dtype: 

2783 result = result.astype(dtype) 

2784 yield result 

2785 break 

2786 

2787 has_read_data = True 

2788 yield _wrap_result( 

2789 data, 

2790 columns, 

2791 index_col=index_col, 

2792 coerce_float=coerce_float, 

2793 parse_dates=parse_dates, 

2794 dtype=dtype, 

2795 dtype_backend=dtype_backend, 

2796 ) 

2797 

2798 def read_query( 

2799 self, 

2800 sql, 

2801 index_col=None, 

2802 coerce_float: bool = True, 

2803 parse_dates=None, 

2804 params=None, 

2805 chunksize: int | None = None, 

2806 dtype: DtypeArg | None = None, 

2807 dtype_backend: DtypeBackend | Literal["numpy"] = "numpy", 

2808 ) -> DataFrame | Iterator[DataFrame]: 

2809 cursor = self.execute(sql, params) 

2810 columns = [col_desc[0] for col_desc in cursor.description] 

2811 

2812 if chunksize is not None: 

2813 return self._query_iterator( 

2814 cursor, 

2815 chunksize, 

2816 columns, 

2817 index_col=index_col, 

2818 coerce_float=coerce_float, 

2819 parse_dates=parse_dates, 

2820 dtype=dtype, 

2821 dtype_backend=dtype_backend, 

2822 ) 

2823 else: 

2824 data = self._fetchall_as_list(cursor) 

2825 cursor.close() 

2826 

2827 frame = _wrap_result( 

2828 data, 

2829 columns, 

2830 index_col=index_col, 

2831 coerce_float=coerce_float, 

2832 parse_dates=parse_dates, 

2833 dtype=dtype, 

2834 dtype_backend=dtype_backend, 

2835 ) 

2836 return frame 

2837 

2838 def _fetchall_as_list(self, cur): 

2839 result = cur.fetchall() 

2840 if not isinstance(result, list): 

2841 result = list(result) 

2842 return result 

2843 

2844 def to_sql( 

2845 self, 

2846 frame, 

2847 name: str, 

2848 if_exists: str = "fail", 

2849 index: bool = True, 

2850 index_label=None, 

2851 schema=None, 

2852 chunksize: int | None = None, 

2853 dtype: DtypeArg | None = None, 

2854 method: Literal["multi"] | Callable | None = None, 

2855 engine: str = "auto", 

2856 **engine_kwargs, 

2857 ) -> int | None: 

2858 """ 

2859 Write records stored in a DataFrame to a SQL database. 

2860 

2861 Parameters 

2862 ---------- 

2863 frame: DataFrame 

2864 name: string 

2865 Name of SQL table. 

2866 if_exists: {'fail', 'replace', 'append', 'delete_rows'}, default 'fail' 

2867 fail: If table exists, do nothing. 

2868 replace: If table exists, drop it, recreate it, and insert data. 

2869 append: If table exists, insert data. Create if it does not exist. 

2870 delete_rows: If a table exists, delete all records and insert data. 

2871 index : bool, default True 

2872 Write DataFrame index as a column 

2873 index_label : string or sequence, default None 

2874 Column label for index column(s). If None is given (default) and 

2875 `index` is True, then the index names are used. 

2876 A sequence should be given if the DataFrame uses MultiIndex. 

2877 schema : string, default None 

2878 Ignored parameter included for compatibility with SQLAlchemy 

2879 version of ``to_sql``. 

2880 chunksize : int, default None 

2881 If not None, then rows will be written in batches of this 

2882 size at a time. If None, all rows will be written at once. 

2883 dtype : single type or dict of column name to SQL type, default None 

2884 Optional specifying the datatype for columns. The SQL type should 

2885 be a string. If all columns are of the same type, one single value 

2886 can be used. 

2887 method : {None, 'multi', callable}, default None 

2888 Controls the SQL insertion clause used: 

2889 

2890 * None : Uses standard SQL ``INSERT`` clause (one per row). 

2891 * 'multi': Pass multiple values in a single ``INSERT`` clause. 

2892 * callable with signature ``(pd_table, conn, keys, data_iter)``. 

2893 

2894 Details and a sample callable implementation can be found in the 

2895 section :ref:`insert method <io.sql.method>`. 

2896 """ 

2897 if dtype: 

2898 if not is_dict_like(dtype): 

2899 # error: Value expression in dictionary comprehension has incompatible 

2900 # type "Union[ExtensionDtype, str, dtype[Any], Type[object], 

2901 # Dict[Hashable, Union[ExtensionDtype, Union[str, dtype[Any]], 

2902 # Type[str], Type[float], Type[int], Type[complex], Type[bool], 

2903 # Type[object]]]]"; expected type "Union[ExtensionDtype, str, 

2904 # dtype[Any], Type[object]]" 

2905 dtype = dict.fromkeys(frame, dtype) # type: ignore[arg-type] 

2906 else: 

2907 dtype = cast(dict, dtype) 

2908 

2909 for col, my_type in dtype.items(): 

2910 if not isinstance(my_type, str): 

2911 raise ValueError(f"{col} ({my_type}) not a string") 

2912 

2913 table = SQLiteTable( 

2914 name, 

2915 self, 

2916 frame=frame, 

2917 index=index, 

2918 if_exists=if_exists, 

2919 index_label=index_label, 

2920 dtype=dtype, 

2921 ) 

2922 table.create() 

2923 return table.insert(chunksize, method) 

2924 

2925 def has_table(self, name: str, schema: str | None = None) -> bool: 

2926 wld = "?" 

2927 query = f""" 

2928 SELECT 

2929 name 

2930 FROM 

2931 sqlite_master 

2932 WHERE 

2933 type IN ('table', 'view') 

2934 AND name={wld}; 

2935 """ 

2936 

2937 return len(self.execute(query, [name]).fetchall()) > 0 

2938 

2939 def get_table(self, table_name: str, schema: str | None = None) -> None: 

2940 return None # not supported in fallback mode 

2941 

2942 def drop_table(self, name: str, schema: str | None = None) -> None: 

2943 drop_sql = f"DROP TABLE {_get_valid_sqlite_name(name)}" 

2944 self.execute(drop_sql).close() 

2945 

2946 def delete_rows(self, name: str, schema: str | None = None) -> None: 

2947 delete_sql = f"DELETE FROM {_get_valid_sqlite_name(name)}" 

2948 if self.has_table(name, schema): 

2949 self.execute(delete_sql).close() 

2950 

2951 def _create_sql_schema( 

2952 self, 

2953 frame, 

2954 table_name: str, 

2955 keys=None, 

2956 dtype: DtypeArg | None = None, 

2957 schema: str | None = None, 

2958 ) -> str: 

2959 table = SQLiteTable( 

2960 table_name, 

2961 self, 

2962 frame=frame, 

2963 index=False, 

2964 keys=keys, 

2965 dtype=dtype, 

2966 schema=schema, 

2967 ) 

2968 return str(table.sql_schema()) 

2969 

2970 

2971def get_schema( 

2972 frame, 

2973 name: str, 

2974 keys=None, 

2975 con=None, 

2976 dtype: DtypeArg | None = None, 

2977 schema: str | None = None, 

2978) -> str: 

2979 """ 

2980 Get the SQL db table schema for the given frame. 

2981 

2982 Parameters 

2983 ---------- 

2984 frame : DataFrame 

2985 name : str 

2986 name of SQL table 

2987 keys : string or sequence, default: None 

2988 columns to use a primary key 

2989 con: ADBC Connection, SQLAlchemy connectable, sqlite3 connection, default: None 

2990 ADBC provides high performance I/O with native type support, where available. 

2991 Using SQLAlchemy makes it possible to use any DB supported by that 

2992 library 

2993 If a DBAPI2 object, only sqlite3 is supported. 

2994 dtype : dict of column name to SQL type, default None 

2995 Optional specifying the datatype for columns. The SQL type should 

2996 be a SQLAlchemy type, or a string for sqlite3 fallback connection. 

2997 schema: str, default: None 

2998 Optional specifying the schema to be used in creating the table. 

2999 """ 

3000 with pandasSQL_builder(con=con) as pandas_sql: 

3001 return pandas_sql._create_sql_schema( 

3002 frame, name, keys=keys, dtype=dtype, schema=schema 

3003 )