Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/sqlalchemy_utils/functions/database.py: 33%

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

172 statements  

1import itertools 

2import os 

3from collections.abc import Mapping, Sequence 

4 

5import sqlalchemy as sa 

6from sqlalchemy.engine.url import make_url 

7from sqlalchemy.exc import OperationalError, ProgrammingError 

8 

9from ..utils import starts_with 

10from .orm import quote 

11 

12 

13def escape_like(string, escape_char='*'): 

14 """ 

15 Escape the string parameter used in SQL LIKE expressions. 

16 

17 :: 

18 

19 from sqlalchemy_utils import escape_like 

20 

21 

22 query = session.query(User).filter( 

23 User.name.ilike(escape_like('John')) 

24 ) 

25 

26 

27 :param string: a string to escape 

28 :param escape_char: escape character 

29 """ 

30 return ( 

31 string.replace(escape_char, escape_char * 2) 

32 .replace('%', escape_char + '%') 

33 .replace('_', escape_char + '_') 

34 ) 

35 

36 

37def json_sql(value, scalars_to_json=True): 

38 """ 

39 Convert python data structures to PostgreSQL specific SQLAlchemy JSON 

40 constructs. This function is extremly useful if you need to build 

41 PostgreSQL JSON on python side. 

42 

43 .. note:: 

44 

45 This function needs PostgreSQL >= 9.4 

46 

47 Scalars are converted to to_json SQLAlchemy function objects 

48 

49 :: 

50 

51 json_sql(1) # Equals SQL: to_json(1) 

52 

53 json_sql('a') # to_json('a') 

54 

55 

56 Mappings are converted to json_build_object constructs 

57 

58 :: 

59 

60 json_sql({'a': 'c', '2': 5}) # json_build_object('a', 'c', '2', 5) 

61 

62 

63 Sequences (other than strings) are converted to json_build_array constructs 

64 

65 :: 

66 

67 json_sql([1, 2, 3]) # json_build_array(1, 2, 3) 

68 

69 

70 You can also nest these data structures 

71 

72 :: 

73 

74 json_sql({'a': [1, 2, 3]}) 

75 # json_build_object('a', json_build_array[1, 2, 3]) 

76 

77 

78 :param value: 

79 value to be converted to SQLAlchemy PostgreSQL function constructs 

80 """ 

81 if scalars_to_json: 

82 

83 def scalar_convert(a): 

84 return sa.func.to_json(sa.text(a)) 

85 else: 

86 scalar_convert = sa.text 

87 

88 if isinstance(value, Mapping): 

89 return sa.func.json_build_object( 

90 *( 

91 json_sql(v, scalars_to_json=False) 

92 for v in itertools.chain(*value.items()) 

93 ) 

94 ) 

95 elif isinstance(value, str): 

96 return scalar_convert(f"'{value}'") 

97 elif isinstance(value, Sequence): 

98 return sa.func.json_build_array( 

99 *(json_sql(v, scalars_to_json=False) for v in value) 

100 ) 

101 elif isinstance(value, (int, float)): 

102 return scalar_convert(str(value)) 

103 return value 

104 

105 

106def jsonb_sql(value, scalars_to_jsonb=True): 

107 """ 

108 Convert python data structures to PostgreSQL specific SQLAlchemy JSONB 

109 constructs. This function is extremly useful if you need to build 

110 PostgreSQL JSONB on python side. 

111 

112 .. note:: 

113 

114 This function needs PostgreSQL >= 9.4 

115 

116 Scalars are converted to to_jsonb SQLAlchemy function objects 

117 

118 :: 

119 

120 jsonb_sql(1) # Equals SQL: to_jsonb(1) 

121 

122 jsonb_sql('a') # to_jsonb('a') 

123 

124 

125 Mappings are converted to jsonb_build_object constructs 

126 

127 :: 

128 

129 jsonb_sql({'a': 'c', '2': 5}) # jsonb_build_object('a', 'c', '2', 5) 

130 

131 

132 Sequences (other than strings) converted to jsonb_build_array constructs 

133 

134 :: 

135 

136 jsonb_sql([1, 2, 3]) # jsonb_build_array(1, 2, 3) 

137 

138 

139 You can also nest these data structures 

140 

141 :: 

142 

143 jsonb_sql({'a': [1, 2, 3]}) 

144 # jsonb_build_object('a', jsonb_build_array[1, 2, 3]) 

145 

146 

147 :param value: 

148 value to be converted to SQLAlchemy PostgreSQL function constructs 

149 :boolean jsonbb: 

150 Flag to alternatively convert the return with a to_jsonb construct 

151 """ 

152 if scalars_to_jsonb: 

153 

154 def scalar_convert(a): 

155 return sa.func.to_jsonb(sa.text(a)) 

156 else: 

157 scalar_convert = sa.text 

158 

159 if isinstance(value, Mapping): 

160 return sa.func.jsonb_build_object( 

161 *( 

162 jsonb_sql(v, scalars_to_jsonb=False) 

163 for v in itertools.chain(*value.items()) 

164 ) 

165 ) 

166 elif isinstance(value, str): 

167 return scalar_convert(f"'{value}'") 

168 elif isinstance(value, Sequence): 

169 return sa.func.jsonb_build_array( 

170 *(jsonb_sql(v, scalars_to_jsonb=False) for v in value) 

171 ) 

172 elif isinstance(value, (int, float)): 

173 return scalar_convert(str(value)) 

174 return value 

175 

176 

177def has_index(column_or_constraint): 

178 """ 

179 Return whether or not given column or the columns of given foreign key 

180 constraint have an index. A column has an index if it has a single column 

181 index or it is the first column in compound column index. 

182 

183 A foreign key constraint has an index if the constraint columns are the 

184 first columns in compound column index. 

185 

186 :param column_or_constraint: 

187 SQLAlchemy Column object or SA ForeignKeyConstraint object 

188 

189 .. versionadded: 0.26.2 

190 

191 .. versionchanged: 0.30.18 

192 Added support for foreign key constaints. 

193 

194 :: 

195 

196 from sqlalchemy_utils import has_index 

197 

198 

199 class Article(Base): 

200 __tablename__ = 'article' 

201 id = sa.Column(sa.Integer, primary_key=True) 

202 title = sa.Column(sa.String(100)) 

203 is_published = sa.Column(sa.Boolean, index=True) 

204 is_deleted = sa.Column(sa.Boolean) 

205 is_archived = sa.Column(sa.Boolean) 

206 

207 __table_args__ = ( 

208 sa.Index('my_index', is_deleted, is_archived), 

209 ) 

210 

211 

212 table = Article.__table__ 

213 

214 has_index(table.c.is_published) # True 

215 has_index(table.c.is_deleted) # True 

216 has_index(table.c.is_archived) # False 

217 

218 

219 Also supports primary key indexes 

220 

221 :: 

222 

223 from sqlalchemy_utils import has_index 

224 

225 

226 class ArticleTranslation(Base): 

227 __tablename__ = 'article_translation' 

228 id = sa.Column(sa.Integer, primary_key=True) 

229 locale = sa.Column(sa.String(10), primary_key=True) 

230 title = sa.Column(sa.String(100)) 

231 

232 

233 table = ArticleTranslation.__table__ 

234 

235 has_index(table.c.locale) # False 

236 has_index(table.c.id) # True 

237 

238 

239 This function supports foreign key constraints as well 

240 

241 :: 

242 

243 

244 class User(Base): 

245 __tablename__ = 'user' 

246 first_name = sa.Column(sa.Unicode(255), primary_key=True) 

247 last_name = sa.Column(sa.Unicode(255), primary_key=True) 

248 

249 class Article(Base): 

250 __tablename__ = 'article' 

251 id = sa.Column(sa.Integer, primary_key=True) 

252 author_first_name = sa.Column(sa.Unicode(255)) 

253 author_last_name = sa.Column(sa.Unicode(255)) 

254 __table_args__ = ( 

255 sa.ForeignKeyConstraint( 

256 [author_first_name, author_last_name], 

257 [User.first_name, User.last_name] 

258 ), 

259 sa.Index( 

260 'my_index', 

261 author_first_name, 

262 author_last_name 

263 ) 

264 ) 

265 

266 table = Article.__table__ 

267 constraint = list(table.foreign_keys)[0].constraint 

268 

269 has_index(constraint) # True 

270 """ 

271 table = column_or_constraint.table 

272 if not isinstance(table, sa.Table): 

273 raise TypeError( 

274 'Only columns belonging to Table objects are supported. Given ' 

275 'column belongs to %r.' % table 

276 ) 

277 primary_keys = table.primary_key.columns.values() 

278 if isinstance(column_or_constraint, sa.ForeignKeyConstraint): 

279 columns = list(column_or_constraint.columns.values()) 

280 else: 

281 columns = [column_or_constraint] 

282 

283 return (primary_keys and starts_with(primary_keys, columns)) or any( 

284 starts_with(index.columns.values(), columns) for index in table.indexes 

285 ) 

286 

287 

288def has_unique_index(column_or_constraint): 

289 """ 

290 Return whether or not given column or given foreign key constraint has a 

291 unique index. 

292 

293 A column has a unique index if it has a single column primary key index or 

294 it has a single column UniqueConstraint. 

295 

296 A foreign key constraint has a unique index if the columns of the 

297 constraint are the same as the columns of table primary key or the coluns 

298 of any unique index or any unique constraint of the given table. 

299 

300 :param column: SQLAlchemy Column object 

301 

302 .. versionadded: 0.27.1 

303 

304 .. versionchanged: 0.30.18 

305 Added support for foreign key constaints. 

306 

307 Fixed support for unique indexes (previously only worked for unique 

308 constraints) 

309 

310 :: 

311 

312 from sqlalchemy_utils import has_unique_index 

313 

314 

315 class Article(Base): 

316 __tablename__ = 'article' 

317 id = sa.Column(sa.Integer, primary_key=True) 

318 title = sa.Column(sa.String(100)) 

319 is_published = sa.Column(sa.Boolean, unique=True) 

320 is_deleted = sa.Column(sa.Boolean) 

321 is_archived = sa.Column(sa.Boolean) 

322 

323 

324 table = Article.__table__ 

325 

326 has_unique_index(table.c.is_published) # True 

327 has_unique_index(table.c.is_deleted) # False 

328 has_unique_index(table.c.id) # True 

329 

330 

331 This function supports foreign key constraints as well 

332 

333 :: 

334 

335 

336 class User(Base): 

337 __tablename__ = 'user' 

338 first_name = sa.Column(sa.Unicode(255), primary_key=True) 

339 last_name = sa.Column(sa.Unicode(255), primary_key=True) 

340 

341 class Article(Base): 

342 __tablename__ = 'article' 

343 id = sa.Column(sa.Integer, primary_key=True) 

344 author_first_name = sa.Column(sa.Unicode(255)) 

345 author_last_name = sa.Column(sa.Unicode(255)) 

346 __table_args__ = ( 

347 sa.ForeignKeyConstraint( 

348 [author_first_name, author_last_name], 

349 [User.first_name, User.last_name] 

350 ), 

351 sa.Index( 

352 'my_index', 

353 author_first_name, 

354 author_last_name, 

355 unique=True 

356 ) 

357 ) 

358 

359 table = Article.__table__ 

360 constraint = list(table.foreign_keys)[0].constraint 

361 

362 has_unique_index(constraint) # True 

363 

364 

365 :raises TypeError: if given column does not belong to a Table object 

366 """ 

367 table = column_or_constraint.table 

368 if not isinstance(table, sa.Table): 

369 raise TypeError( 

370 'Only columns belonging to Table objects are supported. Given ' 

371 'column belongs to %r.' % table 

372 ) 

373 primary_keys = list(table.primary_key.columns.values()) 

374 if isinstance(column_or_constraint, sa.ForeignKeyConstraint): 

375 columns = list(column_or_constraint.columns.values()) 

376 else: 

377 columns = [column_or_constraint] 

378 

379 return ( 

380 (columns == primary_keys) 

381 or any( 

382 columns == list(constraint.columns.values()) 

383 for constraint in table.constraints 

384 if isinstance(constraint, sa.sql.schema.UniqueConstraint) 

385 ) 

386 or any( 

387 columns == list(index.columns.values()) 

388 for index in table.indexes 

389 if index.unique 

390 ) 

391 ) 

392 

393 

394def is_auto_assigned_date_column(column): 

395 """ 

396 Returns whether or not given SQLAlchemy Column object's is auto assigned 

397 DateTime or Date. 

398 

399 :param column: SQLAlchemy Column object 

400 """ 

401 return ( 

402 isinstance(column.type, sa.DateTime) or isinstance(column.type, sa.Date) 

403 ) and ( 

404 column.default 

405 or column.server_default 

406 or column.onupdate 

407 or column.server_onupdate 

408 ) 

409 

410 

411def _set_url_database(url: sa.engine.url.URL, database): 

412 """Set the database of an engine URL. 

413 

414 :param url: A SQLAlchemy engine URL. 

415 :param database: New database to set. 

416 

417 """ 

418 # Cannot use URL.set() as database may need to be set to None. 

419 ret = url._replace(database=database) 

420 assert ret.database == database, ret 

421 return ret 

422 

423 

424def _get_scalar_result(engine, sql): 

425 with engine.connect() as conn: 

426 return conn.scalar(sql) 

427 

428 

429def _sqlite_file_exists(database): 

430 if not os.path.isfile(database) or os.path.getsize(database) < 100: 

431 return False 

432 

433 with open(database, 'rb') as f: 

434 header = f.read(100) 

435 

436 return header[:16] == b'SQLite format 3\x00' 

437 

438 

439def database_exists(url): 

440 """Check if a database exists. 

441 

442 :param url: A SQLAlchemy engine URL. 

443 

444 Performs backend-specific testing to quickly determine if a database 

445 exists on the server. :: 

446 

447 database_exists('postgresql://postgres@localhost/name') #=> False 

448 create_database('postgresql://postgres@localhost/name') 

449 database_exists('postgresql://postgres@localhost/name') #=> True 

450 

451 Supports checking against a constructed URL as well. :: 

452 

453 engine = create_engine('postgresql://postgres@localhost/name') 

454 database_exists(engine.url) #=> False 

455 create_database(engine.url) 

456 database_exists(engine.url) #=> True 

457 

458 """ 

459 

460 url = make_url(url) 

461 database = url.database 

462 dialect_name = url.get_dialect().name 

463 engine = None 

464 try: 

465 if dialect_name == 'postgresql': 

466 text = "SELECT 1 FROM pg_database WHERE datname='%s'" % database 

467 for db in (database, 'postgres', 'template1', 'template0', None): 

468 url = _set_url_database(url, database=db) 

469 engine = sa.create_engine(url, isolation_level='AUTOCOMMIT') 

470 try: 

471 return bool(_get_scalar_result(engine, sa.text(text))) 

472 except (ProgrammingError, OperationalError): 

473 pass 

474 return False 

475 

476 elif dialect_name == 'mysql': 

477 url = _set_url_database(url, database=None) 

478 engine = sa.create_engine(url) 

479 text = ( 

480 'SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA ' 

481 "WHERE SCHEMA_NAME = '%s'" % database 

482 ) 

483 return bool(_get_scalar_result(engine, sa.text(text))) 

484 

485 elif dialect_name == 'sqlite': 

486 url = _set_url_database(url, database=None) 

487 engine = sa.create_engine(url) 

488 if database: 

489 return database == ':memory:' or _sqlite_file_exists(database) 

490 else: 

491 # The default SQLAlchemy database is in memory, and :memory: is 

492 # not required, thus we should support that use case. 

493 return True 

494 elif dialect_name == 'mssql': 

495 text = "SELECT 1 FROM sys.databases WHERE name = '%s'" % database 

496 url = _set_url_database(url, database='master') 

497 engine = sa.create_engine(url, isolation_level='AUTOCOMMIT') 

498 try: 

499 return bool(_get_scalar_result(engine, sa.text(text))) 

500 except (ProgrammingError, OperationalError): 

501 return False 

502 else: 

503 text = 'SELECT 1' 

504 try: 

505 engine = sa.create_engine(url) 

506 return bool(_get_scalar_result(engine, sa.text(text))) 

507 except (ProgrammingError, OperationalError): 

508 return False 

509 finally: 

510 if engine: 

511 engine.dispose() 

512 

513 

514def create_database(url, encoding='utf8', template=None): 

515 """Issue the appropriate CREATE DATABASE statement. 

516 

517 :param url: A SQLAlchemy engine URL. 

518 :param encoding: The encoding to create the database as. 

519 :param template: 

520 The name of the template from which to create the new database. At the 

521 moment only supported by PostgreSQL driver. 

522 

523 To create a database, you can pass a simple URL that would have 

524 been passed to ``create_engine``. :: 

525 

526 create_database('postgresql://postgres@localhost/name') 

527 

528 You may also pass the url from an existing engine. :: 

529 

530 create_database(engine.url) 

531 

532 Has full support for mysql, postgres, and sqlite. In theory, 

533 other database engines should be supported. 

534 """ 

535 

536 url = make_url(url) 

537 database = url.database 

538 dialect_name = url.get_dialect().name 

539 dialect_driver = url.get_dialect().driver 

540 

541 if dialect_name == 'postgresql': 

542 url = _set_url_database(url, database='postgres') 

543 elif dialect_name == 'mssql': 

544 url = _set_url_database(url, database='master') 

545 elif dialect_name == 'cockroachdb': 

546 url = _set_url_database(url, database='defaultdb') 

547 elif not dialect_name == 'sqlite': 

548 url = _set_url_database(url, database=None) 

549 

550 if (dialect_name == 'mssql' and dialect_driver in {'pymssql', 'pyodbc'}) or ( 

551 dialect_name == 'postgresql' 

552 and dialect_driver 

553 in {'asyncpg', 'pg8000', 'psycopg', 'psycopg2', 'psycopg2cffi'} 

554 ): 

555 engine = sa.create_engine(url, isolation_level='AUTOCOMMIT') 

556 else: 

557 engine = sa.create_engine(url) 

558 

559 if dialect_name == 'postgresql': 

560 if not template: 

561 template = 'template1' 

562 

563 with engine.begin() as conn: 

564 text = "CREATE DATABASE {} ENCODING '{}' TEMPLATE {}".format( 

565 quote(conn, database), encoding, quote(conn, template) 

566 ) 

567 conn.execute(sa.text(text)) 

568 

569 elif dialect_name == 'mysql': 

570 with engine.begin() as conn: 

571 text = "CREATE DATABASE {} CHARACTER SET = '{}'".format( 

572 quote(conn, database), encoding 

573 ) 

574 conn.execute(sa.text(text)) 

575 

576 elif dialect_name == 'sqlite' and database != ':memory:': 

577 if database: 

578 with engine.begin() as conn: 

579 conn.execute(sa.text('CREATE TABLE DB(id int)')) 

580 conn.execute(sa.text('DROP TABLE DB')) 

581 

582 else: 

583 with engine.begin() as conn: 

584 text = f'CREATE DATABASE {quote(conn, database)}' 

585 conn.execute(sa.text(text)) 

586 

587 engine.dispose() 

588 

589 

590def drop_database(url): 

591 """Issue the appropriate DROP DATABASE statement. 

592 

593 :param url: A SQLAlchemy engine URL. 

594 

595 Works similar to the :func:`create_database` method in that both url text 

596 and a constructed url are accepted. 

597 

598 :: 

599 

600 drop_database('postgresql://postgres@localhost/name') 

601 drop_database(engine.url) 

602 

603 """ 

604 

605 url = make_url(url) 

606 database = url.database 

607 dialect_name = url.get_dialect().name 

608 dialect_driver = url.get_dialect().driver 

609 

610 if dialect_name == 'postgresql': 

611 url = _set_url_database(url, database='postgres') 

612 elif dialect_name == 'mssql': 

613 url = _set_url_database(url, database='master') 

614 elif dialect_name == 'cockroachdb': 

615 url = _set_url_database(url, database='defaultdb') 

616 elif not dialect_name == 'sqlite': 

617 url = _set_url_database(url, database=None) 

618 

619 if (dialect_name == 'mssql' and dialect_driver in {'pymssql', 'pyodbc'}) or ( 

620 dialect_name == 'postgresql' 

621 and dialect_driver 

622 in {'asyncpg', 'pg8000', 'psycopg', 'psycopg2', 'psycopg2cffi'} 

623 ): 

624 engine = sa.create_engine(url, isolation_level='AUTOCOMMIT') 

625 else: 

626 engine = sa.create_engine(url) 

627 

628 if dialect_name == 'sqlite' and database != ':memory:': 

629 if database: 

630 os.remove(database) 

631 else: 

632 with engine.begin() as conn: 

633 text = f'DROP DATABASE {quote(conn, database)}' 

634 conn.execute(sa.text(text)) 

635 

636 engine.dispose()