extension.py 37 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008
  1. from __future__ import annotations
  2. import os
  3. import types
  4. import typing as t
  5. import warnings
  6. from weakref import WeakKeyDictionary
  7. import sqlalchemy as sa
  8. import sqlalchemy.event as sa_event
  9. import sqlalchemy.exc as sa_exc
  10. import sqlalchemy.orm as sa_orm
  11. from flask import abort
  12. from flask import current_app
  13. from flask import Flask
  14. from flask import has_app_context
  15. from .model import _QueryProperty
  16. from .model import BindMixin
  17. from .model import DefaultMeta
  18. from .model import DefaultMetaNoName
  19. from .model import Model
  20. from .model import NameMixin
  21. from .pagination import Pagination
  22. from .pagination import SelectPagination
  23. from .query import Query
  24. from .session import _app_ctx_id
  25. from .session import Session
  26. from .table import _Table
  27. _O = t.TypeVar("_O", bound=object) # Based on sqlalchemy.orm._typing.py
  28. # Type accepted for model_class argument
  29. _FSA_MCT = t.TypeVar(
  30. "_FSA_MCT",
  31. bound=t.Union[
  32. t.Type[Model],
  33. sa_orm.DeclarativeMeta,
  34. t.Type[sa_orm.DeclarativeBase],
  35. t.Type[sa_orm.DeclarativeBaseNoMeta],
  36. ],
  37. )
  38. # Type returned by make_declarative_base
  39. class _FSAModel(Model):
  40. metadata: sa.MetaData
  41. def _get_2x_declarative_bases(
  42. model_class: _FSA_MCT,
  43. ) -> list[t.Type[t.Union[sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta]]]:
  44. return [
  45. b
  46. for b in model_class.__bases__
  47. if issubclass(b, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta))
  48. ]
  49. class SQLAlchemy:
  50. """Integrates SQLAlchemy with Flask. This handles setting up one or more engines,
  51. associating tables and models with specific engines, and cleaning up connections and
  52. sessions after each request.
  53. Only the engine configuration is specific to each application, other things like
  54. the model, table, metadata, and session are shared for all applications using that
  55. extension instance. Call :meth:`init_app` to configure the extension on an
  56. application.
  57. After creating the extension, create model classes by subclassing :attr:`Model`, and
  58. table classes with :attr:`Table`. These can be accessed before :meth:`init_app` is
  59. called, making it possible to define the models separately from the application.
  60. Accessing :attr:`session` and :attr:`engine` requires an active Flask application
  61. context. This includes methods like :meth:`create_all` which use the engine.
  62. This class also provides access to names in SQLAlchemy's ``sqlalchemy`` and
  63. ``sqlalchemy.orm`` modules. For example, you can use ``db.Column`` and
  64. ``db.relationship`` instead of importing ``sqlalchemy.Column`` and
  65. ``sqlalchemy.orm.relationship``. This can be convenient when defining models.
  66. :param app: Call :meth:`init_app` on this Flask application now.
  67. :param metadata: Use this as the default :class:`sqlalchemy.schema.MetaData`. Useful
  68. for setting a naming convention.
  69. :param session_options: Arguments used by :attr:`session` to create each session
  70. instance. A ``scopefunc`` key will be passed to the scoped session, not the
  71. session instance. See :class:`sqlalchemy.orm.sessionmaker` for a list of
  72. arguments.
  73. :param query_class: Use this as the default query class for models and dynamic
  74. relationships. The query interface is considered legacy in SQLAlchemy.
  75. :param model_class: Use this as the model base class when creating the declarative
  76. model class :attr:`Model`. Can also be a fully created declarative model class
  77. for further customization.
  78. :param engine_options: Default arguments used when creating every engine. These are
  79. lower precedence than application config. See :func:`sqlalchemy.create_engine`
  80. for a list of arguments.
  81. :param add_models_to_shell: Add the ``db`` instance and all model classes to
  82. ``flask shell``.
  83. .. versionchanged:: 3.1.0
  84. The ``metadata`` parameter can still be used with SQLAlchemy 1.x classes,
  85. but is ignored when using SQLAlchemy 2.x style of declarative classes.
  86. Instead, specify metadata on your Base class.
  87. .. versionchanged:: 3.1.0
  88. Added the ``disable_autonaming`` parameter.
  89. .. versionchanged:: 3.1.0
  90. Changed ``model_class`` parameter to accepta SQLAlchemy 2.x
  91. declarative base subclass.
  92. .. versionchanged:: 3.0
  93. An active Flask application context is always required to access ``session`` and
  94. ``engine``.
  95. .. versionchanged:: 3.0
  96. Separate ``metadata`` are used for each bind key.
  97. .. versionchanged:: 3.0
  98. The ``engine_options`` parameter is applied as defaults before per-engine
  99. configuration.
  100. .. versionchanged:: 3.0
  101. The session class can be customized in ``session_options``.
  102. .. versionchanged:: 3.0
  103. Added the ``add_models_to_shell`` parameter.
  104. .. versionchanged:: 3.0
  105. Engines are created when calling ``init_app`` rather than the first time they
  106. are accessed.
  107. .. versionchanged:: 3.0
  108. All parameters except ``app`` are keyword-only.
  109. .. versionchanged:: 3.0
  110. The extension instance is stored directly as ``app.extensions["sqlalchemy"]``.
  111. .. versionchanged:: 3.0
  112. Setup methods are renamed with a leading underscore. They are considered
  113. internal interfaces which may change at any time.
  114. .. versionchanged:: 3.0
  115. Removed the ``use_native_unicode`` parameter and config.
  116. .. versionchanged:: 2.4
  117. Added the ``engine_options`` parameter.
  118. .. versionchanged:: 2.1
  119. Added the ``metadata``, ``query_class``, and ``model_class`` parameters.
  120. .. versionchanged:: 2.1
  121. Use the same query class across ``session``, ``Model.query`` and
  122. ``Query``.
  123. .. versionchanged:: 0.16
  124. ``scopefunc`` is accepted in ``session_options``.
  125. .. versionchanged:: 0.10
  126. Added the ``session_options`` parameter.
  127. """
  128. def __init__(
  129. self,
  130. app: Flask | None = None,
  131. *,
  132. metadata: sa.MetaData | None = None,
  133. session_options: dict[str, t.Any] | None = None,
  134. query_class: type[Query] = Query,
  135. model_class: _FSA_MCT = Model, # type: ignore[assignment]
  136. engine_options: dict[str, t.Any] | None = None,
  137. add_models_to_shell: bool = True,
  138. disable_autonaming: bool = False,
  139. ):
  140. if session_options is None:
  141. session_options = {}
  142. self.Query = query_class
  143. """The default query class used by ``Model.query`` and ``lazy="dynamic"``
  144. relationships.
  145. .. warning::
  146. The query interface is considered legacy in SQLAlchemy.
  147. Customize this by passing the ``query_class`` parameter to the extension.
  148. """
  149. self.session = self._make_scoped_session(session_options)
  150. """A :class:`sqlalchemy.orm.scoping.scoped_session` that creates instances of
  151. :class:`.Session` scoped to the current Flask application context. The session
  152. will be removed, returning the engine connection to the pool, when the
  153. application context exits.
  154. Customize this by passing ``session_options`` to the extension.
  155. This requires that a Flask application context is active.
  156. .. versionchanged:: 3.0
  157. The session is scoped to the current app context.
  158. """
  159. self.metadatas: dict[str | None, sa.MetaData] = {}
  160. """Map of bind keys to :class:`sqlalchemy.schema.MetaData` instances. The
  161. ``None`` key refers to the default metadata, and is available as
  162. :attr:`metadata`.
  163. Customize the default metadata by passing the ``metadata`` parameter to the
  164. extension. This can be used to set a naming convention. When metadata for
  165. another bind key is created, it copies the default's naming convention.
  166. .. versionadded:: 3.0
  167. """
  168. if metadata is not None:
  169. if len(_get_2x_declarative_bases(model_class)) > 0:
  170. warnings.warn(
  171. "When using SQLAlchemy 2.x style of declarative classes,"
  172. " the `metadata` should be an attribute of the base class."
  173. "The metadata passed into SQLAlchemy() is ignored.",
  174. DeprecationWarning,
  175. stacklevel=2,
  176. )
  177. else:
  178. metadata.info["bind_key"] = None
  179. self.metadatas[None] = metadata
  180. self.Table = self._make_table_class()
  181. """A :class:`sqlalchemy.schema.Table` class that chooses a metadata
  182. automatically.
  183. Unlike the base ``Table``, the ``metadata`` argument is not required. If it is
  184. not given, it is selected based on the ``bind_key`` argument.
  185. :param bind_key: Used to select a different metadata.
  186. :param args: Arguments passed to the base class. These are typically the table's
  187. name, columns, and constraints.
  188. :param kwargs: Arguments passed to the base class.
  189. .. versionchanged:: 3.0
  190. This is a subclass of SQLAlchemy's ``Table`` rather than a function.
  191. """
  192. self.Model = self._make_declarative_base(
  193. model_class, disable_autonaming=disable_autonaming
  194. )
  195. """A SQLAlchemy declarative model class. Subclass this to define database
  196. models.
  197. If a model does not set ``__tablename__``, it will be generated by converting
  198. the class name from ``CamelCase`` to ``snake_case``. It will not be generated
  199. if the model looks like it uses single-table inheritance.
  200. If a model or parent class sets ``__bind_key__``, it will use that metadata and
  201. database engine. Otherwise, it will use the default :attr:`metadata` and
  202. :attr:`engine`. This is ignored if the model sets ``metadata`` or ``__table__``.
  203. For code using the SQLAlchemy 1.x API, customize this model by subclassing
  204. :class:`.Model` and passing the ``model_class`` parameter to the extension.
  205. A fully created declarative model class can be
  206. passed as well, to use a custom metaclass.
  207. For code using the SQLAlchemy 2.x API, customize this model by subclassing
  208. :class:`sqlalchemy.orm.DeclarativeBase` or
  209. :class:`sqlalchemy.orm.DeclarativeBaseNoMeta`
  210. and passing the ``model_class`` parameter to the extension.
  211. """
  212. if engine_options is None:
  213. engine_options = {}
  214. self._engine_options = engine_options
  215. self._app_engines: WeakKeyDictionary[Flask, dict[str | None, sa.engine.Engine]]
  216. self._app_engines = WeakKeyDictionary()
  217. self._add_models_to_shell = add_models_to_shell
  218. if app is not None:
  219. self.init_app(app)
  220. def __repr__(self) -> str:
  221. if not has_app_context():
  222. return f"<{type(self).__name__}>"
  223. message = f"{type(self).__name__} {self.engine.url}"
  224. if len(self.engines) > 1:
  225. message = f"{message} +{len(self.engines) - 1}"
  226. return f"<{message}>"
  227. def init_app(self, app: Flask) -> None:
  228. """Initialize a Flask application for use with this extension instance. This
  229. must be called before accessing the database engine or session with the app.
  230. This sets default configuration values, then configures the extension on the
  231. application and creates the engines for each bind key. Therefore, this must be
  232. called after the application has been configured. Changes to application config
  233. after this call will not be reflected.
  234. The following keys from ``app.config`` are used:
  235. - :data:`.SQLALCHEMY_DATABASE_URI`
  236. - :data:`.SQLALCHEMY_ENGINE_OPTIONS`
  237. - :data:`.SQLALCHEMY_ECHO`
  238. - :data:`.SQLALCHEMY_BINDS`
  239. - :data:`.SQLALCHEMY_RECORD_QUERIES`
  240. - :data:`.SQLALCHEMY_TRACK_MODIFICATIONS`
  241. :param app: The Flask application to initialize.
  242. """
  243. if "sqlalchemy" in app.extensions:
  244. raise RuntimeError(
  245. "A 'SQLAlchemy' instance has already been registered on this Flask app."
  246. " Import and use that instance instead."
  247. )
  248. app.extensions["sqlalchemy"] = self
  249. app.teardown_appcontext(self._teardown_session)
  250. if self._add_models_to_shell:
  251. from .cli import add_models_to_shell
  252. app.shell_context_processor(add_models_to_shell)
  253. basic_uri: str | sa.engine.URL | None = app.config.setdefault(
  254. "SQLALCHEMY_DATABASE_URI", None
  255. )
  256. basic_engine_options = self._engine_options.copy()
  257. basic_engine_options.update(
  258. app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {})
  259. )
  260. echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False)
  261. config_binds: dict[
  262. str | None, str | sa.engine.URL | dict[str, t.Any]
  263. ] = app.config.setdefault("SQLALCHEMY_BINDS", {})
  264. engine_options: dict[str | None, dict[str, t.Any]] = {}
  265. # Build the engine config for each bind key.
  266. for key, value in config_binds.items():
  267. engine_options[key] = self._engine_options.copy()
  268. if isinstance(value, (str, sa.engine.URL)):
  269. engine_options[key]["url"] = value
  270. else:
  271. engine_options[key].update(value)
  272. # Build the engine config for the default bind key.
  273. if basic_uri is not None:
  274. basic_engine_options["url"] = basic_uri
  275. if "url" in basic_engine_options:
  276. engine_options.setdefault(None, {}).update(basic_engine_options)
  277. if not engine_options:
  278. raise RuntimeError(
  279. "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set."
  280. )
  281. engines = self._app_engines.setdefault(app, {})
  282. # Dispose existing engines in case init_app is called again.
  283. if engines:
  284. for engine in engines.values():
  285. engine.dispose()
  286. engines.clear()
  287. # Create the metadata and engine for each bind key.
  288. for key, options in engine_options.items():
  289. self._make_metadata(key)
  290. options.setdefault("echo", echo)
  291. options.setdefault("echo_pool", echo)
  292. self._apply_driver_defaults(options, app)
  293. engines[key] = self._make_engine(key, options, app)
  294. if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False):
  295. from . import record_queries
  296. for engine in engines.values():
  297. record_queries._listen(engine)
  298. if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False):
  299. from . import track_modifications
  300. track_modifications._listen(self.session)
  301. def _make_scoped_session(
  302. self, options: dict[str, t.Any]
  303. ) -> sa_orm.scoped_session[Session]:
  304. """Create a :class:`sqlalchemy.orm.scoping.scoped_session` around the factory
  305. from :meth:`_make_session_factory`. The result is available as :attr:`session`.
  306. The scope function can be customized using the ``scopefunc`` key in the
  307. ``session_options`` parameter to the extension. By default it uses the current
  308. thread or greenlet id.
  309. This method is used for internal setup. Its signature may change at any time.
  310. :meta private:
  311. :param options: The ``session_options`` parameter from ``__init__``. Keyword
  312. arguments passed to the session factory. A ``scopefunc`` key is popped.
  313. .. versionchanged:: 3.0
  314. The session is scoped to the current app context.
  315. .. versionchanged:: 3.0
  316. Renamed from ``create_scoped_session``, this method is internal.
  317. """
  318. scope = options.pop("scopefunc", _app_ctx_id)
  319. factory = self._make_session_factory(options)
  320. return sa_orm.scoped_session(factory, scope)
  321. def _make_session_factory(
  322. self, options: dict[str, t.Any]
  323. ) -> sa_orm.sessionmaker[Session]:
  324. """Create the SQLAlchemy :class:`sqlalchemy.orm.sessionmaker` used by
  325. :meth:`_make_scoped_session`.
  326. To customize, pass the ``session_options`` parameter to :class:`SQLAlchemy`. To
  327. customize the session class, subclass :class:`.Session` and pass it as the
  328. ``class_`` key.
  329. This method is used for internal setup. Its signature may change at any time.
  330. :meta private:
  331. :param options: The ``session_options`` parameter from ``__init__``. Keyword
  332. arguments passed to the session factory.
  333. .. versionchanged:: 3.0
  334. The session class can be customized.
  335. .. versionchanged:: 3.0
  336. Renamed from ``create_session``, this method is internal.
  337. """
  338. options.setdefault("class_", Session)
  339. options.setdefault("query_cls", self.Query)
  340. return sa_orm.sessionmaker(db=self, **options)
  341. def _teardown_session(self, exc: BaseException | None) -> None:
  342. """Remove the current session at the end of the request.
  343. :meta private:
  344. .. versionadded:: 3.0
  345. """
  346. self.session.remove()
  347. def _make_metadata(self, bind_key: str | None) -> sa.MetaData:
  348. """Get or create a :class:`sqlalchemy.schema.MetaData` for the given bind key.
  349. This method is used for internal setup. Its signature may change at any time.
  350. :meta private:
  351. :param bind_key: The name of the metadata being created.
  352. .. versionadded:: 3.0
  353. """
  354. if bind_key in self.metadatas:
  355. return self.metadatas[bind_key]
  356. if bind_key is not None:
  357. # Copy the naming convention from the default metadata.
  358. naming_convention = self._make_metadata(None).naming_convention
  359. else:
  360. naming_convention = None
  361. # Set the bind key in info to be used by session.get_bind.
  362. metadata = sa.MetaData(
  363. naming_convention=naming_convention, info={"bind_key": bind_key}
  364. )
  365. self.metadatas[bind_key] = metadata
  366. return metadata
  367. def _make_table_class(self) -> type[_Table]:
  368. """Create a SQLAlchemy :class:`sqlalchemy.schema.Table` class that chooses a
  369. metadata automatically based on the ``bind_key``. The result is available as
  370. :attr:`Table`.
  371. This method is used for internal setup. Its signature may change at any time.
  372. :meta private:
  373. .. versionadded:: 3.0
  374. """
  375. class Table(_Table):
  376. def __new__(
  377. cls, *args: t.Any, bind_key: str | None = None, **kwargs: t.Any
  378. ) -> Table:
  379. # If a metadata arg is passed, go directly to the base Table. Also do
  380. # this for no args so the correct error is shown.
  381. if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)):
  382. return super().__new__(cls, *args, **kwargs)
  383. metadata = self._make_metadata(bind_key)
  384. return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs)
  385. return Table
  386. def _make_declarative_base(
  387. self,
  388. model_class: _FSA_MCT,
  389. disable_autonaming: bool = False,
  390. ) -> t.Type[_FSAModel]:
  391. """Create a SQLAlchemy declarative model class. The result is available as
  392. :attr:`Model`.
  393. To customize, subclass :class:`.Model` and pass it as ``model_class`` to
  394. :class:`SQLAlchemy`. To customize at the metaclass level, pass an already
  395. created declarative model class as ``model_class``.
  396. This method is used for internal setup. Its signature may change at any time.
  397. :meta private:
  398. :param model_class: A model base class, or an already created declarative model
  399. class.
  400. :param disable_autonaming: Turns off automatic tablename generation in models.
  401. .. versionchanged:: 3.1.0
  402. Added support for passing SQLAlchemy 2.x base class as model class.
  403. Added optional ``disable_autonaming`` parameter.
  404. .. versionchanged:: 3.0
  405. Renamed with a leading underscore, this method is internal.
  406. .. versionchanged:: 2.3
  407. ``model`` can be an already created declarative model class.
  408. """
  409. model: t.Type[_FSAModel]
  410. declarative_bases = _get_2x_declarative_bases(model_class)
  411. if len(declarative_bases) > 1:
  412. # raise error if more than one declarative base is found
  413. raise ValueError(
  414. "Only one declarative base can be passed to SQLAlchemy."
  415. " Got: {}".format(model_class.__bases__)
  416. )
  417. elif len(declarative_bases) == 1:
  418. body = dict(model_class.__dict__)
  419. body["__fsa__"] = self
  420. mixin_classes = [BindMixin, NameMixin, Model]
  421. if disable_autonaming:
  422. mixin_classes.remove(NameMixin)
  423. model = types.new_class(
  424. "FlaskSQLAlchemyBase",
  425. (*mixin_classes, *model_class.__bases__),
  426. {"metaclass": type(declarative_bases[0])},
  427. lambda ns: ns.update(body),
  428. )
  429. elif not isinstance(model_class, sa_orm.DeclarativeMeta):
  430. metadata = self._make_metadata(None)
  431. metaclass = DefaultMetaNoName if disable_autonaming else DefaultMeta
  432. model = sa_orm.declarative_base(
  433. metadata=metadata, cls=model_class, name="Model", metaclass=metaclass
  434. )
  435. else:
  436. model = model_class # type: ignore[assignment]
  437. if None not in self.metadatas:
  438. # Use the model's metadata as the default metadata.
  439. model.metadata.info["bind_key"] = None
  440. self.metadatas[None] = model.metadata
  441. else:
  442. # Use the passed in default metadata as the model's metadata.
  443. model.metadata = self.metadatas[None]
  444. model.query_class = self.Query
  445. model.query = _QueryProperty() # type: ignore[assignment]
  446. model.__fsa__ = self
  447. return model
  448. def _apply_driver_defaults(self, options: dict[str, t.Any], app: Flask) -> None:
  449. """Apply driver-specific configuration to an engine.
  450. SQLite in-memory databases use ``StaticPool`` and disable ``check_same_thread``.
  451. File paths are relative to the app's :attr:`~flask.Flask.instance_path`,
  452. which is created if it doesn't exist.
  453. MySQL sets ``charset="utf8mb4"``, and ``pool_timeout`` defaults to 2 hours.
  454. This method is used for internal setup. Its signature may change at any time.
  455. :meta private:
  456. :param options: Arguments passed to the engine.
  457. :param app: The application that the engine configuration belongs to.
  458. .. versionchanged:: 3.0
  459. SQLite paths are relative to ``app.instance_path``. It does not use
  460. ``NullPool`` if ``pool_size`` is 0. Driver-level URIs are supported.
  461. .. versionchanged:: 3.0
  462. MySQL sets ``charset="utf8mb4". It does not set ``pool_size`` to 10. It
  463. does not set ``pool_recycle`` if not using a queue pool.
  464. .. versionchanged:: 3.0
  465. Renamed from ``apply_driver_hacks``, this method is internal. It does not
  466. return anything.
  467. .. versionchanged:: 2.5
  468. Returns ``(sa_url, options)``.
  469. """
  470. url = sa.engine.make_url(options["url"])
  471. if url.drivername in {"sqlite", "sqlite+pysqlite"}:
  472. if url.database is None or url.database in {"", ":memory:"}:
  473. options["poolclass"] = sa.pool.StaticPool
  474. if "connect_args" not in options:
  475. options["connect_args"] = {}
  476. options["connect_args"]["check_same_thread"] = False
  477. else:
  478. # the url might look like sqlite:///file:path?uri=true
  479. is_uri = url.query.get("uri", False)
  480. if is_uri:
  481. db_str = url.database[5:]
  482. else:
  483. db_str = url.database
  484. if not os.path.isabs(db_str):
  485. os.makedirs(app.instance_path, exist_ok=True)
  486. db_str = os.path.join(app.instance_path, db_str)
  487. if is_uri:
  488. db_str = f"file:{db_str}"
  489. options["url"] = url.set(database=db_str)
  490. elif url.drivername.startswith("mysql"):
  491. # set queue defaults only when using queue pool
  492. if (
  493. "pool_class" not in options
  494. or options["pool_class"] is sa.pool.QueuePool
  495. ):
  496. options.setdefault("pool_recycle", 7200)
  497. if "charset" not in url.query:
  498. options["url"] = url.update_query_dict({"charset": "utf8mb4"})
  499. def _make_engine(
  500. self, bind_key: str | None, options: dict[str, t.Any], app: Flask
  501. ) -> sa.engine.Engine:
  502. """Create the :class:`sqlalchemy.engine.Engine` for the given bind key and app.
  503. To customize, use :data:`.SQLALCHEMY_ENGINE_OPTIONS` or
  504. :data:`.SQLALCHEMY_BINDS` config. Pass ``engine_options`` to :class:`SQLAlchemy`
  505. to set defaults for all engines.
  506. This method is used for internal setup. Its signature may change at any time.
  507. :meta private:
  508. :param bind_key: The name of the engine being created.
  509. :param options: Arguments passed to the engine.
  510. :param app: The application that the engine configuration belongs to.
  511. .. versionchanged:: 3.0
  512. Renamed from ``create_engine``, this method is internal.
  513. """
  514. return sa.engine_from_config(options, prefix="")
  515. @property
  516. def metadata(self) -> sa.MetaData:
  517. """The default metadata used by :attr:`Model` and :attr:`Table` if no bind key
  518. is set.
  519. """
  520. return self.metadatas[None]
  521. @property
  522. def engines(self) -> t.Mapping[str | None, sa.engine.Engine]:
  523. """Map of bind keys to :class:`sqlalchemy.engine.Engine` instances for current
  524. application. The ``None`` key refers to the default engine, and is available as
  525. :attr:`engine`.
  526. To customize, set the :data:`.SQLALCHEMY_BINDS` config, and set defaults by
  527. passing the ``engine_options`` parameter to the extension.
  528. This requires that a Flask application context is active.
  529. .. versionadded:: 3.0
  530. """
  531. app = current_app._get_current_object() # type: ignore[attr-defined]
  532. if app not in self._app_engines:
  533. raise RuntimeError(
  534. "The current Flask app is not registered with this 'SQLAlchemy'"
  535. " instance. Did you forget to call 'init_app', or did you create"
  536. " multiple 'SQLAlchemy' instances?"
  537. )
  538. return self._app_engines[app]
  539. @property
  540. def engine(self) -> sa.engine.Engine:
  541. """The default :class:`~sqlalchemy.engine.Engine` for the current application,
  542. used by :attr:`session` if the :attr:`Model` or :attr:`Table` being queried does
  543. not set a bind key.
  544. To customize, set the :data:`.SQLALCHEMY_ENGINE_OPTIONS` config, and set
  545. defaults by passing the ``engine_options`` parameter to the extension.
  546. This requires that a Flask application context is active.
  547. """
  548. return self.engines[None]
  549. def get_engine(
  550. self, bind_key: str | None = None, **kwargs: t.Any
  551. ) -> sa.engine.Engine:
  552. """Get the engine for the given bind key for the current application.
  553. This requires that a Flask application context is active.
  554. :param bind_key: The name of the engine.
  555. .. deprecated:: 3.0
  556. Will be removed in Flask-SQLAlchemy 3.2. Use ``engines[key]`` instead.
  557. .. versionchanged:: 3.0
  558. Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``
  559. parameter.
  560. """
  561. warnings.warn(
  562. "'get_engine' is deprecated and will be removed in Flask-SQLAlchemy"
  563. " 3.2. Use 'engine' or 'engines[key]' instead. If you're using"
  564. " Flask-Migrate or Alembic, you'll need to update your 'env.py' file.",
  565. DeprecationWarning,
  566. stacklevel=2,
  567. )
  568. if "bind" in kwargs:
  569. bind_key = kwargs.pop("bind")
  570. return self.engines[bind_key]
  571. def get_or_404(
  572. self,
  573. entity: type[_O],
  574. ident: t.Any,
  575. *,
  576. description: str | None = None,
  577. **kwargs: t.Any,
  578. ) -> _O:
  579. """Like :meth:`session.get() <sqlalchemy.orm.Session.get>` but aborts with a
  580. ``404 Not Found`` error instead of returning ``None``.
  581. :param entity: The model class to query.
  582. :param ident: The primary key to query.
  583. :param description: A custom message to show on the error page.
  584. :param kwargs: Extra arguments passed to ``session.get()``.
  585. .. versionchanged:: 3.1
  586. Pass extra keyword arguments to ``session.get()``.
  587. .. versionadded:: 3.0
  588. """
  589. value = self.session.get(entity, ident, **kwargs)
  590. if value is None:
  591. abort(404, description=description)
  592. return value
  593. def first_or_404(
  594. self, statement: sa.sql.Select[t.Any], *, description: str | None = None
  595. ) -> t.Any:
  596. """Like :meth:`Result.scalar() <sqlalchemy.engine.Result.scalar>`, but aborts
  597. with a ``404 Not Found`` error instead of returning ``None``.
  598. :param statement: The ``select`` statement to execute.
  599. :param description: A custom message to show on the error page.
  600. .. versionadded:: 3.0
  601. """
  602. value = self.session.execute(statement).scalar()
  603. if value is None:
  604. abort(404, description=description)
  605. return value
  606. def one_or_404(
  607. self, statement: sa.sql.Select[t.Any], *, description: str | None = None
  608. ) -> t.Any:
  609. """Like :meth:`Result.scalar_one() <sqlalchemy.engine.Result.scalar_one>`,
  610. but aborts with a ``404 Not Found`` error instead of raising ``NoResultFound``
  611. or ``MultipleResultsFound``.
  612. :param statement: The ``select`` statement to execute.
  613. :param description: A custom message to show on the error page.
  614. .. versionadded:: 3.0
  615. """
  616. try:
  617. return self.session.execute(statement).scalar_one()
  618. except (sa_exc.NoResultFound, sa_exc.MultipleResultsFound):
  619. abort(404, description=description)
  620. def paginate(
  621. self,
  622. select: sa.sql.Select[t.Any],
  623. *,
  624. page: int | None = None,
  625. per_page: int | None = None,
  626. max_per_page: int | None = None,
  627. error_out: bool = True,
  628. count: bool = True,
  629. ) -> Pagination:
  630. """Apply an offset and limit to a select statment based on the current page and
  631. number of items per page, returning a :class:`.Pagination` object.
  632. The statement should select a model class, like ``select(User)``. This applies
  633. ``unique()`` and ``scalars()`` modifiers to the result, so compound selects will
  634. not return the expected results.
  635. :param select: The ``select`` statement to paginate.
  636. :param page: The current page, used to calculate the offset. Defaults to the
  637. ``page`` query arg during a request, or 1 otherwise.
  638. :param per_page: The maximum number of items on a page, used to calculate the
  639. offset and limit. Defaults to the ``per_page`` query arg during a request,
  640. or 20 otherwise.
  641. :param max_per_page: The maximum allowed value for ``per_page``, to limit a
  642. user-provided value. Use ``None`` for no limit. Defaults to 100.
  643. :param error_out: Abort with a ``404 Not Found`` error if no items are returned
  644. and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if
  645. either are not ints.
  646. :param count: Calculate the total number of values by issuing an extra count
  647. query. For very complex queries this may be inaccurate or slow, so it can be
  648. disabled and set manually if necessary.
  649. .. versionchanged:: 3.0
  650. The ``count`` query is more efficient.
  651. .. versionadded:: 3.0
  652. """
  653. return SelectPagination(
  654. select=select,
  655. session=self.session(),
  656. page=page,
  657. per_page=per_page,
  658. max_per_page=max_per_page,
  659. error_out=error_out,
  660. count=count,
  661. )
  662. def _call_for_binds(
  663. self, bind_key: str | None | list[str | None], op_name: str
  664. ) -> None:
  665. """Call a method on each metadata.
  666. :meta private:
  667. :param bind_key: A bind key or list of keys. Defaults to all binds.
  668. :param op_name: The name of the method to call.
  669. .. versionchanged:: 3.0
  670. Renamed from ``_execute_for_all_tables``.
  671. """
  672. if bind_key == "__all__":
  673. keys: list[str | None] = list(self.metadatas)
  674. elif bind_key is None or isinstance(bind_key, str):
  675. keys = [bind_key]
  676. else:
  677. keys = bind_key
  678. for key in keys:
  679. try:
  680. engine = self.engines[key]
  681. except KeyError:
  682. message = f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config."
  683. if key is None:
  684. message = f"'SQLALCHEMY_DATABASE_URI' config is not set. {message}"
  685. raise sa_exc.UnboundExecutionError(message) from None
  686. metadata = self.metadatas[key]
  687. getattr(metadata, op_name)(bind=engine)
  688. def create_all(self, bind_key: str | None | list[str | None] = "__all__") -> None:
  689. """Create tables that do not exist in the database by calling
  690. ``metadata.create_all()`` for all or some bind keys. This does not
  691. update existing tables, use a migration library for that.
  692. This requires that a Flask application context is active.
  693. :param bind_key: A bind key or list of keys to create the tables for. Defaults
  694. to all binds.
  695. .. versionchanged:: 3.0
  696. Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``
  697. parameter.
  698. .. versionchanged:: 0.12
  699. Added the ``bind`` and ``app`` parameters.
  700. """
  701. self._call_for_binds(bind_key, "create_all")
  702. def drop_all(self, bind_key: str | None | list[str | None] = "__all__") -> None:
  703. """Drop tables by calling ``metadata.drop_all()`` for all or some bind keys.
  704. This requires that a Flask application context is active.
  705. :param bind_key: A bind key or list of keys to drop the tables from. Defaults to
  706. all binds.
  707. .. versionchanged:: 3.0
  708. Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``
  709. parameter.
  710. .. versionchanged:: 0.12
  711. Added the ``bind`` and ``app`` parameters.
  712. """
  713. self._call_for_binds(bind_key, "drop_all")
  714. def reflect(self, bind_key: str | None | list[str | None] = "__all__") -> None:
  715. """Load table definitions from the database by calling ``metadata.reflect()``
  716. for all or some bind keys.
  717. This requires that a Flask application context is active.
  718. :param bind_key: A bind key or list of keys to reflect the tables from. Defaults
  719. to all binds.
  720. .. versionchanged:: 3.0
  721. Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app``
  722. parameter.
  723. .. versionchanged:: 0.12
  724. Added the ``bind`` and ``app`` parameters.
  725. """
  726. self._call_for_binds(bind_key, "reflect")
  727. def _set_rel_query(self, kwargs: dict[str, t.Any]) -> None:
  728. """Apply the extension's :attr:`Query` class as the default for relationships
  729. and backrefs.
  730. :meta private:
  731. """
  732. kwargs.setdefault("query_class", self.Query)
  733. if "backref" in kwargs:
  734. backref = kwargs["backref"]
  735. if isinstance(backref, str):
  736. backref = (backref, {})
  737. backref[1].setdefault("query_class", self.Query)
  738. def relationship(
  739. self, *args: t.Any, **kwargs: t.Any
  740. ) -> sa_orm.RelationshipProperty[t.Any]:
  741. """A :func:`sqlalchemy.orm.relationship` that applies this extension's
  742. :attr:`Query` class for dynamic relationships and backrefs.
  743. .. versionchanged:: 3.0
  744. The :attr:`Query` class is set on ``backref``.
  745. """
  746. self._set_rel_query(kwargs)
  747. return sa_orm.relationship(*args, **kwargs)
  748. def dynamic_loader(
  749. self, argument: t.Any, **kwargs: t.Any
  750. ) -> sa_orm.RelationshipProperty[t.Any]:
  751. """A :func:`sqlalchemy.orm.dynamic_loader` that applies this extension's
  752. :attr:`Query` class for relationships and backrefs.
  753. .. versionchanged:: 3.0
  754. The :attr:`Query` class is set on ``backref``.
  755. """
  756. self._set_rel_query(kwargs)
  757. return sa_orm.dynamic_loader(argument, **kwargs)
  758. def _relation(
  759. self, *args: t.Any, **kwargs: t.Any
  760. ) -> sa_orm.RelationshipProperty[t.Any]:
  761. """A :func:`sqlalchemy.orm.relationship` that applies this extension's
  762. :attr:`Query` class for dynamic relationships and backrefs.
  763. SQLAlchemy 2.0 removes this name, use ``relationship`` instead.
  764. :meta private:
  765. .. versionchanged:: 3.0
  766. The :attr:`Query` class is set on ``backref``.
  767. """
  768. self._set_rel_query(kwargs)
  769. f = sa_orm.relationship
  770. return f(*args, **kwargs)
  771. def __getattr__(self, name: str) -> t.Any:
  772. if name == "relation":
  773. return self._relation
  774. if name == "event":
  775. return sa_event
  776. if name.startswith("_"):
  777. raise AttributeError(name)
  778. for mod in (sa, sa_orm):
  779. if hasattr(mod, name):
  780. return getattr(mod, name)
  781. raise AttributeError(name)