command.py 21 KB

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