command.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. # mypy: allow-untyped-defs, allow-untyped-calls
  2. from __future__ import annotations
  3. import os
  4. import pathlib
  5. from typing import List
  6. from typing import Optional
  7. from typing import TYPE_CHECKING
  8. from typing import Union
  9. from . import autogenerate as autogen
  10. from . import util
  11. from .runtime.environment import EnvironmentContext
  12. from .script import ScriptDirectory
  13. from .util import compat
  14. if TYPE_CHECKING:
  15. from alembic.config import Config
  16. from alembic.script.base import Script
  17. from alembic.script.revision import _RevIdType
  18. from .runtime.environment import ProcessRevisionDirectiveFn
  19. def list_templates(config: Config) -> None:
  20. """List available templates.
  21. :param config: a :class:`.Config` object.
  22. """
  23. config.print_stdout("Available templates:\n")
  24. for tempname in config._get_template_path().iterdir():
  25. with (tempname / "README").open() as readme:
  26. synopsis = next(readme).rstrip()
  27. config.print_stdout("%s - %s", tempname.name, synopsis)
  28. config.print_stdout("\nTemplates are used via the 'init' command, e.g.:")
  29. config.print_stdout("\n alembic init --template generic ./scripts")
  30. def init(
  31. config: Config,
  32. directory: str,
  33. template: str = "generic",
  34. package: bool = False,
  35. ) -> None:
  36. """Initialize a new scripts directory.
  37. :param config: a :class:`.Config` object.
  38. :param directory: string path of the target directory.
  39. :param template: string name of the migration environment template to
  40. use.
  41. :param package: when True, write ``__init__.py`` files into the
  42. environment location as well as the versions/ location.
  43. """
  44. directory_path = pathlib.Path(directory)
  45. if directory_path.exists() and list(directory_path.iterdir()):
  46. raise util.CommandError(
  47. "Directory %s already exists and is not empty" % directory_path
  48. )
  49. template_path = config._get_template_path() / template
  50. if not template_path.exists():
  51. raise util.CommandError(f"No such template {template_path}")
  52. # left as os.access() to suit unit test mocking
  53. if not os.access(directory_path, os.F_OK):
  54. with util.status(
  55. f"Creating directory {directory_path.absolute()}",
  56. **config.messaging_opts,
  57. ):
  58. os.makedirs(directory_path)
  59. versions = directory_path / "versions"
  60. with util.status(
  61. f"Creating directory {versions.absolute()}",
  62. **config.messaging_opts,
  63. ):
  64. os.makedirs(versions)
  65. if not directory_path.is_absolute():
  66. # for non-absolute path, state config file in .ini / pyproject
  67. # as relative to the %(here)s token, which is where the config
  68. # file itself would be
  69. if config._config_file_path is not None:
  70. rel_dir = compat.path_relative_to(
  71. directory_path.absolute(),
  72. config._config_file_path.absolute().parent,
  73. walk_up=True,
  74. )
  75. ini_script_location_directory = ("%(here)s" / rel_dir).as_posix()
  76. if config._toml_file_path is not None:
  77. rel_dir = compat.path_relative_to(
  78. directory_path.absolute(),
  79. config._toml_file_path.absolute().parent,
  80. walk_up=True,
  81. )
  82. toml_script_location_directory = ("%(here)s" / rel_dir).as_posix()
  83. else:
  84. ini_script_location_directory = directory_path.as_posix()
  85. toml_script_location_directory = directory_path.as_posix()
  86. script = ScriptDirectory(directory_path)
  87. has_toml = False
  88. config_file: pathlib.Path | None = None
  89. for file_path in template_path.iterdir():
  90. file_ = file_path.name
  91. if file_ == "alembic.ini.mako":
  92. assert config.config_file_name is not None
  93. config_file = pathlib.Path(config.config_file_name).absolute()
  94. if config_file.exists():
  95. util.msg(
  96. f"File {config_file} already exists, skipping",
  97. **config.messaging_opts,
  98. )
  99. else:
  100. script._generate_template(
  101. file_path,
  102. config_file,
  103. script_location=ini_script_location_directory,
  104. )
  105. elif file_ == "pyproject.toml.mako":
  106. has_toml = True
  107. assert config._toml_file_path is not None
  108. toml_path = config._toml_file_path.absolute()
  109. if toml_path.exists():
  110. # left as open() to suit unit test mocking
  111. with open(toml_path, "rb") as f:
  112. toml_data = compat.tomllib.load(f)
  113. if "tool" in toml_data and "alembic" in toml_data["tool"]:
  114. util.msg(
  115. f"File {toml_path} already exists "
  116. "and already has a [tool.alembic] section, "
  117. "skipping",
  118. )
  119. continue
  120. script._append_template(
  121. file_path,
  122. toml_path,
  123. script_location=toml_script_location_directory,
  124. )
  125. else:
  126. script._generate_template(
  127. file_path,
  128. toml_path,
  129. script_location=toml_script_location_directory,
  130. )
  131. elif file_path.is_file():
  132. output_file = directory_path / file_
  133. script._copy_file(file_path, output_file)
  134. if package:
  135. for path in [
  136. directory_path.absolute() / "__init__.py",
  137. versions.absolute() / "__init__.py",
  138. ]:
  139. with util.status(f"Adding {path!s}", **config.messaging_opts):
  140. # left as open() to suit unit test mocking
  141. with open(path, "w"):
  142. pass
  143. assert config_file is not None
  144. if has_toml:
  145. util.msg(
  146. f"Please edit configuration settings in {toml_path} and "
  147. "configuration/connection/logging "
  148. f"settings in {config_file} before proceeding.",
  149. **config.messaging_opts,
  150. )
  151. else:
  152. util.msg(
  153. "Please edit configuration/connection/logging "
  154. f"settings in {config_file} before proceeding.",
  155. **config.messaging_opts,
  156. )
  157. def revision(
  158. config: Config,
  159. message: Optional[str] = None,
  160. autogenerate: bool = False,
  161. sql: bool = False,
  162. head: str = "head",
  163. splice: bool = False,
  164. branch_label: Optional[_RevIdType] = None,
  165. version_path: Union[str, os.PathLike[str], None] = None,
  166. rev_id: Optional[str] = None,
  167. depends_on: Optional[str] = None,
  168. process_revision_directives: Optional[ProcessRevisionDirectiveFn] = None,
  169. ) -> Union[Optional[Script], List[Optional[Script]]]:
  170. """Create a new revision file.
  171. :param config: a :class:`.Config` object.
  172. :param message: string message to apply to the revision; this is the
  173. ``-m`` option to ``alembic revision``.
  174. :param autogenerate: whether or not to autogenerate the script from
  175. the database; this is the ``--autogenerate`` option to
  176. ``alembic revision``.
  177. :param sql: whether to dump the script out as a SQL string; when specified,
  178. the script is dumped to stdout. This is the ``--sql`` option to
  179. ``alembic revision``.
  180. :param head: head revision to build the new revision upon as a parent;
  181. this is the ``--head`` option to ``alembic revision``.
  182. :param splice: whether or not the new revision should be made into a
  183. new head of its own; is required when the given ``head`` is not itself
  184. a head. This is the ``--splice`` option to ``alembic revision``.
  185. :param branch_label: string label to apply to the branch; this is the
  186. ``--branch-label`` option to ``alembic revision``.
  187. :param version_path: string symbol identifying a specific version path
  188. from the configuration; this is the ``--version-path`` option to
  189. ``alembic revision``.
  190. :param rev_id: optional revision identifier to use instead of having
  191. one generated; this is the ``--rev-id`` option to ``alembic revision``.
  192. :param depends_on: optional list of "depends on" identifiers; this is the
  193. ``--depends-on`` option to ``alembic revision``.
  194. :param process_revision_directives: this is a callable that takes the
  195. same form as the callable described at
  196. :paramref:`.EnvironmentContext.configure.process_revision_directives`;
  197. will be applied to the structure generated by the revision process
  198. where it can be altered programmatically. Note that unlike all
  199. the other parameters, this option is only available via programmatic
  200. use of :func:`.command.revision`.
  201. """
  202. script_directory = ScriptDirectory.from_config(config)
  203. command_args = dict(
  204. message=message,
  205. autogenerate=autogenerate,
  206. sql=sql,
  207. head=head,
  208. splice=splice,
  209. branch_label=branch_label,
  210. version_path=version_path,
  211. rev_id=rev_id,
  212. depends_on=depends_on,
  213. )
  214. revision_context = autogen.RevisionContext(
  215. config,
  216. script_directory,
  217. command_args,
  218. process_revision_directives=process_revision_directives,
  219. )
  220. environment = util.asbool(
  221. config.get_alembic_option("revision_environment")
  222. )
  223. if autogenerate:
  224. environment = True
  225. if sql:
  226. raise util.CommandError(
  227. "Using --sql with --autogenerate does not make any sense"
  228. )
  229. def retrieve_migrations(rev, context):
  230. revision_context.run_autogenerate(rev, context)
  231. return []
  232. elif environment:
  233. def retrieve_migrations(rev, context):
  234. revision_context.run_no_autogenerate(rev, context)
  235. return []
  236. elif sql:
  237. raise util.CommandError(
  238. "Using --sql with the revision command when "
  239. "revision_environment is not configured does not make any sense"
  240. )
  241. if environment:
  242. with EnvironmentContext(
  243. config,
  244. script_directory,
  245. fn=retrieve_migrations,
  246. as_sql=sql,
  247. template_args=revision_context.template_args,
  248. revision_context=revision_context,
  249. ):
  250. script_directory.run_env()
  251. # the revision_context now has MigrationScript structure(s) present.
  252. # these could theoretically be further processed / rewritten *here*,
  253. # in addition to the hooks present within each run_migrations() call,
  254. # or at the end of env.py run_migrations_online().
  255. scripts = [script for script in revision_context.generate_scripts()]
  256. if len(scripts) == 1:
  257. return scripts[0]
  258. else:
  259. return scripts
  260. def check(config: "Config") -> None:
  261. """Check if revision command with autogenerate has pending upgrade ops.
  262. :param config: a :class:`.Config` object.
  263. .. versionadded:: 1.9.0
  264. """
  265. script_directory = ScriptDirectory.from_config(config)
  266. command_args = dict(
  267. message=None,
  268. autogenerate=True,
  269. sql=False,
  270. head="head",
  271. splice=False,
  272. branch_label=None,
  273. version_path=None,
  274. rev_id=None,
  275. depends_on=None,
  276. )
  277. revision_context = autogen.RevisionContext(
  278. config,
  279. script_directory,
  280. command_args,
  281. )
  282. def retrieve_migrations(rev, context):
  283. revision_context.run_autogenerate(rev, context)
  284. return []
  285. with EnvironmentContext(
  286. config,
  287. script_directory,
  288. fn=retrieve_migrations,
  289. as_sql=False,
  290. template_args=revision_context.template_args,
  291. revision_context=revision_context,
  292. ):
  293. script_directory.run_env()
  294. # the revision_context now has MigrationScript structure(s) present.
  295. migration_script = revision_context.generated_revisions[-1]
  296. diffs = []
  297. for upgrade_ops in migration_script.upgrade_ops_list:
  298. diffs.extend(upgrade_ops.as_diffs())
  299. if diffs:
  300. raise util.AutogenerateDiffsDetected(
  301. f"New upgrade operations detected: {diffs}",
  302. revision_context=revision_context,
  303. diffs=diffs,
  304. )
  305. else:
  306. config.print_stdout("No new upgrade operations detected.")
  307. def merge(
  308. config: Config,
  309. revisions: _RevIdType,
  310. message: Optional[str] = None,
  311. branch_label: Optional[_RevIdType] = None,
  312. rev_id: Optional[str] = None,
  313. ) -> Optional[Script]:
  314. """Merge two revisions together. Creates a new migration file.
  315. :param config: a :class:`.Config` instance
  316. :param revisions: The revisions to merge.
  317. :param message: string message to apply to the revision.
  318. :param branch_label: string label name to apply to the new revision.
  319. :param rev_id: hardcoded revision identifier instead of generating a new
  320. one.
  321. .. seealso::
  322. :ref:`branches`
  323. """
  324. script = ScriptDirectory.from_config(config)
  325. template_args = {
  326. "config": config # Let templates use config for
  327. # e.g. multiple databases
  328. }
  329. environment = util.asbool(
  330. config.get_alembic_option("revision_environment")
  331. )
  332. if environment:
  333. def nothing(rev, context):
  334. return []
  335. with EnvironmentContext(
  336. config,
  337. script,
  338. fn=nothing,
  339. as_sql=False,
  340. template_args=template_args,
  341. ):
  342. script.run_env()
  343. return script.generate_revision(
  344. rev_id or util.rev_id(),
  345. message,
  346. refresh=True,
  347. head=revisions,
  348. branch_labels=branch_label,
  349. **template_args, # type:ignore[arg-type]
  350. )
  351. def upgrade(
  352. config: Config,
  353. revision: str,
  354. sql: bool = False,
  355. tag: Optional[str] = None,
  356. ) -> None:
  357. """Upgrade to a later version.
  358. :param config: a :class:`.Config` instance.
  359. :param revision: string revision target or range for --sql mode. May be
  360. ``"heads"`` to target the most recent revision(s).
  361. :param sql: if True, use ``--sql`` mode.
  362. :param tag: an arbitrary "tag" that can be intercepted by custom
  363. ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
  364. method.
  365. """
  366. script = ScriptDirectory.from_config(config)
  367. starting_rev = None
  368. if ":" in revision:
  369. if not sql:
  370. raise util.CommandError("Range revision not allowed")
  371. starting_rev, revision = revision.split(":", 2)
  372. def upgrade(rev, context):
  373. return script._upgrade_revs(revision, rev)
  374. with EnvironmentContext(
  375. config,
  376. script,
  377. fn=upgrade,
  378. as_sql=sql,
  379. starting_rev=starting_rev,
  380. destination_rev=revision,
  381. tag=tag,
  382. ):
  383. script.run_env()
  384. def downgrade(
  385. config: Config,
  386. revision: str,
  387. sql: bool = False,
  388. tag: Optional[str] = None,
  389. ) -> None:
  390. """Revert to a previous version.
  391. :param config: a :class:`.Config` instance.
  392. :param revision: string revision target or range for --sql mode. May
  393. be ``"base"`` to target the first revision.
  394. :param sql: if True, use ``--sql`` mode.
  395. :param tag: an arbitrary "tag" that can be intercepted by custom
  396. ``env.py`` scripts via the :meth:`.EnvironmentContext.get_tag_argument`
  397. method.
  398. """
  399. script = ScriptDirectory.from_config(config)
  400. starting_rev = None
  401. if ":" in revision:
  402. if not sql:
  403. raise util.CommandError("Range revision not allowed")
  404. starting_rev, revision = revision.split(":", 2)
  405. elif sql:
  406. raise util.CommandError(
  407. "downgrade with --sql requires <fromrev>:<torev>"
  408. )
  409. def downgrade(rev, context):
  410. return script._downgrade_revs(revision, rev)
  411. with EnvironmentContext(
  412. config,
  413. script,
  414. fn=downgrade,
  415. as_sql=sql,
  416. starting_rev=starting_rev,
  417. destination_rev=revision,
  418. tag=tag,
  419. ):
  420. script.run_env()
  421. def show(config: Config, rev: str) -> None:
  422. """Show the revision(s) denoted by the given symbol.
  423. :param config: a :class:`.Config` instance.
  424. :param rev: string revision target. May be ``"current"`` to show the
  425. revision(s) currently applied in the database.
  426. """
  427. script = ScriptDirectory.from_config(config)
  428. if rev == "current":
  429. def show_current(rev, context):
  430. for sc in script.get_revisions(rev):
  431. config.print_stdout(sc.log_entry)
  432. return []
  433. with EnvironmentContext(config, script, fn=show_current):
  434. script.run_env()
  435. else:
  436. for sc in script.get_revisions(rev):
  437. config.print_stdout(sc.log_entry)
  438. def history(
  439. config: Config,
  440. rev_range: Optional[str] = None,
  441. verbose: bool = False,
  442. indicate_current: bool = False,
  443. ) -> None:
  444. """List changeset scripts in chronological order.
  445. :param config: a :class:`.Config` instance.
  446. :param rev_range: string revision range.
  447. :param verbose: output in verbose mode.
  448. :param indicate_current: indicate current revision.
  449. """
  450. base: Optional[str]
  451. head: Optional[str]
  452. script = ScriptDirectory.from_config(config)
  453. if rev_range is not None:
  454. if ":" not in rev_range:
  455. raise util.CommandError(
  456. "History range requires [start]:[end], " "[start]:, or :[end]"
  457. )
  458. base, head = rev_range.strip().split(":")
  459. else:
  460. base = head = None
  461. environment = (
  462. util.asbool(config.get_alembic_option("revision_environment"))
  463. or indicate_current
  464. )
  465. def _display_history(config, script, base, head, currents=()):
  466. for sc in script.walk_revisions(
  467. base=base or "base", head=head or "heads"
  468. ):
  469. if indicate_current:
  470. sc._db_current_indicator = sc.revision in currents
  471. config.print_stdout(
  472. sc.cmd_format(
  473. verbose=verbose,
  474. include_branches=True,
  475. include_doc=True,
  476. include_parents=True,
  477. )
  478. )
  479. def _display_history_w_current(config, script, base, head):
  480. def _display_current_history(rev, context):
  481. if head == "current":
  482. _display_history(config, script, base, rev, rev)
  483. elif base == "current":
  484. _display_history(config, script, rev, head, rev)
  485. else:
  486. _display_history(config, script, base, head, rev)
  487. return []
  488. with EnvironmentContext(config, script, fn=_display_current_history):
  489. script.run_env()
  490. if base == "current" or head == "current" or environment:
  491. _display_history_w_current(config, script, base, head)
  492. else:
  493. _display_history(config, script, base, head)
  494. def heads(
  495. config: Config, verbose: bool = False, resolve_dependencies: bool = False
  496. ) -> None:
  497. """Show current available heads in the script directory.
  498. :param config: a :class:`.Config` instance.
  499. :param verbose: output in verbose mode.
  500. :param resolve_dependencies: treat dependency version as down revisions.
  501. """
  502. script = ScriptDirectory.from_config(config)
  503. if resolve_dependencies:
  504. heads = script.get_revisions("heads")
  505. else:
  506. heads = script.get_revisions(script.get_heads())
  507. for rev in heads:
  508. config.print_stdout(
  509. rev.cmd_format(
  510. verbose, include_branches=True, tree_indicators=False
  511. )
  512. )
  513. def branches(config: Config, verbose: bool = False) -> None:
  514. """Show current branch points.
  515. :param config: a :class:`.Config` instance.
  516. :param verbose: output in verbose mode.
  517. """
  518. script = ScriptDirectory.from_config(config)
  519. for sc in script.walk_revisions():
  520. if sc.is_branch_point:
  521. config.print_stdout(
  522. "%s\n%s\n",
  523. sc.cmd_format(verbose, include_branches=True),
  524. "\n".join(
  525. "%s -> %s"
  526. % (
  527. " " * len(str(sc.revision)),
  528. rev_obj.cmd_format(
  529. False, include_branches=True, include_doc=verbose
  530. ),
  531. )
  532. for rev_obj in (
  533. script.get_revision(rev) for rev in sc.nextrev
  534. )
  535. ),
  536. )
  537. def current(config: Config, verbose: bool = False) -> None:
  538. """Display the current revision for a database.
  539. :param config: a :class:`.Config` instance.
  540. :param verbose: output in verbose mode.
  541. """
  542. script = ScriptDirectory.from_config(config)
  543. def display_version(rev, context):
  544. if verbose:
  545. config.print_stdout(
  546. "Current revision(s) for %s:",
  547. util.obfuscate_url_pw(context.connection.engine.url),
  548. )
  549. for rev in script.get_all_current(rev):
  550. config.print_stdout(rev.cmd_format(verbose))
  551. return []
  552. with EnvironmentContext(
  553. config, script, fn=display_version, dont_mutate=True
  554. ):
  555. script.run_env()
  556. def stamp(
  557. config: Config,
  558. revision: _RevIdType,
  559. sql: bool = False,
  560. tag: Optional[str] = None,
  561. purge: bool = False,
  562. ) -> None:
  563. """'stamp' the revision table with the given revision; don't
  564. run any migrations.
  565. :param config: a :class:`.Config` instance.
  566. :param revision: target revision or list of revisions. May be a list
  567. to indicate stamping of multiple branch heads; may be ``"base"``
  568. to remove all revisions from the table or ``"heads"`` to stamp the
  569. most recent revision(s).
  570. .. note:: this parameter is called "revisions" in the command line
  571. interface.
  572. :param sql: use ``--sql`` mode
  573. :param tag: an arbitrary "tag" that can be intercepted by custom
  574. ``env.py`` scripts via the :class:`.EnvironmentContext.get_tag_argument`
  575. method.
  576. :param purge: delete all entries in the version table before stamping.
  577. """
  578. script = ScriptDirectory.from_config(config)
  579. if sql:
  580. destination_revs = []
  581. starting_rev = None
  582. for _revision in util.to_list(revision):
  583. if ":" in _revision:
  584. srev, _revision = _revision.split(":", 2)
  585. if starting_rev != srev:
  586. if starting_rev is None:
  587. starting_rev = srev
  588. else:
  589. raise util.CommandError(
  590. "Stamp operation with --sql only supports a "
  591. "single starting revision at a time"
  592. )
  593. destination_revs.append(_revision)
  594. else:
  595. destination_revs = util.to_list(revision)
  596. def do_stamp(rev, context):
  597. return script._stamp_revs(util.to_tuple(destination_revs), rev)
  598. with EnvironmentContext(
  599. config,
  600. script,
  601. fn=do_stamp,
  602. as_sql=sql,
  603. starting_rev=starting_rev if sql else None,
  604. destination_rev=util.to_tuple(destination_revs),
  605. tag=tag,
  606. purge=purge,
  607. ):
  608. script.run_env()
  609. def edit(config: Config, rev: str) -> None:
  610. """Edit revision script(s) using $EDITOR.
  611. :param config: a :class:`.Config` instance.
  612. :param rev: target revision.
  613. """
  614. script = ScriptDirectory.from_config(config)
  615. if rev == "current":
  616. def edit_current(rev, context):
  617. if not rev:
  618. raise util.CommandError("No current revisions")
  619. for sc in script.get_revisions(rev):
  620. util.open_in_editor(sc.path)
  621. return []
  622. with EnvironmentContext(config, script, fn=edit_current):
  623. script.run_env()
  624. else:
  625. revs = script.get_revisions(rev)
  626. if not revs:
  627. raise util.CommandError(
  628. "No revision files indicated by symbol '%s'" % rev
  629. )
  630. for sc in revs:
  631. assert sc
  632. util.open_in_editor(sc.path)
  633. def ensure_version(config: Config, sql: bool = False) -> None:
  634. """Create the alembic version table if it doesn't exist already .
  635. :param config: a :class:`.Config` instance.
  636. :param sql: use ``--sql`` mode.
  637. .. versionadded:: 1.7.6
  638. """
  639. script = ScriptDirectory.from_config(config)
  640. def do_ensure_version(rev, context):
  641. context._ensure_version_table()
  642. return []
  643. with EnvironmentContext(
  644. config,
  645. script,
  646. fn=do_ensure_version,
  647. as_sql=sql,
  648. ):
  649. script.run_env()