diff --git a/CHANGES b/CHANGES index 44baa1e7..a0429946 100644 --- a/CHANGES +++ b/CHANGES @@ -11,6 +11,7 @@ In development - Table names are automatically generated in more cases, including subclassing mixins and abstract models. +- Allow using a custom MetaData object. Version 2.0 ----------- diff --git a/docs/config.rst b/docs/config.rst index 6eee8768..eddbd764 100644 --- a/docs/config.rst +++ b/docs/config.rst @@ -107,3 +107,37 @@ Oracle:: SQLite (note the four leading slashes):: sqlite:////absolute/path/to/foo.db + +Using custom MetaData and naming conventions +-------------------------------------------- + +You can optionally construct the :class:`SQLAlchemy` object with a custom +:class:`~sqlalchemy.schema.MetaData` object. +This allows you to, among other things, +specify a `custom constraint naming convention +`_. +Doing so is important for dealing with database migrations (for instance using +`alembic `_ as stated +`here `_. Since SQL +defines no standard naming conventions, there is no guaranteed nor effective +compatibility by default among database implementations. You can define a +custom naming convention like this as suggested by the SQLAlchemy docs:: + + from sqlalchemy import MetaData + from flask import Flask + from flask.ext.sqlalchemy import SQLAlchemy + + convention = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + } + + metadata = MetaData(naming_convention=convention) + db = SQLAlchemy(app, metadata=metadata) + +For more info about :class:`~sqlalchemy.schema.MetaData`, +`check out the official docs on it +`_. diff --git a/flask_sqlalchemy/__init__.py b/flask_sqlalchemy/__init__.py index 589e2f40..e82e9f27 100644 --- a/flask_sqlalchemy/__init__.py +++ b/flask_sqlalchemy/__init__.py @@ -678,11 +678,16 @@ class User(db.Model): .. versionadded:: 0.16 `scopefunc` is now accepted on `session_options`. It allows specifying a custom function which will define the SQLAlchemy session's scoping. + + .. versionadded:: 2.1 + The `metadata` parameter was added. This allows for setting custom + naming conventions among other, non-trivial things. """ def __init__(self, app=None, use_native_unicode=True, - session_options=None): + session_options=None, + metadata=None): self.use_native_unicode = use_native_unicode if session_options is None: @@ -693,7 +698,7 @@ def __init__(self, app=None, ) self.session = self.create_scoped_session(session_options) - self.Model = self.make_declarative_base() + self.Model = self.make_declarative_base(metadata) self._engine_lock = Lock() if app is not None: @@ -730,9 +735,10 @@ def create_session(self, options): """ return SignallingSession(self, **options) - def make_declarative_base(self): + def make_declarative_base(self, metadata=None): """Creates the declarative base.""" base = declarative_base(cls=Model, name='Model', + metadata=metadata, metaclass=_BoundDeclarativeMeta) base.query = _QueryProperty(self) return base diff --git a/test_sqlalchemy.py b/test_sqlalchemy.py index 86f2c5a3..7e36015d 100644 --- a/test_sqlalchemy.py +++ b/test_sqlalchemy.py @@ -5,6 +5,7 @@ from datetime import datetime import flask from flask.ext import sqlalchemy +from sqlalchemy import MetaData from sqlalchemy.ext.declarative import declared_attr from sqlalchemy.orm import sessionmaker @@ -81,6 +82,63 @@ def test_helper_api(self): self.assertEqual(self.db.metadata, self.db.Model.metadata) +class CustomMetaDataTestCase(unittest.TestCase): + + def setUp(self): + self.app = flask.Flask(__name__) + self.app.config['SQLALCHEMY_ENGINE'] = 'sqlite://' + self.app.config['TESTING'] = True + + def test_custom_metadata_positive(self): + + convention = { + "ix": 'ix_%(column_0_label)s', + "uq": "uq_%(table_name)s_%(column_0_name)s", + "ck": "ck_%(table_name)s_%(constraint_name)s", + "fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s", + "pk": "pk_%(table_name)s" + } + + metadata = MetaData(naming_convention=convention) + db = sqlalchemy.SQLAlchemy(self.app, metadata=metadata) + self.db = db + + class One(db.Model): + id = db.Column(db.Integer, primary_key=True) + myindex = db.Column(db.Integer, index=True) + + class Two(db.Model): + id = db.Column(db.Integer, primary_key=True) + one_id = db.Column(db.Integer, db.ForeignKey(One.id)) + myunique = db.Column(db.Integer, unique=True) + + self.assertEqual(list(One.__table__.constraints)[0].name, 'pk_one') + self.assertEqual(list(One.__table__.indexes)[0].name, 'ix_one_myindex') + + self.assertIn('fk_two_one_id_one', [c.name for c in Two.__table__.constraints]) + self.assertIn('uq_two_myunique', [c.name for c in Two.__table__.constraints]) + self.assertIn('pk_two', [c.name for c in Two.__table__.constraints]) + + def test_custom_metadata_negative(self): + db = sqlalchemy.SQLAlchemy(self.app, metadata=None) + self.db = db + + class One(db.Model): + id = db.Column(db.Integer, primary_key=True) + myindex = db.Column(db.Integer, index=True) + + class Two(db.Model): + id = db.Column(db.Integer, primary_key=True) + one_id = db.Column(db.Integer, db.ForeignKey(One.id)) + myunique = db.Column(db.Integer, unique=True) + + self.assertNotEqual(list(One.__table__.constraints)[0].name, 'pk_one') + + self.assertNotIn('fk_two_one_id_one', [c.name for c in Two.__table__.constraints]) + self.assertNotIn('uq_two_myunique', [c.name for c in Two.__table__.constraints]) + self.assertNotIn('pk_two', [c.name for c in Two.__table__.constraints]) + + class TestQueryProperty(unittest.TestCase): def setUp(self): @@ -591,6 +649,7 @@ class QazWsx(db.Model): def suite(): suite = unittest.TestSuite() suite.addTest(unittest.makeSuite(BasicAppTestCase)) + suite.addTest(unittest.makeSuite(CustomMetaDataTestCase)) suite.addTest(unittest.makeSuite(TestQueryProperty)) suite.addTest(unittest.makeSuite(TablenameTestCase)) suite.addTest(unittest.makeSuite(PaginationTestCase))