]> git.ipfire.org Git - thirdparty/sqlalchemy/alembic.git/commitdiff
docs
authorMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Nov 2011 20:16:31 +0000 (12:16 -0800)
committerMike Bayer <mike_mp@zzzcomputing.com>
Tue, 8 Nov 2011 20:16:31 +0000 (12:16 -0800)
.hgignore
alembic/command.py
alembic/context.py
alembic/op.py
alembic/script.py
alembic/util.py
docs/build/api.rst [new file with mode: 0644]
docs/build/conf.py
docs/build/index.rst
docs/build/ops.rst [new file with mode: 0644]
docs/build/tutorial.rst [new file with mode: 0644]

index 45f1fab908578a9387e40657f0ad4de9ddeb02dc..89d44e2eee45bbdb68aea5dcda7fb9524f37682a 100644 (file)
--- a/.hgignore
+++ b/.hgignore
@@ -1,7 +1,7 @@
 syntax:regexp
 ^build/
 ^dist/
-^doc/build/output
+^docs/build/output
 .pyc$
 .orig$
 .egg-info
index db7301680a3f8dbe34c2c8eab3694c62e690e3bd..c0743ae22f27783b41d62860f91015e86ad068da 100644 (file)
@@ -140,6 +140,11 @@ def stamp(config, revision, sql=False):
     script.run_env()
 
 def splice(config, parent, child):
-    """'splice' two branches, creating a new revision file."""
+    """'splice' two branches, creating a new revision file.
+    
+    this command isn't implemented right now.
+    
+    """
+    raise NotImplementedError()
 
 
index 0b33a9e107a8d28e2828faeab7bd6cf5345f2a95..bdb67fb8a3afc045e9348018ee217b07a049cf6c 100644 (file)
@@ -132,22 +132,22 @@ class DefaultContext(object):
             return self.connection
 
     def alter_column(self, table_name, column_name, 
-                        nullable=util.NO_VALUE,
-                        server_default=util.NO_VALUE,
-                        name=util.NO_VALUE,
-                        type_=util.NO_VALUE,
+                        nullable=None,
+                        server_default=False,
+                        name=None,
+                        type_=None,
                         schema=None,
     ):
 
-        if nullable is not util.NO_VALUE:
+        if nullable is not None:
             self._exec(base.ColumnNullable(table_name, column_name, 
                                 nullable, schema=schema))
-        if server_default is not util.NO_VALUE:
+        if server_default is not False:
             self._exec(base.ColumnDefault(
                                 table_name, column_name, server_default,
                                 schema=schema
                             ))
-        if type_ is not util.NO_VALUE:
+        if type_ is not None:
             self._exec(base.ColumnType(
                                 table_name, column_name, type_, schema=schema
                             ))
@@ -187,11 +187,27 @@ def _render_literal_bindparam(element, compiler, **kw):
     return compiler.render_literal_bindparam(element, **kw)
 
 def opts(cfg, **kw):
+    """Set up options that will be used by the :func:`.configure_connection`
+    function.
+    
+    This basically sets some global variables.
+    
+    """
     global _context_opts, config
     _context_opts = kw
     config = cfg
 
 def configure_connection(connection):
+    """Configure the migration environment against a specific
+    database connection, an instance of :class:`sqlalchemy.engine.Connection`.
+    
+    This function is typically called from the ``env.py``
+    script within a migration environment.  It can be called
+    multiple times for an invocation.  The most recent :class:`~sqlalchemy.engine.Connection`
+    for which it was called is the one that will be operated upon
+    by the next call to :func:`.run_migrations`.
+    
+    """
     global _context
     from alembic.ddl import base
     _context = _context_impls.get(
index 2f3d633bf4849facdbfb0d4b80372b7b9e4cd066..beea769dc7e4526883e37382edb0418f03180786 100644 (file)
@@ -82,12 +82,12 @@ def _ensure_table_for_fk(metadata, fk):
 
 
 def alter_column(table_name, column_name, 
-                    nullable=util.NO_VALUE,
-                    server_default=util.NO_VALUE,
-                    name=util.NO_VALUE,
-                    type_=util.NO_VALUE
+                    nullable=None,
+                    server_default=False,
+                    name=None,
+                    type_=None
 ):
-    """Issue ALTER COLUMN using the current change context."""
+    """Issue an "alter column" instruction using the current change context."""
 
     get_context().alter_column(table_name, column_name, 
         nullable=nullable,
@@ -97,6 +97,16 @@ def alter_column(table_name, column_name,
     )
 
 def add_column(table_name, column):
+    """Issue an "add column" instruction using the current change context.
+    
+    e.g.::
+    
+        add_column('organization', 
+            Column('account_id', INTEGER, ForeignKey('accounts.id'))
+        )        
+    
+    """
+
     t = _table(table_name, column)
     get_context().add_column(
         table_name,
@@ -106,43 +116,119 @@ def add_column(table_name, column):
         get_context().add_constraint(constraint)
 
 def drop_column(table_name, column_name):
+    """Issue a "drop column" instruction using the current change context.
+    
+    e.g.::
+    
+        drop_column('organization', 'account_id')
+    
+    """
+
     get_context().drop_column(
         table_name,
         _column(column_name, NULLTYPE)
     )
 
 def add_constraint(table_name, constraint):
+    """Issue an "add constraint" instruction using the current change context."""
+
     _ensure_table_for_constraint(table_name, constraint)
     get_context().add_constraint(
         constraint
     )
 
 def create_foreign_key(name, source, referent, local_cols, remote_cols):
+    """Issue a "create foreign key" instruction using the current change context."""
+
     get_context().add_constraint(
                 _foreign_key_constraint(name, source, referent, 
                         local_cols, remote_cols)
             )
 
 def create_unique_constraint(name, source, local_cols):
+    """Issue a "create unique constraint" instruction using the current change context."""
+
     get_context().add_constraint(
                 _unique_constraint(name, source, local_cols)
             )
 
 def create_table(name, *columns, **kw):
+    """Issue a "create table" instruction using the current change context.
+    
+    This directive receives an argument list similar to that of the 
+    traditional :class:`sqlalchemy.schema.Table` construct, but without the
+    metadata::
+        
+        from sqlalchemy import INTEGER, VARCHAR, NVARCHAR, Column
+        from alembic.op import create_table
+
+        create_table(
+            'accounts',
+            Column('id', INTEGER, primary_key=True),
+            Column('name', VARCHAR(50), nullable=False),
+            Column('description', NVARCHAR(200))
+        )
+
+    """
+
     get_context().create_table(
         _table(name, *columns, **kw)
     )
 
 def drop_table(name, *columns, **kw):
+    """Issue a "drop table" instruction using the current change context.
+    
+    
+    e.g.::
+    
+        drop_table("accounts")
+        
+    """
     get_context().drop_table(
         _table(name, *columns, **kw)
     )
 
 def bulk_insert(table, rows):
+    """Issue a "bulk insert" operation using the current change context.
+    
+    This provides a means of representing an INSERT of multiple rows
+    which works equally well in the context of executing on a live 
+    connection as well as that of generating a SQL script.   In the 
+    case of a SQL script, the values are rendered inline into the 
+    statement.
+    
+    e.g.::
+    
+        from myapp.mymodel import accounts_table
+        from datetime import date
+        
+        bulk_insert(accounts_table,
+            [
+                {'id':1, 'name':'John Smith', 'create_date':date(2010, 10, 5)},
+                {'id':2, 'name':'Ed Williams', 'create_date':date(2007, 5, 27)},
+                {'id':3, 'name':'Wendy Jones', 'create_date':date(2008, 8, 15)},
+            ]
+        )
+      """
     get_context().bulk_insert(table, rows)
 
 def execute(sql):
+    """Execute the given SQL using the current change context.
+    
+    In a SQL script context, the statement is emitted directly to the 
+    output stream.
+    
+    """
     get_context().execute(sql)
 
 def get_bind():
+    """Return the current 'bind'.
+    
+    Under normal circumstances, this is the 
+    :class:`sqlalchemy.engine.Connection` currently being used
+    to emit SQL to the database.
+    
+    In a SQL script context, this value is ``None``. [TODO: verify this]
+    
+    """
     return get_context().bind
\ No newline at end of file
index 9784d68e2682141a1df513021bd0a6fab1ed75a3..a8ac9a0436043a882a2143fe51c81c22aca6b740 100644 (file)
@@ -99,6 +99,18 @@ class ScriptDirectory(object):
             ]
 
     def run_env(self):
+        """Run the script environment.
+        
+        This basically runs the ``env.py`` script present
+        in the migration environment.   It is called exclusively
+        by the command functions in :mod:`alembic.command`.
+        
+        As ``env.py`` runs :func:`.context.configure_connection`, 
+        the connection environment should be set up first.   This
+        is typically achieved using the :func:`.context.opts`.
+        
+        
+        """
         util.load_python_file(self.dir, 'env.py')
 
     @util.memoized_property
index 5a598cdf491dc4deb7a9ca9cfdbf0fe4ae776621..cda77c2deda76f2a5025b61f4e1c5d15358dff27 100644 (file)
@@ -2,7 +2,6 @@ from mako.template import Template
 import sys
 import os
 import textwrap
-from sqlalchemy import util
 from sqlalchemy.engine import url
 import imp
 import warnings
@@ -11,7 +10,6 @@ import time
 import random
 import uuid
 
-NO_VALUE = util.symbol("NO_VALUE")
 
 class CommandError(Exception):
     pass
diff --git a/docs/build/api.rst b/docs/build/api.rst
new file mode 100644 (file)
index 0000000..42fdaab
--- /dev/null
@@ -0,0 +1,6 @@
+===========
+API Details
+===========
+
+This section describes some key functions used within the migration process.
+
index 1039fba9f1c505d2f60de15a30107cad2f9394d6..f5374d8dad6b15ce2f10bfe64299b6223177600b 100644 (file)
@@ -99,7 +99,7 @@ pygments_style = 'sphinx'
 
 # The theme to use for HTML and HTML Help pages.  Major themes that come with
 # Sphinx are currently 'default' and 'sphinxdoc'.
-html_theme = 'sphinxdoc'
+html_theme = 'default'
 
 # Theme options are theme-specific and customize the look and feel of a theme
 # further.  For a list of options available for each theme, see the
index d9a67c6a4fe9c545665f49fb01493bbf64d7c783..805724a135c8587a99d4f2807461d968f3798603 100644 (file)
@@ -1,8 +1,4 @@
-.. Alembic documentation master file, created by
-   sphinx-quickstart on Sat May  1 12:47:55 2010.
-   You can adapt this file completely to your liking, but it should at least
-   contain the root `toctree` directive.
-
+===================================
 Welcome to Alembic's documentation!
 ===================================
 
@@ -11,6 +7,10 @@ Contents:
 .. toctree::
    :maxdepth: 2
 
+   tutorial
+   ops
+   api
+
 Indices and tables
 ==================
 
diff --git a/docs/build/ops.rst b/docs/build/ops.rst
new file mode 100644 (file)
index 0000000..809e072
--- /dev/null
@@ -0,0 +1,8 @@
+===================
+Operation Reference
+===================
+
+This file provides documentation on Alembic migration directives.
+
+.. automodule:: alembic.op
+    :members:
diff --git a/docs/build/tutorial.rst b/docs/build/tutorial.rst
new file mode 100644 (file)
index 0000000..77b42dc
--- /dev/null
@@ -0,0 +1,311 @@
+========
+Tutorial
+========
+
+Alembic provides for the creation, management, and invocation of *change management* 
+scripts for a relational database, using `SQLAlchemy <http://www.sqlalchemy.org>`_ as the underlying engine.
+This tutorial will provide a full introduction to the theory and usage of this tool.
+
+Installation
+============
+
+Install Alembic with `pip <http://pypi.python.org/pypi/pip>`_ or a similar tool::
+
+    pip install alembic
+
+The install will add the ``alembic`` command to the environment.  All operations with Alembic
+then proceed through the usage of this command.
+
+The Migration Environment
+==========================
+
+Usage of Alembic starts with creation of the *Migration Environment*.  This is a directory of scripts
+that is specific to a particular application.   The migration environment is created just once,
+and is then maintained along with the application's source code itself.   The environment is 
+created using the ``init`` command of Alembic, and is then customizable to suit the specific
+needs of the application.
+
+The structure of this environment, including some generated migration scripts, looks like::
+
+    yourproject/
+        alembic/
+            env.py
+            README
+            script.py.mako
+            versions/
+                3512b954651e.py
+                2b1ae634e5cd.py
+                3adcc9a56557.py
+
+The directory includes these directories/files:
+
+* ``yourproject`` - this is the root of your application's source code, or some directory within it.
+* ``alembic`` - this directory lives within your application's source tree and is the home of the
+  migration environment.   It can be named anything, and a project that uses multiple databases
+  may even have more than one.
+* ``env.py`` - This is a Python script that is run whenever the alembic migration tool is invoked.
+  At the very least, it contains instructions to configure and generate a SQLAlchemy engine,
+  procure a connection from that engine along with a transaction, and to then invoke the migration
+  engine, using the connection as a source of database connectivity.
+
+  The ``env.py`` script is part of the generated environment so that the way migrations run
+  is entirely customizable.   The exact specifics of how to connect are here, as well as 
+  the specifics of how the migration enviroment are invoked.  The script can be modified
+  so that multiple engines can be operated upon, custom arguments can be passed into the
+  migration environment, application-specific libraries and models can be loaded in and 
+  made available.  
+
+  Alembic includes a set of initialization templates which feature different varieties
+  of ``env.py`` for different use cases.
+* ``README`` - included with the various enviromnent templates, should have something
+  informative.
+* ``script.py.mako`` - This is a `Mako <http://www.makotemplates.org>`_ template file which
+  is used to generate new migration scripts.   Whatever is here is used to generate new
+  files within ``versions/``.   This is scriptable so that the structure of each migration
+  file can be controlled, including standard imports to be within each, as well as 
+  changes to the structure of the ``upgrade()`` and ``downgrade()`` functions.  For example,
+  the ``multidb`` environment allows for multiple functions to be generated using a
+  naming scheme ``upgrade_engine1()``, ``upgrade_engine2()``.   
+* ``versions/`` - This directory holds the individual version scripts.  Users of other migration
+  tools may notice that the files here don't use ascending integers, and instead use a 
+  partial GUID approach.   In Alembic, the ordering of version scripts is relative
+  to directives within the scripts themselves, and it is theoretically possible to "splice" version files
+  in between others, allowing migration sequences from different branches to be merged,
+  albeit carefully by hand.
+
+
+Creating an Environment
+=======================
+
+With a basic understanding of what the environment is, we can create one using ``alembic init``.
+This will create an environment using the "generic" template::
+
+    $ cd yourproject
+    $ alembic init alembic
+
+Where above, the ``init`` command was called to generate a migrations directory called ``alembic``::
+
+    Creating directory /path/to/yourproject/alembic...done
+    Creating directory /path/to/yourproject/alembic/versions...done
+    Generating /path/to/yourproject/alembic.ini...done
+    Generating /path/to/yourproject/alembic/env.py...done
+    Generating /path/to/yourproject/alembic/README...done
+    Generating /path/to/yourproject/alembic/script.py.mako...done
+    Please edit configuration/connection/logging settings in
+    '/path/to/yourproject/alembic.ini' before proceeding.
+
+Alembic also includes other environment templates.  These can be listed out using the ``list_templates``
+command::
+
+    $ alembic list_templates
+    Available templates:
+
+    generic - Generic single-database configuration.
+    multidb - Rudimentary multi-database configuration.
+    pylons - Configuration that reads from a Pylons project environment.
+
+    Templates are used via the 'init' command, e.g.:
+
+      alembic init --template pylons ./scripts
+
+Editing the .ini File
+=====================
+
+Alembic placed a file ``alembic.ini`` into the current directory.  This is a file that the ``alembic``
+script looks for when invoked.  This file can be anywhere, either in the same directory
+from which the ``alembic`` script will normally be invoked, or if in a different directory, can
+be specified by using the ``--config`` option to the ``alembic`` runner.
+
+The file generated with the "generic" configuration looks like::
+
+    # A generic, single database configuration.
+
+    [alembic]
+    script_location = alembic
+    sqlalchemy.url = driver://user:pass@localhost/dbname
+
+    # Logging configuration
+    [loggers]
+    keys = root,sqlalchemy,alembic
+
+    [handlers]
+    keys = console
+
+    [formatters]
+    keys = generic
+
+    [logger_root]
+    level = WARN
+    handlers = console
+    qualname =
+
+    [logger_sqlalchemy]
+    level = WARN
+    handlers =
+    qualname = sqlalchemy.engine
+
+    [logger_alembic]
+    level = INFO
+    handlers =
+    qualname = alembic
+
+    [handler_console]
+    class = StreamHandler
+    args = (sys.stderr,)
+    level = NOTSET
+    formatter = generic
+
+    [formatter_generic]
+    format = %(levelname)-5.5s [%(name)s] %(message)s
+    datefmt = %H:%M:%S
+
+This file contains the following features:
+
+* ``[alembic]`` - this is the section read by Alembic to determine configuration.  Alembic
+  itself does not directly read any other areas of the file.
+* ``script_location`` - this is the location of the Alembic environment, relative to 
+  the location of the .ini file [TODO: verify this].   It can also be an absolute
+  file path.  This is the only key required by Alembic in all cases.   The generation 
+  of the .ini file by the command ``alembic init alembic`` automatically placed the 
+  directory name ``alembic`` here.
+* ``sqlalchemy.url`` - A URL to connect to the database via SQLAlchemy.  This key is in fact
+  only referenced within the ``env.py`` file that is specific to the "generic" configuration;
+  a file that can be customized by the developer. A multiple
+  database configuration may respond to multiple keys here, or may reference other sections
+  of the file.
+* ``[loggers]``, ``[handlers]``, ``[formatters]``, ``[logger_*]``, ``[handler_*]``, 
+  ``[formatter_*]`` - these sections are all part of Python's standard logging configuration,
+  the mechanics of which are documented at `Configuration File Format <http://docs.python.org/library/logging.config.html#configuration-file-format>`_.
+  As is the case with the database connection, these directives are used directly as the
+  result of the ``logging.config.fileConfig()`` call present in the fully customizable
+  ``env.py`` script.
+
+For starting up with just a single database and the generic configuration, setting up
+the SQLAlchemy URL is all that's needed::
+
+    sqlalchemy.url = postgresql://scott:tiger@localhost/test
+
+Create a Migration Script
+=========================
+
+With the environment in place we can create a new revision, using ``alembic revision``::
+
+    $ alembic revision -m "add a column"
+    Generating /path/to/yourproject/alembic/versions/1975ea83b712.py...done
+
+A new file 1975ea83b712.py is generated.  Looking inside the file::
+
+    """add a column
+
+    Revision ID: 1975ea83b712
+    Revises: None
+    Create Date: 2011-11-08 11:40:27.089406
+
+    """
+
+    # downgrade revision identifier, used by Alembic.
+    down_revision = None
+
+    from alembic.op import *
+
+    def upgrade():
+        pass
+
+    def downgrade():
+        pass
+
+The file contains some header information, a "downgrade revision identifier", an import
+of basic Alembic directives, and empty ``upgrade()`` and ``downgrade()`` functions.  Our 
+job here is to populate the ``upgrade()`` and ``downgrade()`` functions with directives that
+will apply a set of changes to our database.    Typically, ``upgrade()`` is required
+while ``downgrade()`` is only needed if down-revision capability is desired, though it's
+probably a good idea.
+
+Another thing to notice is the ``down_revision`` variable.  This is how Alembic 
+knows the correct order in which to apply migrations.   When we create the next revision,
+the new file's ``down_revision`` identifier would point to this one::
+
+    # downgrade revision identifier, used by Alembic.
+    down_revision = '1975ea83b712'
+
+Every time Alembic runs an operation against the ``versions/`` directory, it reads all
+the files in, and composes a list based on how the ``down_revision`` identifiers link together,
+with the ``down_revision`` of ``None`` representing the first file.   In theory, if a
+migration environment had thousands of migrations, this could begin to add some latency to 
+startup, but in practice a project should probably prune old migrations anyway
+(see the section :ref:`building_uptodate` for a description on how to do this, while maintaining
+the ability to build the current database fully).
+
+We can then add some directives to our script, suppose adding a new table ``account``::
+
+    from sqlalchemy import Integer, String, Unicode, Column
+
+    def upgrade():
+        create_table(
+            'account',
+            Column('id', Integer, primary_key=True),
+            Column('name', String(50), nullable=False),
+            Column('description', Unicode(200)),
+        )
+
+    def downgrade():
+        drop_table('account')
+
+foo::
+
+    def upgrade():
+        add_column('accounts', 
+            Column('account_id', INTEGER, ForeignKey('accounts.id'))
+        )
+
+    def downgrade():
+        drop_column('organization', 'account_id')
+        drop_table("accounts")
+
+
+
+
+.. _building_uptodate:
+
+Building an Up to Date Database from Scratch
+=============================================
+
+There's a theory of database migrations that says that the revisions in existence for a database should be
+able to go from an entirely blank schema to the finished product, and back again.   Alembic can roll
+this way.   Though we think it's kind of overkill, considering that SQLAlchemy itself can emit 
+the full CREATE statements for any given model using :meth:`.MetaData.create_all`.   If you check out
+a copy of an application, running this will give you the entire database in one shot, without the need
+to run through all those migration files, which are instead tailored towards applying incremental
+changes to an existing database.
+
+Alembic can integrate with a :meth:`.MetaData.create_all` script quite easily.  After running the
+create operation, tell Alembic to create a new version table, and to stamp it with the most recent
+revision (i.e. ``head``)::
+
+    # inside of a "create the database" script, first create
+    # tables:
+    my_metadata.create_all(engine)
+
+    # then, load the Alembic configuration and generate the
+    # version table, "stamping" it with the most recent rev:
+    from alembic.config import Config
+    from alembic import command
+    alembic_cfg = Config("/path/to/yourapp/alembic.ini")
+    command.stamp(alembic_cfg, "head")
+
+When this approach is used, the application can generate the database using normal SQLAlchemy
+techniques instead of iterating through hundreds of migration scripts.   Now, the purpose of the 
+migration scripts is relegated just to movement between versions on out-of-date databases, not
+*new* databases.    You can now remove old migration files that are no longer represented 
+on any existing environments.
+
+To prune old migration files, simply delete the files.   Then, in the earliest, still-remaining
+migration file, set ``down_revision`` to ``None``::
+
+    # replace this:
+    #down_revision = '290696571ad2'
+
+    # with this:
+    down_revision = None
+
+That file now becomes the "base" of the migration series.