diff --git a/couchpotato/core/settings/model.py b/couchpotato/core/settings/model.py
index ef6e8a5c..09d46afa 100644
--- a/couchpotato/core/settings/model.py
+++ b/couchpotato/core/settings/model.py
@@ -1,23 +1,20 @@
+import uuid
+import datetime
+
+from sqlalchemy import Column, ForeignKey, Table, Index
+from sqlalchemy.ext.associationproxy import AssociationProxy
+from sqlalchemy.ext.hybrid import hybrid_property
+from sqlalchemy.orm import relationship, object_mapper, ColumnProperty, class_mapper
+from sqlalchemy.orm.exc import UnmappedInstanceError
+from sqlalchemy.orm.query import Query
+from sqlalchemy.ext.declarative import declarative_base
from couchpotato.core.helpers.encoding import toUnicode
-from elixir.entity import Entity
-from elixir.fields import Field
-from elixir.options import options_defaults, using_options
-from elixir.relationships import ManyToMany, OneToMany, ManyToOne
from sqlalchemy.ext.mutable import Mutable
from sqlalchemy.types import Integer, Unicode, UnicodeText, Boolean, String, \
TypeDecorator
import json
import time
-options_defaults["shortnames"] = True
-
-# We would like to be able to create this schema in a specific database at
-# will, so we can test it easily.
-# Make elixir not bind to any session to make this possible.
-#
-# http://elixir.ematia.de/trac/wiki/Recipes/MultipleDatabasesOneMetadata
-__session__ = None
-
class SetEncoder(json.JSONEncoder):
def default(self, obj):
@@ -73,74 +70,227 @@ class MutableDict(Mutable, dict):
MutableDict.associate_with(JsonType)
+Base = declarative_base()
+
+COLUMN_BLACKLIST = ('_sa_polymorphic_on', )
+
+def is_mapped_class(cls):
+ try:
+ class_mapper(cls)
+ return True
+ except:
+ return False
+
+def is_like_list(instance, relation):
+ """Returns ``True`` if and only if the relation of `instance` whose name is
+`relation` is list-like.
+
+A relation may be like a list if, for example, it is a non-lazy one-to-many
+relation, or it is a dynamically loaded one-to-many.
+
+"""
+ if relation in instance._sa_class_manager:
+ return instance._sa_class_manager[relation].property.uselist
+ related_value = getattr(type(instance), relation, None)
+ return isinstance(related_value, AssociationProxy)
+
+class TableHelper():
+ def to_dict(self, deep = None, exclude = None, include = None,
+ exclude_relations = None, include_relations = None,
+ include_methods = None):
+ instance = self
+
+ if (exclude is not None or exclude_relations is not None) and \
+ (include is not None or include_relations is not None):
+ raise ValueError('Cannot specify both include and exclude.')
+ # create a list of names of columns, including hybrid properties
+ try:
+ columns = [p.key for p in object_mapper(instance).iterate_properties
+ if isinstance(p, ColumnProperty)]
+ except UnmappedInstanceError:
+ return instance
+ for parent in type(instance).mro():
+ columns += [key for key, value in parent.__dict__.items()
+ if isinstance(value, hybrid_property)]
+ # filter the columns based on exclude and include values
+ if exclude is not None:
+ columns = (c for c in columns if c not in exclude)
+ elif include is not None:
+ columns = (c for c in columns if c in include)
+ # create a dictionary mapping column name to value
+ result = dict((col, getattr(instance, col)) for col in columns
+ if not (col.startswith('__') or col in COLUMN_BLACKLIST))
+ # add any included methods
+ if include_methods is not None:
+ result.update(dict((method, getattr(instance, method)()) for method in include_methods if not '.' in method))
+ # Check for objects in the dictionary that may not be serializable by
+ # default. Specifically, convert datetime and date objects to ISO 8601
+ # format, and convert UUID objects to hexadecimal strings.
+ for key, value in result.items():
+ # TODO We can get rid of this when issue #33 is resolved.
+ if isinstance(value, datetime.date):
+ result[key] = value.isoformat()
+ elif isinstance(value, uuid.UUID):
+ result[key] = str(value)
+ elif is_mapped_class(type(value)):
+ result[key] = value.to_dict()
+ # recursively call _to_dict on each of the `deep` relations
+ deep = deep or {}
+ for relation, rdeep in deep.items():
+ # Get the related value so we can see if it is None, a list, a query
+ # (as specified by a dynamic relationship loader), or an actual
+ # instance of a model.
+ relatedvalue = getattr(instance, relation)
+ if relatedvalue is None:
+ result[relation] = None
+ continue
+ # Determine the included and excluded fields for the related model.
+ newexclude = None
+ newinclude = None
+ if exclude_relations is not None and relation in exclude_relations:
+ newexclude = exclude_relations[relation]
+ elif (include_relations is not None and
+ relation in include_relations):
+ newinclude = include_relations[relation]
+ # Determine the included methods for the related model.
+ newmethods = None
+ if include_methods is not None:
+ newmethods = [method.split('.', 1)[1] for method in include_methods
+ if method.split('.', 1)[0] == relation]
+ if is_like_list(instance, relation):
+ result[relation] = [inst.to_dict(rdeep, exclude = newexclude,
+ include = newinclude,
+ include_methods = newmethods)
+ for inst in relatedvalue]
+ continue
+ # If the related value is dynamically loaded, resolve the query to get
+ # the single instance.
+ if isinstance(relatedvalue, Query):
+ relatedvalue = relatedvalue.one()
+ result[relation] = relatedvalue.to_dict(rdeep, exclude = newexclude,
+ include = newinclude,
+ include_methods = newmethods)
+
+ return result
+
+
+movie_files = Table('movie_files__file_movie', Base.metadata,
+ Column('movie_id', Integer, ForeignKey('movie.id'), nullable = False),
+ Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
+ Index('movie_files_idx', 'movie_id', 'file_id', unique = True)
+)
+
+release_files = Table('release_files__file_release', Base.metadata,
+ Column('release_id', Integer, ForeignKey('release.id'), nullable = False),
+ Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
+ Index('release_files_idx', 'release_id', 'file_id', unique = True)
+)
+
+library_files = Table('library_files__file_library', Base.metadata,
+ Column('library_id', Integer, ForeignKey('library.id'), nullable = False),
+ Column('file_id', Integer, ForeignKey('file.id'), nullable = False),
+ Index('library_files_idx', 'library_id', 'file_id', unique = True)
+)
+
+class Movie(Base, TableHelper):
+ __tablename__ = 'movie'
+ id = Column(Integer, primary_key = True)
-class Movie(Entity):
"""Movie Resource a movie could have multiple releases
The files belonging to the movie object are global for the whole movie
such as trailers, nfo, thumbnails"""
- last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
+ last_edit = Column(Integer, default = lambda: int(time.time()), index = True)
type = 'movie' # Compat tv branch
- library = ManyToOne('Library', cascade = 'delete, delete-orphan', single_parent = True)
- status = ManyToOne('Status')
- profile = ManyToOne('Profile')
- category = ManyToOne('Category')
- releases = OneToMany('Release', cascade = 'all, delete-orphan')
- files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
+ library_id = Column(Integer, ForeignKey('library.id'), index = True)
+ status_id = Column(Integer, ForeignKey('status.id'), index = True)
+ profile_id = Column(Integer, ForeignKey('profile.id'), index = True)
+ category_id = Column(Integer, ForeignKey('category.id'), index = True)
+
+ library = relationship('Library') #cascade = 'delete, delete-orphan', single_parent = True)
+ status = relationship('Status')
+ profile = relationship('Profile')
+ category = relationship('Category')
+ releases = relationship('Release') #, cascade = 'all, delete-orphan')
+ files = relationship('File', secondary = movie_files) #, cascade = 'all, delete-orphan', single_parent = True)
Media = Movie # Compat tv branch
-class Library(Entity):
+class Library(Base, TableHelper):
+ __tablename__ = 'library'
+ id = Column(Integer, primary_key = True)
+
""""""
- year = Field(Integer)
- identifier = Field(String(20), index = True)
+ year = Column(Integer)
+ identifier = Column(String(20), index = True)
- plot = Field(UnicodeText)
- tagline = Field(UnicodeText(255))
- info = Field(JsonType)
+ plot = Column(UnicodeText)
+ tagline = Column(UnicodeText(255))
+ info = Column(JsonType)
- status = ManyToOne('Status')
- movies = OneToMany('Movie', cascade = 'all, delete-orphan')
- titles = OneToMany('LibraryTitle', cascade = 'all, delete-orphan')
- files = ManyToMany('File', cascade = 'all, delete-orphan', single_parent = True)
+ status_id = Column(Integer, ForeignKey('status.id'), index = True)
+ status = relationship('Status')
+
+ movies = relationship('Movie') #, cascade = 'all, delete-orphan')
+ titles = relationship('LibraryTitle', order_by="desc(LibraryTitle.default)") #, cascade = 'all, delete-orphan')
+ files = relationship('File', secondary = library_files) #, cascade = 'all, delete-orphan', single_parent = True)
-class LibraryTitle(Entity):
- """"""
- using_options(order_by = '-default')
+class LibraryTitle(Base, TableHelper):
+ __tablename__ = 'librarytitle'
+ id = Column(Integer, primary_key = True)
- title = Field(Unicode)
- simple_title = Field(Unicode, index = True)
- default = Field(Boolean, default = False, index = True)
-
- language = OneToMany('Language')
- libraries = ManyToOne('Library')
-
-
-class Language(Entity):
""""""
- identifier = Field(String(20), index = True)
- label = Field(Unicode)
+ #using_options(order_by = '-default')
- titles = ManyToOne('LibraryTitle')
+ title = Column(Unicode)
+ simple_title = Column(Unicode, index = True)
+ default = Column(Boolean, default = False, index = True)
+
+ language = relationship('Language')
+
+ libraries_id = Column(Integer, ForeignKey('library.id'), index = True)
+ libraries = relationship('Library')
-class Release(Entity):
+class Language(Base, TableHelper):
+ __tablename__ = 'language'
+ id = Column(Integer, primary_key = True)
+
+ """"""
+
+ identifier = Column(String(20), index = True)
+ label = Column(Unicode)
+
+ titles_id = Column(Integer, ForeignKey('librarytitle.id'), index = True)
+ titles = relationship('LibraryTitle')
+
+
+class Release(Base, TableHelper):
+ __tablename__ = 'release'
+ id = Column(Integer, primary_key = True)
+
"""Logically groups all files that belong to a certain release, such as
parts of a movie, subtitles."""
- last_edit = Field(Integer, default = lambda: int(time.time()), index = True)
- identifier = Field(String(100), index = True)
+ last_edit = Column(Integer, default = lambda: int(time.time()), index = True)
+ identifier = Column(String(100), index = True)
- movie = ManyToOne('Movie')
- status = ManyToOne('Status')
- quality = ManyToOne('Quality')
- files = ManyToMany('File')
- info = OneToMany('ReleaseInfo', cascade = 'all, delete-orphan')
+ movie_id = Column(Integer, ForeignKey('movie.id'), index = True)
+ movie = relationship('Movie')
+
+ status_id = Column(Integer, ForeignKey('status.id'), index = True)
+ status = relationship('Status')
+
+ quality_id = Column(Integer, ForeignKey('quality.id'), index = True)
+ quality = relationship('Quality')
+
+ files = relationship('File', secondary = release_files)
+ info = relationship('ReleaseInfo') #, cascade = 'all, delete-orphan')
def to_dict(self, deep = None, exclude = None):
if not exclude: exclude = []
@@ -162,51 +312,66 @@ class Release(Entity):
return orig_dict
-class ReleaseInfo(Entity):
+class ReleaseInfo(Base, TableHelper):
+ __tablename__ = 'releaseinfo'
+ id = Column(Integer, primary_key = True)
+
"""Properties that can be bound to a file for off-line usage"""
- identifier = Field(String(50), index = True)
- value = Field(Unicode(255), nullable = False)
+ identifier = Column(String(50), index = True)
+ value = Column(Unicode(255), nullable = False)
- release = ManyToOne('Release')
+ release_id = Column(Integer, ForeignKey('release.id'), index = True)
+ release = relationship('Release')
-class Status(Entity):
+class Status(Base, TableHelper):
+ __tablename__ = 'status'
+ id = Column(Integer, primary_key = True)
+
"""The status of a release, such as Downloaded, Deleted, Wanted etc"""
- identifier = Field(String(20), unique = True)
- label = Field(Unicode(20))
+ identifier = Column(String(20), unique = True)
+ label = Column(Unicode(20))
- releases = OneToMany('Release')
- movies = OneToMany('Movie')
+ releases = relationship('Release')
+ movies = relationship('Movie')
-class Quality(Entity):
+class Quality(Base, TableHelper):
+ __tablename__ = 'quality'
+ id = Column(Integer, primary_key = True)
+
"""Quality name of a release, DVD, 720p, DVD-Rip etc"""
- using_options(order_by = 'order')
- identifier = Field(String(20), unique = True)
- label = Field(Unicode(20))
- order = Field(Integer, default = 0, index = True)
+ #using_options(order_by = 'order')
- size_min = Field(Integer)
- size_max = Field(Integer)
+ identifier = Column(String(20), unique = True)
+ label = Column(Unicode(20))
+ order = Column(Integer, default = 0, index = True)
- releases = OneToMany('Release')
- profile_types = OneToMany('ProfileType')
+ size_min = Column(Integer)
+ size_max = Column(Integer)
+
+ releases = relationship('Release')
+ profile_types = relationship('ProfileType', order_by="asc(ProfileType.order)")
-class Profile(Entity):
+class Profile(Base, TableHelper):
+ __tablename__ = 'profile'
+ id = Column(Integer, primary_key = True)
+
""""""
- using_options(order_by = 'order')
- label = Field(Unicode(50))
- order = Field(Integer, default = 0, index = True)
- core = Field(Boolean, default = False)
- hide = Field(Boolean, default = False)
+ #using_options(order_by = 'order')
- movie = OneToMany('Movie')
- types = OneToMany('ProfileType', cascade = 'all, delete-orphan')
+ label = Column(Unicode(50))
+ order = Column(Integer, default = 0, index = True)
+ core = Column(Boolean, default = False)
+ hide = Column(Boolean, default = False)
+
+ movie = relationship('Movie')
+ types = relationship('ProfileType', order_by="asc(ProfileType.order)") #, cascade = 'all, delete-orphan')
def to_dict(self, deep = None, exclude = None):
if not exclude: exclude = []
@@ -219,100 +384,119 @@ class Profile(Entity):
return orig_dict
-class Category(Entity):
+class Category(Base, TableHelper):
+ __tablename__ = 'category'
+ id = Column(Integer, primary_key = True)
+
""""""
- using_options(order_by = 'order')
- label = Field(Unicode(50))
- order = Field(Integer, default = 0, index = True)
- required = Field(Unicode(255))
- preferred = Field(Unicode(255))
- ignored = Field(Unicode(255))
- destination = Field(Unicode(255))
+ #using_options(order_by = 'order')
- movie = OneToMany('Movie')
+ label = Column(Unicode(50))
+ order = Column(Integer, default = 0, index = True)
+ required = Column(Unicode(255))
+ preferred = Column(Unicode(255))
+ ignored = Column(Unicode(255))
+ destination = Column(Unicode(255))
+
+ movie = relationship('Movie')
-class ProfileType(Entity):
+class ProfileType(Base, TableHelper):
+ __tablename__ = 'profiletype'
+ id = Column(Integer, primary_key = True)
+
""""""
- using_options(order_by = 'order')
- order = Field(Integer, default = 0, index = True)
- finish = Field(Boolean, default = True)
- wait_for = Field(Integer, default = 0)
+ #using_options(order_by = 'order')
- quality = ManyToOne('Quality')
- profile = ManyToOne('Profile')
+ order = Column(Integer, default = 0, index = True)
+ finish = Column(Boolean, default = True)
+ wait_for = Column(Integer, default = 0)
+
+ quality_id = Column(Integer, ForeignKey('quality.id'), index = True)
+ quality = relationship('Quality')
+
+ profile_id = Column(Integer, ForeignKey('profile.id'), index = True)
+ profile = relationship('Profile')
-class File(Entity):
+class File(Base, TableHelper):
+ __tablename__ = 'file'
+ id = Column(Integer, primary_key = True)
+
"""File that belongs to a release."""
- path = Field(Unicode(255), nullable = False, unique = True)
- part = Field(Integer, default = 1)
- available = Field(Boolean, default = True)
+ path = Column(Unicode(255), nullable = False, unique = True)
+ part = Column(Integer, default = 1)
+ available = Column(Boolean, default = True)
- type = ManyToOne('FileType')
- properties = OneToMany('FileProperty')
+ type_id = Column(Integer, ForeignKey('filetype.id'), index = True)
+ type = relationship('FileType')
- history = OneToMany('RenameHistory')
- movie = ManyToMany('Movie')
- release = ManyToMany('Release')
- library = ManyToMany('Library')
+ properties = relationship('FileProperty')
+
+ movie = relationship('Movie', secondary = movie_files)
+ release = relationship('Release', secondary = release_files)
+ library = relationship('Library', secondary = library_files)
-class FileType(Entity):
+class FileType(Base, TableHelper):
+ __tablename__ = 'filetype'
+ id = Column(Integer, primary_key = True)
+
"""Types could be trailer, subtitle, movie, partial movie etc."""
- identifier = Field(String(20), unique = True)
- type = Field(Unicode(20))
- name = Field(Unicode(50), nullable = False)
+ identifier = Column(String(20), unique = True)
+ type = Column(Unicode(20))
+ name = Column(Unicode(50), nullable = False)
- files = OneToMany('File')
+ files = relationship('File')
-class FileProperty(Entity):
+class FileProperty(Base, TableHelper):
+ __tablename__ = 'fileproperty'
+ id = Column(Integer, primary_key = True)
+
"""Properties that can be bound to a file for off-line usage"""
- identifier = Field(String(20), index = True)
- value = Field(Unicode(255), nullable = False)
+ identifier = Column(String(20), index = True)
+ value = Column(Unicode(255), nullable = False)
- file = ManyToOne('File')
+ file_id = Column(Integer, ForeignKey('file.id'), index = True)
+ file = relationship('File')
-class RenameHistory(Entity):
- """Remembers from where to where files have been moved."""
+class Notification(Base, TableHelper):
+ __tablename__ = 'notification'
+ id = Column(Integer, primary_key = True)
- old = Field(Unicode(255))
- new = Field(Unicode(255))
+ """"""
- file = ManyToOne('File')
+ #using_options(order_by = 'added')
+
+ added = Column(Integer, default = lambda: int(time.time()), index = True)
+ read = Column(Boolean, default = False, index = True)
+ message = Column(Unicode(255))
+ data = Column(JsonType)
-class Notification(Entity):
- using_options(order_by = 'added')
+class Properties(Base, TableHelper):
+ __tablename__ = 'properties'
+ id = Column(Integer, primary_key = True)
- added = Field(Integer, default = lambda: int(time.time()))
- read = Field(Boolean, default = False)
- message = Field(Unicode(255))
- data = Field(JsonType)
+ """"""
-
-class Properties(Entity):
-
- identifier = Field(String(50), index = True)
- value = Field(Unicode(255), nullable = False)
+ identifier = Column(String(50), index = True)
+ value = Column(Unicode(255), nullable = False)
def setup():
"""Setup the database and create the tables that don't exists yet"""
- from elixir import setup_all, create_all
from couchpotato.environment import Env
engine = Env.getEngine()
-
- setup_all()
- create_all(engine)
+ Base.metadata.create_all(engine)
try:
engine.execute("PRAGMA journal_mode = WAL")
diff --git a/couchpotato/runner.py b/couchpotato/runner.py
index 5c175201..bd033983 100644
--- a/couchpotato/runner.py
+++ b/couchpotato/runner.py
@@ -188,7 +188,7 @@ def runCouchPotato(options, base_path, args, data_dir = None, log_dir = None, En
db_exists = os.path.isfile(toUnicode(db_path))
# Load migrations
- if db_exists:
+ if False and db_exists:
from migrate.versioning.api import version_control, db_version, version, upgrade
repo = os.path.join(base_path, 'couchpotato', 'core', 'migration')
diff --git a/libs/elixir/__init__.py b/libs/elixir/__init__.py
deleted file mode 100644
index a242b538..00000000
--- a/libs/elixir/__init__.py
+++ /dev/null
@@ -1,114 +0,0 @@
-'''
-Elixir package
-
-A declarative layer on top of the `SQLAlchemy library
-`_. It is a fairly thin wrapper, which provides
-the ability to create simple Python classes that map directly to relational
-database tables (this pattern is often referred to as the Active Record design
-pattern), providing many of the benefits of traditional databases
-without losing the convenience of Python objects.
-
-Elixir is intended to replace the ActiveMapper SQLAlchemy extension, and the
-TurboEntity project but does not intend to replace SQLAlchemy's core features,
-and instead focuses on providing a simpler syntax for defining model objects
-when you do not need the full expressiveness of SQLAlchemy's manual mapper
-definitions.
-'''
-
-try:
- set
-except NameError:
- from sets import Set as set
-
-import sqlalchemy
-from sqlalchemy.types import *
-
-from elixir.options import using_options, using_table_options, \
- using_mapper_options, options_defaults, \
- using_options_defaults
-from elixir.entity import Entity, EntityBase, EntityMeta, EntityDescriptor, \
- setup_entities, cleanup_entities
-from elixir.fields import has_field, Field
-from elixir.relationships import belongs_to, has_one, has_many, \
- has_and_belongs_to_many, \
- ManyToOne, OneToOne, OneToMany, ManyToMany
-from elixir.properties import has_property, GenericProperty, ColumnProperty, \
- Synonym
-from elixir.statements import Statement
-from elixir.collection import EntityCollection, GlobalEntityCollection
-
-
-__version__ = '0.8.0dev'
-
-__all__ = ['Entity', 'EntityBase', 'EntityMeta', 'EntityCollection',
- 'entities',
- 'Field', 'has_field',
- 'has_property', 'GenericProperty', 'ColumnProperty', 'Synonym',
- 'belongs_to', 'has_one', 'has_many', 'has_and_belongs_to_many',
- 'ManyToOne', 'OneToOne', 'OneToMany', 'ManyToMany',
- 'using_options', 'using_table_options', 'using_mapper_options',
- 'options_defaults', 'using_options_defaults',
- 'metadata', 'session',
- 'create_all', 'drop_all',
- 'setup_all', 'cleanup_all',
- 'setup_entities', 'cleanup_entities'] + \
- sqlalchemy.types.__all__
-
-__doc_all__ = ['create_all', 'drop_all',
- 'setup_all', 'cleanup_all',
- 'metadata', 'session']
-
-# default session
-session = sqlalchemy.orm.scoped_session(sqlalchemy.orm.sessionmaker())
-
-# default metadata
-metadata = sqlalchemy.MetaData()
-
-metadatas = set()
-
-# default entity collection
-entities = GlobalEntityCollection()
-
-
-def create_all(*args, **kwargs):
- '''Create the necessary tables for all declared entities'''
- for md in metadatas:
- md.create_all(*args, **kwargs)
-
-
-def drop_all(*args, **kwargs):
- '''Drop tables for all declared entities'''
- for md in metadatas:
- md.drop_all(*args, **kwargs)
-
-
-def setup_all(create_tables=False, *args, **kwargs):
- '''Setup the table and mapper of all entities in the default entity
- collection.
- '''
- setup_entities(entities)
-
- # issue the "CREATE" SQL statements
- if create_tables:
- create_all(*args, **kwargs)
-
-
-def cleanup_all(drop_tables=False, *args, **kwargs):
- '''Clear all mappers, clear the session, and clear all metadatas.
- Optionally drops the tables.
- '''
- session.close()
-
- cleanup_entities(entities)
-
- sqlalchemy.orm.clear_mappers()
- entities.clear()
-
- if drop_tables:
- drop_all(*args, **kwargs)
-
- for md in metadatas:
- md.clear()
- metadatas.clear()
-
-
diff --git a/libs/elixir/collection.py b/libs/elixir/collection.py
deleted file mode 100644
index 78127e3e..00000000
--- a/libs/elixir/collection.py
+++ /dev/null
@@ -1,125 +0,0 @@
-'''
-Default entity collection implementation
-'''
-import sys
-import re
-
-class BaseCollection(list):
- def __init__(self, entities=None):
- list.__init__(self)
- if entities is not None:
- self.extend(entities)
-
- def extend(self, entities):
- for e in entities:
- self.append(e)
-
- def clear(self):
- del self[:]
-
- def resolve_absolute(self, key, full_path, entity=None, root=None):
- if root is None:
- root = entity._descriptor.resolve_root
- if root:
- full_path = '%s.%s' % (root, full_path)
- module_path, classname = full_path.rsplit('.', 1)
- module = sys.modules[module_path]
- res = getattr(module, classname, None)
- if res is None:
- if entity is not None:
- raise Exception("Couldn't resolve target '%s' <%s> in '%s'!"
- % (key, full_path, entity.__name__))
- else:
- raise Exception("Couldn't resolve target '%s' <%s>!"
- % (key, full_path))
- return res
-
- def __getattr__(self, key):
- return self.resolve(key)
-
-# default entity collection
-class GlobalEntityCollection(BaseCollection):
- def __init__(self, entities=None):
- # _entities is a dict of entities keyed on their name.
- self._entities = {}
- super(GlobalEntityCollection, self).__init__(entities)
-
- def append(self, entity):
- '''
- Add an entity to the collection.
- '''
- super(EntityCollection, self).append(entity)
-
- existing_entities = self._entities.setdefault(entity.__name__, [])
- existing_entities.append(entity)
-
- def resolve(self, key, entity=None):
- '''
- Resolve a key to an Entity. The optional `entity` argument is the
- "source" entity when resolving relationship targets.
- '''
- # Do we have a fully qualified entity name?
- if '.' in key:
- return self.resolve_absolute(key, key, entity)
- else:
- # Otherwise we look in the entities of this collection
- res = self._entities.get(key, None)
- if res is None:
- if entity:
- raise Exception("Couldn't resolve target '%s' in '%s'"
- % (key, entity.__name__))
- else:
- raise Exception("This collection does not contain any "
- "entity corresponding to the key '%s'!"
- % key)
- elif len(res) > 1:
- raise Exception("'%s' resolves to several entities, you should"
- " use the full path (including the full module"
- " name) to that entity." % key)
- else:
- return res[0]
-
- def clear(self):
- self._entities = {}
- super(GlobalEntityCollection, self).clear()
-
-# backward compatible name
-EntityCollection = GlobalEntityCollection
-
-_leading_dots = re.compile('^([.]*).*$')
-
-class RelativeEntityCollection(BaseCollection):
- # the entity=None does not make any sense with a relative entity collection
- def resolve(self, key, entity):
- '''
- Resolve a key to an Entity. The optional `entity` argument is the
- "source" entity when resolving relationship targets.
- '''
- full_path = key
-
- if '.' not in key or key.startswith('.'):
- # relative target
-
- # any leading dot is stripped and with each dot removed,
- # the entity_module is stripped of one more chunk (starting with
- # the last one).
- num_dots = _leading_dots.match(full_path).end(1)
- full_path = full_path[num_dots:]
- chunks = entity.__module__.split('.')
- chunkstokeep = len(chunks) - num_dots
- if chunkstokeep < 0:
- raise Exception("Couldn't resolve relative target "
- "'%s' relative to '%s'" % (key, entity.__module__))
- entity_module = '.'.join(chunks[:chunkstokeep])
-
- if entity_module and entity_module is not '__main__':
- full_path = '%s.%s' % (entity_module, full_path)
-
- root = ''
- else:
- root = None
- return self.resolve_absolute(key, full_path, entity, root=root)
-
- def __getattr__(self, key):
- raise NotImplementedError
-
diff --git a/libs/elixir/entity.py b/libs/elixir/entity.py
deleted file mode 100644
index 87f5154c..00000000
--- a/libs/elixir/entity.py
+++ /dev/null
@@ -1,1039 +0,0 @@
-'''
-This module provides the ``Entity`` base class, as well as its metaclass
-``EntityMeta``.
-'''
-
-import sys
-import types
-import warnings
-
-from copy import deepcopy
-
-import sqlalchemy
-from sqlalchemy import Table, Column, Integer, desc, ForeignKey, and_, \
- ForeignKeyConstraint
-from sqlalchemy.orm import MapperExtension, mapper, object_session, \
- EXT_CONTINUE, polymorphic_union, ScopedSession, \
- ColumnProperty
-from sqlalchemy.sql import ColumnCollection
-
-import elixir
-from elixir.statements import process_mutators, MUTATORS
-from elixir import options
-from elixir.properties import Property
-
-DEBUG = False
-
-__doc_all__ = ['Entity', 'EntityMeta']
-
-
-def session_mapper_factory(scoped_session):
- def session_mapper(cls, *args, **kwargs):
- if kwargs.pop('save_on_init', True):
- old_init = cls.__init__
- def __init__(self, *args, **kwargs):
- old_init(self, *args, **kwargs)
- scoped_session.add(self)
- cls.__init__ = __init__
- cls.query = scoped_session.query_property()
- return mapper(cls, *args, **kwargs)
- return session_mapper
-
-
-class EntityDescriptor(object):
- '''
- EntityDescriptor describes fields and options needed for table creation.
- '''
-
- def __init__(self, entity):
- self.entity = entity
- self.parent = None
-
- bases = []
- for base in entity.__bases__:
- if isinstance(base, EntityMeta):
- if is_entity(base) and not is_abstract_entity(base):
- if self.parent:
- raise Exception(
- '%s entity inherits from several entities, '
- 'and this is not supported.'
- % self.entity.__name__)
- else:
- self.parent = base
- bases.extend(base._descriptor.bases)
- self.parent._descriptor.children.append(entity)
- else:
- bases.append(base)
- self.bases = bases
- if not is_entity(entity) or is_abstract_entity(entity):
- return
-
- # entity.__module__ is not always reliable (eg in mod_python)
- self.module = sys.modules.get(entity.__module__)
-
- self.builders = []
-
- #XXX: use entity.__subclasses__ ?
- self.children = []
-
- # used for multi-table inheritance
- self.join_condition = None
- self.has_pk = False
- self._pk_col_done = False
-
- # columns and constraints waiting for a table to exist
- self._columns = ColumnCollection()
- self.constraints = []
-
- # properties (it is only useful for checking dupe properties at the
- # moment, and when adding properties before the mapper is created,
- # which shouldn't happen).
- self.properties = {}
-
- #
- self.relationships = []
-
- # set default value for options
- self.table_args = []
-
- # base class(es) options_defaults
- options_defaults = self.options_defaults()
-
- complete_defaults = options.options_defaults.copy()
- complete_defaults.update({
- 'metadata': elixir.metadata,
- 'session': elixir.session,
- 'collection': elixir.entities
- })
-
- # set default value for other options
- for key in options.valid_options:
- value = options_defaults.get(key, complete_defaults[key])
- if isinstance(value, dict):
- value = value.copy()
- setattr(self, key, value)
-
- # override options with module-level defaults defined
- for key in ('metadata', 'session', 'collection'):
- attr = '__%s__' % key
- if hasattr(self.module, attr):
- setattr(self, key, getattr(self.module, attr))
-
- def options_defaults(self):
- base_defaults = {}
- for base in self.bases:
- base_defaults.update(base._descriptor.options_defaults())
- base_defaults.update(getattr(self.entity, 'options_defaults', {}))
- return base_defaults
-
- def setup_options(self):
- '''
- Setup any values that might depend on the "using_options" class
- mutator. For example, the tablename or the metadata.
- '''
- elixir.metadatas.add(self.metadata)
- if self.collection is not None:
- self.collection.append(self.entity)
-
- entity = self.entity
- if self.parent:
- if self.inheritance == 'single':
- self.tablename = self.parent._descriptor.tablename
-
- if not self.tablename:
- if self.shortnames:
- self.tablename = entity.__name__.lower()
- else:
- modulename = entity.__module__.replace('.', '_')
- tablename = "%s_%s" % (modulename, entity.__name__)
- self.tablename = tablename.lower()
- elif hasattr(self.tablename, '__call__'):
- self.tablename = self.tablename(entity)
-
- if not self.identity:
- if 'polymorphic_identity' in self.mapper_options:
- self.identity = self.mapper_options['polymorphic_identity']
- else:
- #TODO: include module name (We could have b.Account inherit
- # from a.Account)
- self.identity = entity.__name__.lower()
- elif 'polymorphic_identity' in self.mapper_options:
- raise Exception('You cannot use the "identity" option and the '
- 'polymorphic_identity mapper option at the same '
- 'time.')
- elif hasattr(self.identity, '__call__'):
- self.identity = self.identity(entity)
-
- if self.polymorphic:
- if not isinstance(self.polymorphic, basestring):
- self.polymorphic = options.DEFAULT_POLYMORPHIC_COL_NAME
-
- #---------------------
- # setup phase methods
-
- def setup_autoload_table(self):
- self.setup_table(True)
-
- def create_pk_cols(self):
- """
- Create primary_key columns. That is, call the 'create_pk_cols'
- builders then add a primary key to the table if it hasn't already got
- one and needs one.
-
- This method is "semi-recursive" in some cases: it calls the
- create_keys method on ManyToOne relationships and those in turn call
- create_pk_cols on their target. It shouldn't be possible to have an
- infinite loop since a loop of primary_keys is not a valid situation.
- """
- if self._pk_col_done:
- return
-
- self.call_builders('create_pk_cols')
-
- if not self.autoload:
- if self.parent:
- if self.inheritance == 'multi':
- # Add columns with foreign keys to the parent's primary
- # key columns
- parent_desc = self.parent._descriptor
- tablename = parent_desc.table_fullname
- join_clauses = []
- fk_columns = []
- for pk_col in parent_desc.primary_keys:
- colname = options.MULTIINHERITANCECOL_NAMEFORMAT % \
- {'entity': self.parent.__name__.lower(),
- 'key': pk_col.key}
-
- # It seems like SA ForeignKey is not happy being given
- # a real column object when said column is not yet
- # attached to a table
- pk_col_name = "%s.%s" % (tablename, pk_col.key)
- col = Column(colname, pk_col.type, primary_key=True)
- fk_columns.append(col)
- self.add_column(col)
- join_clauses.append(col == pk_col)
- self.join_condition = and_(*join_clauses)
- self.add_constraint(
- ForeignKeyConstraint(fk_columns,
- parent_desc.primary_keys, ondelete='CASCADE'))
- elif self.inheritance == 'concrete':
- # Copy primary key columns from the parent.
- for col in self.parent._descriptor.columns:
- if col.primary_key:
- self.add_column(col.copy())
- elif not self.has_pk and self.auto_primarykey:
- if isinstance(self.auto_primarykey, basestring):
- colname = self.auto_primarykey
- else:
- colname = options.DEFAULT_AUTO_PRIMARYKEY_NAME
-
- self.add_column(
- Column(colname, options.DEFAULT_AUTO_PRIMARYKEY_TYPE,
- primary_key=True))
- self._pk_col_done = True
-
- def setup_relkeys(self):
- self.call_builders('create_non_pk_cols')
-
- def before_table(self):
- self.call_builders('before_table')
-
- def setup_table(self, only_autoloaded=False):
- '''
- Create a SQLAlchemy table-object with all columns that have been
- defined up to this point.
- '''
- if self.entity.table is not None:
- return
-
- if self.autoload != only_autoloaded:
- return
-
- kwargs = self.table_options
- if self.autoload:
- args = self.table_args
- kwargs['autoload'] = True
- else:
- if self.parent:
- if self.inheritance == 'single':
- # we know the parent is setup before the child
- self.entity.table = self.parent.table
-
- # re-add the entity columns to the parent entity so that
- # they are added to the parent's table (whether the
- # parent's table is already setup or not).
- for col in self._columns:
- self.parent._descriptor.add_column(col)
- for constraint in self.constraints:
- self.parent._descriptor.add_constraint(constraint)
- return
- elif self.inheritance == 'concrete':
- #TODO: we should also copy columns from the parent table
- # if the parent is a base (abstract?) entity (whatever the
- # inheritance type -> elif will need to be changed)
-
- # Copy all non-primary key columns from parent table
- # (primary key columns have already been copied earlier).
- for col in self.parent._descriptor.columns:
- if not col.primary_key:
- self.add_column(col.copy())
-
- for con in self.parent._descriptor.constraints:
- self.add_constraint(
- ForeignKeyConstraint(
- [e.parent.key for e in con.elements],
- [e.target_fullname for e in con.elements],
- name=con.name, #TODO: modify it
- onupdate=con.onupdate, ondelete=con.ondelete,
- use_alter=con.use_alter))
-
- if self.polymorphic and \
- self.inheritance in ('single', 'multi') and \
- self.children and not self.parent:
- self.add_column(Column(self.polymorphic,
- options.POLYMORPHIC_COL_TYPE))
-
- if self.version_id_col:
- if not isinstance(self.version_id_col, basestring):
- self.version_id_col = options.DEFAULT_VERSION_ID_COL_NAME
- self.add_column(Column(self.version_id_col, Integer))
-
- args = list(self.columns) + self.constraints + self.table_args
- self.entity.table = Table(self.tablename, self.metadata,
- *args, **kwargs)
- if DEBUG:
- print self.entity.table.repr2()
-
- def setup_reltables(self):
- self.call_builders('create_tables')
-
- def after_table(self):
- self.call_builders('after_table')
-
- def setup_events(self):
- def make_proxy_method(methods):
- def proxy_method(self, mapper, connection, instance):
- for func in methods:
- ret = func(instance)
- # I couldn't commit myself to force people to
- # systematicaly return EXT_CONTINUE in all their event
- # methods.
- # But not doing that diverge to how SQLAlchemy works.
- # I should try to convince Mike to do EXT_CONTINUE by
- # default, and stop processing as the special case.
-# if ret != EXT_CONTINUE:
- if ret is not None and ret != EXT_CONTINUE:
- return ret
- return EXT_CONTINUE
- return proxy_method
-
- # create a list of callbacks for each event
- methods = {}
-
- all_methods = getmembers(self.entity,
- lambda a: isinstance(a, types.MethodType))
-
- for name, method in all_methods:
- for event in getattr(method, '_elixir_events', []):
- event_methods = methods.setdefault(event, [])
- event_methods.append(method)
-
- if not methods:
- return
-
- # transform that list into methods themselves
- for event in methods:
- methods[event] = make_proxy_method(methods[event])
-
- # create a custom mapper extension class, tailored to our entity
- ext = type('EventMapperExtension', (MapperExtension,), methods)()
-
- # then, make sure that the entity's mapper has our mapper extension
- self.add_mapper_extension(ext)
-
- def before_mapper(self):
- self.call_builders('before_mapper')
-
- def _get_children(self):
- children = self.children[:]
- for child in self.children:
- children.extend(child._descriptor._get_children())
- return children
-
- def translate_order_by(self, order_by):
- if isinstance(order_by, basestring):
- order_by = [order_by]
-
- order = []
- for colname in order_by:
- #FIXME: get_column uses self.columns[key] instead of property
- # names. self.columns correspond to the columns of the table if
- # the table was already created and to self._columns otherwise,
- # which is a ColumnCollection indexed on columns.key
- # See ticket #108.
- col = self.get_column(colname.strip('-'))
- if colname.startswith('-'):
- col = desc(col)
- order.append(col)
- return order
-
- def setup_mapper(self):
- '''
- Initializes and assign a mapper to the entity.
- At this point the mapper will usually have no property as they are
- added later.
- '''
- if self.entity.mapper:
- return
-
- # for now we don't support the "abstract" parent class in a concrete
- # inheritance scenario as demonstrated in
- # sqlalchemy/test/orm/inheritance/concrete.py
- # this should be added along other
- kwargs = {}
- if self.order_by:
- kwargs['order_by'] = self.translate_order_by(self.order_by)
-
- if self.version_id_col:
- kwargs['version_id_col'] = self.get_column(self.version_id_col)
-
- if self.inheritance in ('single', 'concrete', 'multi'):
- if self.parent and \
- (self.inheritance != 'concrete' or self.polymorphic):
- # non-polymorphic concrete doesn't need this
- kwargs['inherits'] = self.parent.mapper
-
- if self.inheritance == 'multi' and self.parent:
- kwargs['inherit_condition'] = self.join_condition
-
- if self.polymorphic:
- if self.children:
- if self.inheritance == 'concrete':
- keys = [(self.identity, self.entity.table)]
- keys.extend([(child._descriptor.identity, child.table)
- for child in self._get_children()])
- # Having the same alias name for an entity and one of
- # its child (which is a parent itself) shouldn't cause
- # any problem because the join shouldn't be used at
- # the same time. But in reality, some versions of SA
- # do misbehave on this. Since it doesn't hurt to have
- # different names anyway, here they go.
- pjoin = polymorphic_union(
- dict(keys), self.polymorphic,
- 'pjoin_%s' % self.identity)
-
- kwargs['with_polymorphic'] = ('*', pjoin)
- kwargs['polymorphic_on'] = \
- getattr(pjoin.c, self.polymorphic)
- elif not self.parent:
- kwargs['polymorphic_on'] = \
- self.get_column(self.polymorphic)
-
- if self.children or self.parent:
- kwargs['polymorphic_identity'] = self.identity
-
- if self.parent and self.inheritance == 'concrete':
- kwargs['concrete'] = True
-
- if self.parent and self.inheritance == 'single':
- args = []
- else:
- args = [self.entity.table]
-
- # let user-defined kwargs override Elixir-generated ones, though that's
- # not very usefull since most of them expect Column instances.
- kwargs.update(self.mapper_options)
-
- #TODO: document this!
- if 'primary_key' in kwargs:
- cols = self.entity.table.c
- kwargs['primary_key'] = [getattr(cols, colname) for
- colname in kwargs['primary_key']]
-
- # do the mapping
- if self.session is None:
- self.entity.mapper = mapper(self.entity, *args, **kwargs)
- elif isinstance(self.session, ScopedSession):
- session_mapper = session_mapper_factory(self.session)
- self.entity.mapper = session_mapper(self.entity, *args, **kwargs)
- else:
- raise Exception("Failed to map entity '%s' with its table or "
- "selectable. You can only bind an Entity to a "
- "ScopedSession object or None for manual session "
- "management."
- % self.entity.__name__)
-
- def after_mapper(self):
- self.call_builders('after_mapper')
-
- def setup_properties(self):
- self.call_builders('create_properties')
-
- def finalize(self):
- self.call_builders('finalize')
- self.entity._setup_done = True
-
- #----------------
- # helper methods
-
- def call_builders(self, what):
- for builder in self.builders:
- if hasattr(builder, what):
- getattr(builder, what)()
-
- def add_column(self, col, check_duplicate=None):
- '''when check_duplicate is None, the value of the allowcoloverride
- option of the entity is used.
- '''
- if check_duplicate is None:
- check_duplicate = not self.allowcoloverride
-
- if col.key in self._columns:
- if check_duplicate:
- raise Exception("Column '%s' already exist in '%s' ! " %
- (col.key, self.entity.__name__))
- else:
- del self._columns[col.key]
- # are indexed on col.key
- self._columns.add(col)
-
- if col.primary_key:
- self.has_pk = True
-
- table = self.entity.table
- if table is not None:
- if check_duplicate and col.key in table.columns.keys():
- raise Exception("Column '%s' already exist in table '%s' ! " %
- (col.key, table.name))
- table.append_column(col)
- if DEBUG:
- print "table.append_column(%s)" % col
-
- def add_constraint(self, constraint):
- self.constraints.append(constraint)
-
- table = self.entity.table
- if table is not None:
- table.append_constraint(constraint)
-
- def add_property(self, name, property, check_duplicate=True):
- if check_duplicate and name in self.properties:
- raise Exception("property '%s' already exist in '%s' ! " %
- (name, self.entity.__name__))
- self.properties[name] = property
-
-#FIXME: something like this is needed to propagate the relationships from
-# parent entities to their children in a concrete inheritance scenario. But
-# this doesn't work because of the backref matching code. In most case
-# (test_concrete.py) it doesn't even happen at all.
-# if self.children and self.inheritance == 'concrete':
-# for child in self.children:
-# child._descriptor.add_property(name, property)
-
- mapper = self.entity.mapper
- if mapper:
- mapper.add_property(name, property)
- if DEBUG:
- print "mapper.add_property('%s', %s)" % (name, repr(property))
-
- def add_mapper_extension(self, extension):
- extensions = self.mapper_options.get('extension', [])
- if not isinstance(extensions, list):
- extensions = [extensions]
- extensions.append(extension)
- self.mapper_options['extension'] = extensions
-
- def get_column(self, key, check_missing=True):
- #TODO: this needs to work whether the table is already setup or not
- #TODO: support SA table/autoloaded entity
- try:
- return self.columns[key]
- except KeyError:
- if check_missing:
- raise Exception("No column named '%s' found in the table of "
- "the '%s' entity!"
- % (key, self.entity.__name__))
-
- def get_inverse_relation(self, rel, check_reverse=True):
- '''
- Return the inverse relation of rel, if any, None otherwise.
- '''
-
- matching_rel = None
- for other_rel in self.relationships:
- if rel.is_inverse(other_rel):
- if matching_rel is None:
- matching_rel = other_rel
- else:
- raise Exception(
- "Several relations match as inverse of the '%s' "
- "relation in entity '%s'. You should specify "
- "inverse relations manually by using the inverse "
- "keyword."
- % (rel.name, rel.entity.__name__))
- # When a matching inverse is found, we check that it has only
- # one relation matching as its own inverse. We don't need the result
- # of the method though. But we do need to be careful not to start an
- # infinite recursive loop.
- if matching_rel and check_reverse:
- rel.entity._descriptor.get_inverse_relation(matching_rel, False)
-
- return matching_rel
-
- def find_relationship(self, name):
- for rel in self.relationships:
- if rel.name == name:
- return rel
- if self.parent:
- return self.parent._descriptor.find_relationship(name)
- else:
- return None
-
- #------------------------
- # some useful properties
-
- @property
- def table_fullname(self):
- '''
- Complete name of the table for the related entity.
- Includes the schema name if there is one specified.
- '''
- schema = self.table_options.get('schema', None)
- if schema is not None:
- return "%s.%s" % (schema, self.tablename)
- else:
- return self.tablename
-
- @property
- def columns(self):
- if self.entity.table is not None:
- return self.entity.table.columns
- else:
- #FIXME: depending on the type of inheritance, we should also
- # return the parent entity's columns (for example for order_by
- # using a column defined in the parent.
- return self._columns
-
- @property
- def primary_keys(self):
- """
- Returns the list of primary key columns of the entity.
-
- This property isn't valid before the "create_pk_cols" phase.
- """
- if self.autoload:
- return [col for col in self.entity.table.primary_key.columns]
- else:
- if self.parent and self.inheritance == 'single':
- return self.parent._descriptor.primary_keys
- else:
- return [col for col in self.columns if col.primary_key]
-
- @property
- def table(self):
- if self.entity.table is not None:
- return self.entity.table
- else:
- return FakeTable(self)
-
- @property
- def primary_key_properties(self):
- """
- Returns the list of (mapper) properties corresponding to the primary
- key columns of the table of the entity.
-
- This property caches its value, so it shouldn't be called before the
- entity is fully set up.
- """
- if not hasattr(self, '_pk_props'):
- col_to_prop = {}
- mapper = self.entity.mapper
- for prop in mapper.iterate_properties:
- if isinstance(prop, ColumnProperty):
- for col in prop.columns:
- #XXX: Why is this extra loop necessary? What is this
- # "proxy_set" supposed to mean?
- for col in col.proxy_set:
- col_to_prop[col] = prop
- pk_cols = [c for c in mapper.mapped_table.c if c.primary_key]
- self._pk_props = [col_to_prop[c] for c in pk_cols]
- return self._pk_props
-
-class FakePK(object):
- def __init__(self, descriptor):
- self.descriptor = descriptor
-
- @property
- def columns(self):
- return self.descriptor.primary_keys
-
-class FakeTable(object):
- def __init__(self, descriptor):
- self.descriptor = descriptor
- self.primary_key = FakePK(descriptor)
-
- @property
- def columns(self):
- return self.descriptor.columns
-
- @property
- def fullname(self):
- '''
- Complete name of the table for the related entity.
- Includes the schema name if there is one specified.
- '''
- schema = self.descriptor.table_options.get('schema', None)
- if schema is not None:
- return "%s.%s" % (schema, self.descriptor.tablename)
- else:
- return self.descriptor.tablename
-
-
-def is_entity(cls):
- """
- Scan the bases classes of `cls` to see if any is an instance of
- EntityMeta. If we don't find any, it means it is either an unrelated class
- or an entity base class (like the 'Entity' class).
- """
- for base in cls.__bases__:
- if isinstance(base, EntityMeta):
- return True
- return False
-
-
-# Note that we don't use inspect.getmembers because of
-# http://bugs.python.org/issue1785
-# See also http://elixir.ematia.de/trac/changeset/262
-def getmembers(object, predicate=None):
- base_props = []
- for key in dir(object):
- try:
- value = getattr(object, key)
- except AttributeError:
- continue
- if not predicate or predicate(value):
- base_props.append((key, value))
- return base_props
-
-def is_abstract_entity(dict_or_cls):
- if not isinstance(dict_or_cls, dict):
- dict_or_cls = dict_or_cls.__dict__
- for mutator, args, kwargs in dict_or_cls.get(MUTATORS, []):
- if 'abstract' in kwargs:
- return kwargs['abstract']
-
- return False
-
-def instrument_class(cls):
- """
- Instrument a class as an Entity. This is usually done automatically through
- the EntityMeta metaclass.
- """
- # Create the entity descriptor
- desc = cls._descriptor = EntityDescriptor(cls)
-
- # Process mutators
- # We *do* want mutators to be processed for base/abstract classes
- # (so that statements like using_options_defaults work).
- process_mutators(cls)
-
- # We do not want to do any more processing for base/abstract classes
- # (Entity et al.).
- if not is_entity(cls) or is_abstract_entity(cls):
- return
-
- cls.table = None
- cls.mapper = None
-
- # Copy the properties ('Property' instances) of the entity base class(es).
- # We use getmembers (instead of __dict__) so that we also get the
- # properties from the parents of the base class if any.
- base_props = []
- for base in cls.__bases__:
- if isinstance(base, EntityMeta) and \
- (not is_entity(base) or is_abstract_entity(base)):
- base_props += [(name, deepcopy(attr)) for name, attr in
- getmembers(base, lambda a: isinstance(a, Property))]
-
- # Process attributes (using the assignment syntax), looking for
- # 'Property' instances and attaching them to this entity.
- properties = [(name, attr) for name, attr in cls.__dict__.iteritems()
- if isinstance(attr, Property)]
- sorted_props = sorted(base_props + properties,
- key=lambda i: i[1]._counter)
- for name, prop in sorted_props:
- prop.attach(cls, name)
-
- # setup misc options here (like tablename etc.)
- desc.setup_options()
-
-
-class EntityMeta(type):
- """
- Entity meta class.
- You should only use it directly if you want to define your own base class
- for your entities (ie you don't want to use the provided 'Entity' class).
- """
-
- def __init__(cls, name, bases, dict_):
- instrument_class(cls)
-
- def __setattr__(cls, key, value):
- if isinstance(value, Property):
- if hasattr(cls, '_setup_done'):
- raise Exception('Cannot set attribute on a class after '
- 'setup_all')
- else:
- value.attach(cls, key)
- else:
- type.__setattr__(cls, key, value)
-
-
-def setup_entities(entities):
- '''Setup all entities in the list passed as argument'''
-
- for entity in entities:
- # delete all Elixir properties so that it doesn't interfere with
- # SQLAlchemy. At this point they should have be converted to
- # builders.
- for name, attr in entity.__dict__.items():
- if isinstance(attr, Property):
- delattr(entity, name)
-
- for method_name in (
- 'setup_autoload_table', 'create_pk_cols', 'setup_relkeys',
- 'before_table', 'setup_table', 'setup_reltables', 'after_table',
- 'setup_events',
- 'before_mapper', 'setup_mapper', 'after_mapper',
- 'setup_properties',
- 'finalize'):
-# if DEBUG:
-# print "=" * 40
-# print method_name
-# print "=" * 40
- for entity in entities:
-# print entity.__name__, "...",
- if hasattr(entity, '_setup_done'):
-# print "already done"
- continue
- method = getattr(entity._descriptor, method_name)
- method()
-# print "ok"
-
-
-def cleanup_entities(entities):
- """
- Try to revert back the list of entities passed as argument to the state
- they had just before their setup phase.
-
- As of now, this function is *not* functional in that it doesn't revert to
- the exact same state the entities were before setup. For example, the
- properties do not work yet as those would need to be regenerated (since the
- columns they are based on are regenerated too -- and as such the
- corresponding joins are not correct) but this doesn't happen because of
- the way relationship setup is designed to be called only once (especially
- the backref stuff in create_properties).
- """
- for entity in entities:
- desc = entity._descriptor
-
- if hasattr(entity, '_setup_done'):
- del entity._setup_done
-
- entity.table = None
- entity.mapper = None
-
- desc._pk_col_done = False
- desc.has_pk = False
- desc._columns = ColumnCollection()
- desc.constraints = []
- desc.properties = {}
-
-class EntityBase(object):
- """
- This class holds all methods of the "Entity" base class, but does not act
- as a base class itself (it does not use the EntityMeta metaclass), but
- rather as a parent class for Entity. This is meant so that people who want
- to provide their own base class but don't want to loose or copy-paste all
- the methods of Entity can do so by inheriting from EntityBase:
-
- .. sourcecode:: python
-
- class MyBase(EntityBase):
- __metaclass__ = EntityMeta
-
- def myCustomMethod(self):
- # do something great
- """
-
- def __init__(self, **kwargs):
- self.set(**kwargs)
-
- def set(self, **kwargs):
- for key, value in kwargs.iteritems():
- setattr(self, key, value)
-
- @classmethod
- def update_or_create(cls, data, surrogate=True):
- pk_props = cls._descriptor.primary_key_properties
-
- # if all pk are present and not None
- if not [1 for p in pk_props if data.get(p.key) is None]:
- pk_tuple = tuple([data[prop.key] for prop in pk_props])
- record = cls.query.get(pk_tuple)
- if record is None:
- if surrogate:
- raise Exception("Cannot create surrogate with pk")
- else:
- record = cls()
- else:
- if surrogate:
- record = cls()
- else:
- raise Exception("Cannot create non surrogate without pk")
- record.from_dict(data)
- return record
-
- def from_dict(self, data):
- """
- Update a mapped class with data from a JSON-style nested dict/list
- structure.
- """
- # surrogate can be guessed from autoincrement/sequence but I guess
- # that's not 100% reliable, so we'll need an override
-
- mapper = sqlalchemy.orm.object_mapper(self)
-
- for key, value in data.iteritems():
- if isinstance(value, dict):
- dbvalue = getattr(self, key)
- rel_class = mapper.get_property(key).mapper.class_
- pk_props = rel_class._descriptor.primary_key_properties
-
- # If the data doesn't contain any pk, and the relationship
- # already has a value, update that record.
- if not [1 for p in pk_props if p.key in data] and \
- dbvalue is not None:
- dbvalue.from_dict(value)
- else:
- record = rel_class.update_or_create(value)
- setattr(self, key, record)
- elif isinstance(value, list) and \
- value and isinstance(value[0], dict):
-
- rel_class = mapper.get_property(key).mapper.class_
- new_attr_value = []
- for row in value:
- if not isinstance(row, dict):
- raise Exception(
- 'Cannot send mixed (dict/non dict) data '
- 'to list relationships in from_dict data.')
- record = rel_class.update_or_create(row)
- new_attr_value.append(record)
- setattr(self, key, new_attr_value)
- else:
- setattr(self, key, value)
-
- def to_dict(self, deep={}, exclude=[]):
- """Generate a JSON-style nested dict/list structure from an object."""
- col_prop_names = [p.key for p in self.mapper.iterate_properties \
- if isinstance(p, ColumnProperty)]
- data = dict([(name, getattr(self, name))
- for name in col_prop_names if name not in exclude])
- for rname, rdeep in deep.iteritems():
- dbdata = getattr(self, rname)
- #FIXME: use attribute names (ie coltoprop) instead of column names
- fks = self.mapper.get_property(rname).remote_side
- exclude = [c.name for c in fks]
- if dbdata is None:
- data[rname] = None
- elif isinstance(dbdata, list):
- data[rname] = [o.to_dict(rdeep, exclude) for o in dbdata]
- else:
- data[rname] = dbdata.to_dict(rdeep, exclude)
- return data
-
- # session methods
- def flush(self, *args, **kwargs):
- return object_session(self).flush([self], *args, **kwargs)
-
- def delete(self, *args, **kwargs):
- return object_session(self).delete(self, *args, **kwargs)
-
- def expire(self, *args, **kwargs):
- return object_session(self).expire(self, *args, **kwargs)
-
- def refresh(self, *args, **kwargs):
- return object_session(self).refresh(self, *args, **kwargs)
-
- def expunge(self, *args, **kwargs):
- return object_session(self).expunge(self, *args, **kwargs)
-
- # This bunch of session methods, along with all the query methods below
- # only make sense when using a global/scoped/contextual session.
- @property
- def _global_session(self):
- return self._descriptor.session.registry()
-
- #FIXME: remove all deprecated methods, possibly all of these
- def merge(self, *args, **kwargs):
- return self._global_session.merge(self, *args, **kwargs)
-
- def save(self, *args, **kwargs):
- return self._global_session.save(self, *args, **kwargs)
-
- def update(self, *args, **kwargs):
- return self._global_session.update(self, *args, **kwargs)
-
- # only exist in SA < 0.5
- # IMO, the replacement (session.add) doesn't sound good enough to be added
- # here. For example: "o = Order(); o.add()" is not very telling. It's
- # better to leave it as "session.add(o)"
- def save_or_update(self, *args, **kwargs):
- return self._global_session.save_or_update(self, *args, **kwargs)
-
- # query methods
- @classmethod
- def get_by(cls, *args, **kwargs):
- """
- Returns the first instance of this class matching the given criteria.
- This is equivalent to:
- session.query(MyClass).filter_by(...).first()
- """
- return cls.query.filter_by(*args, **kwargs).first()
-
- @classmethod
- def get(cls, *args, **kwargs):
- """
- Return the instance of this class based on the given identifier,
- or None if not found. This is equivalent to:
- session.query(MyClass).get(...)
- """
- return cls.query.get(*args, **kwargs)
-
-
-class Entity(EntityBase):
- '''
- The base class for all entities
-
- All Elixir model objects should inherit from this class. Statements can
- appear within the body of the definition of an entity to define its
- fields, relationships, and other options.
-
- Here is an example:
-
- .. sourcecode:: python
-
- class Person(Entity):
- name = Field(Unicode(128))
- birthdate = Field(DateTime, default=datetime.now)
-
- Please note, that if you don't specify any primary keys, Elixir will
- automatically create one called ``id``.
-
- For further information, please refer to the provided examples or
- tutorial.
- '''
- __metaclass__ = EntityMeta
-
-
diff --git a/libs/elixir/events.py b/libs/elixir/events.py
deleted file mode 100644
index 293a8a4a..00000000
--- a/libs/elixir/events.py
+++ /dev/null
@@ -1,27 +0,0 @@
-from sqlalchemy.orm import reconstructor
-
-__all__ = [
- 'before_insert',
- 'after_insert',
- 'before_update',
- 'after_update',
- 'before_delete',
- 'after_delete',
- 'reconstructor'
-]
-
-def create_decorator(event_name):
- def decorator(func):
- if not hasattr(func, '_elixir_events'):
- func._elixir_events = []
- func._elixir_events.append(event_name)
- return func
- return decorator
-
-before_insert = create_decorator('before_insert')
-after_insert = create_decorator('after_insert')
-before_update = create_decorator('before_update')
-after_update = create_decorator('after_update')
-before_delete = create_decorator('before_delete')
-after_delete = create_decorator('after_delete')
-
diff --git a/libs/elixir/ext/__init__.py b/libs/elixir/ext/__init__.py
deleted file mode 100644
index c8708f25..00000000
--- a/libs/elixir/ext/__init__.py
+++ /dev/null
@@ -1,5 +0,0 @@
-'''
-Ext package
-
-Additional Elixir statements and functionality.
-'''
diff --git a/libs/elixir/ext/associable.py b/libs/elixir/ext/associable.py
deleted file mode 100644
index b31c5a74..00000000
--- a/libs/elixir/ext/associable.py
+++ /dev/null
@@ -1,234 +0,0 @@
-'''
-Associable Elixir Statement Generator
-
-==========
-Associable
-==========
-
-About Polymorphic Associations
-------------------------------
-
-A frequent pattern in database schemas is the has_and_belongs_to_many, or a
-many-to-many table. Quite often multiple tables will refer to a single one
-creating quite a few many-to-many intermediate tables.
-
-Polymorphic associations lower the amount of many-to-many tables by setting up
-a table that allows relations to any other table in the database, and relates
-it to the associable table. In some implementations, this layout does not
-enforce referential integrity with database foreign key constraints, this
-implementation uses an additional many-to-many table with foreign key
-constraints to avoid this problem.
-
-.. note:
- SQLite does not support foreign key constraints, so referential integrity
- can only be enforced using database backends with such support.
-
-Elixir Statement Generator for Polymorphic Associations
--------------------------------------------------------
-
-The ``associable`` function generates the intermediary tables for an Elixir
-entity that should be associable with other Elixir entities and returns an
-Elixir Statement for use with them. This automates the process of creating the
-polymorphic association tables and ensuring their referential integrity.
-
-Matching select_XXX and select_by_XXX are also added to the associated entity
-which allow queries to be run for the associated objects.
-
-Example usage:
-
-.. sourcecode:: python
-
- class Tag(Entity):
- name = Field(Unicode)
-
- acts_as_taggable = associable(Tag)
-
- class Entry(Entity):
- title = Field(Unicode)
- acts_as_taggable('tags')
-
- class Article(Entity):
- title = Field(Unicode)
- acts_as_taggable('tags')
-
-Or if one of the entities being associated should only have a single member of
-the associated table:
-
-.. sourcecode:: python
-
- class Address(Entity):
- street = Field(String(130))
- city = Field(String(100))
-
- is_addressable = associable(Address, 'addresses')
-
- class Person(Entity):
- name = Field(Unicode)
- orders = OneToMany('Order')
- is_addressable()
-
- class Order(Entity):
- order_num = Field(primary_key=True)
- item_count = Field(Integer)
- person = ManyToOne('Person')
- is_addressable('address', uselist=False)
-
- home = Address(street='123 Elm St.', city='Spooksville')
- user = Person(name='Jane Doe')
- user.addresses.append(home)
-
- neworder = Order(item_count=4)
- neworder.address = home
- user.orders.append(neworder)
-
- # Queries using the added helpers
- Person.select_by_addresses(city='Cupertino')
- Person.select_addresses(and_(Address.c.street=='132 Elm St',
- Address.c.city=='Smallville'))
-
-Statement Options
------------------
-
-The generated Elixir Statement has several options available:
-
-+---------------+-------------------------------------------------------------+
-| Option Name | Description |
-+===============+=============================================================+
-| ``name`` | Specify a custom name for the Entity attribute. This is |
-| | used to declare the attribute used to access the associated |
-| | table values. Otherwise, the name will use the plural_name |
-| | provided to the associable call. |
-+---------------+-------------------------------------------------------------+
-| ``uselist`` | Whether or not the associated table should be represented |
-| | as a list, or a single property. It should be set to False |
-| | when the entity should only have a single associated |
-| | entity. Defaults to True. |
-+---------------+-------------------------------------------------------------+
-| ``lazy`` | Determines eager loading of the associated entity objects. |
-| | Defaults to False, to indicate that they should not be |
-| | lazily loaded. |
-+---------------+-------------------------------------------------------------+
-'''
-from elixir.statements import Statement
-import sqlalchemy as sa
-
-__doc_all__ = ['associable']
-
-
-def associable(assoc_entity, plural_name=None, lazy=True):
- '''
- Generate an associable Elixir Statement
- '''
- interface_name = assoc_entity._descriptor.tablename
- able_name = interface_name + 'able'
-
- if plural_name:
- attr_name = "%s_rel" % plural_name
- else:
- plural_name = interface_name
- attr_name = "%s_rel" % interface_name
-
- class GenericAssoc(object):
-
- def __init__(self, tablename):
- self.type = tablename
-
- #TODO: inherit from entity builder
- class Associable(object):
- """An associable Elixir Statement object"""
-
- def __init__(self, entity, name=None, uselist=True, lazy=True):
- self.entity = entity
- self.lazy = lazy
- self.uselist = uselist
-
- if name is None:
- self.name = plural_name
- else:
- self.name = name
-
- def after_table(self):
- col = sa.Column('%s_assoc_id' % interface_name, sa.Integer,
- sa.ForeignKey('%s.id' % able_name))
- self.entity._descriptor.add_column(col)
-
- if not hasattr(assoc_entity, '_assoc_table'):
- metadata = assoc_entity._descriptor.metadata
- association_table = sa.Table("%s" % able_name, metadata,
- sa.Column('id', sa.Integer, primary_key=True),
- sa.Column('type', sa.String(40), nullable=False),
- )
- tablename = "%s_to_%s" % (able_name, interface_name)
- association_to_table = sa.Table(tablename, metadata,
- sa.Column('assoc_id', sa.Integer,
- sa.ForeignKey(association_table.c.id,
- ondelete="CASCADE"),
- primary_key=True),
- #FIXME: this assumes a single id col
- sa.Column('%s_id' % interface_name, sa.Integer,
- sa.ForeignKey(assoc_entity.table.c.id,
- ondelete="RESTRICT"),
- primary_key=True),
- )
-
- assoc_entity._assoc_table = association_table
- assoc_entity._assoc_to_table = association_to_table
-
- def after_mapper(self):
- if not hasattr(assoc_entity, '_assoc_mapper'):
- assoc_entity._assoc_mapper = sa.orm.mapper(
- GenericAssoc, assoc_entity._assoc_table, properties={
- 'targets': sa.orm.relation(
- assoc_entity,
- secondary=assoc_entity._assoc_to_table,
- lazy=lazy, backref='associations',
- order_by=assoc_entity.mapper.order_by)
- })
-
- entity = self.entity
- entity.mapper.add_property(
- attr_name,
- sa.orm.relation(GenericAssoc, lazy=self.lazy,
- backref='_backref_%s' % entity.table.name)
- )
-
- if self.uselist:
- def get(self):
- if getattr(self, attr_name) is None:
- setattr(self, attr_name,
- GenericAssoc(entity.table.name))
- return getattr(self, attr_name).targets
- setattr(entity, self.name, property(get))
- else:
- # scalar based property decorator
- def get(self):
- attr = getattr(self, attr_name)
- if attr is not None:
- return attr.targets[0]
- else:
- return None
- def set(self, value):
- if getattr(self, attr_name) is None:
- setattr(self, attr_name,
- GenericAssoc(entity.table.name))
- getattr(self, attr_name).targets = [value]
- setattr(entity, self.name, property(get, set))
-
- # self.name is both set via mapper synonym and the python
- # property, but that's how synonym properties work.
- # adding synonym property after "real" property otherwise it
- # breaks when using SQLAlchemy > 0.4.1
- entity.mapper.add_property(self.name, sa.orm.synonym(attr_name))
-
- # add helper methods
- def select_by(cls, **kwargs):
- return cls.query.join(attr_name, 'targets') \
- .filter_by(**kwargs).all()
- setattr(entity, 'select_by_%s' % self.name, classmethod(select_by))
-
- def select(cls, *args, **kwargs):
- return cls.query.join(attr_name, 'targets') \
- .filter(*args, **kwargs).all()
- setattr(entity, 'select_%s' % self.name, classmethod(select))
-
- return Statement(Associable)
diff --git a/libs/elixir/ext/encrypted.py b/libs/elixir/ext/encrypted.py
deleted file mode 100644
index 410855d2..00000000
--- a/libs/elixir/ext/encrypted.py
+++ /dev/null
@@ -1,124 +0,0 @@
-'''
-An encryption plugin for Elixir utilizing the excellent PyCrypto library, which
-can be downloaded here: http://www.amk.ca/python/code/crypto
-
-Values for columns that are specified to be encrypted will be transparently
-encrypted and safely encoded for storage in a unicode column using the powerful
-and secure Blowfish Cipher using a specified "secret" which can be passed into
-the plugin at class declaration time.
-
-Example usage:
-
-.. sourcecode:: python
-
- from elixir import *
- from elixir.ext.encrypted import acts_as_encrypted
-
- class Person(Entity):
- name = Field(Unicode)
- password = Field(Unicode)
- ssn = Field(Unicode)
- acts_as_encrypted(for_fields=['password', 'ssn'],
- with_secret='secret')
-
-The above Person entity will automatically encrypt and decrypt the password and
-ssn columns on save, update, and load. Different secrets can be specified on
-an entity by entity basis, for added security.
-
-**Important note**: instance attributes are encrypted in-place. This means that
-if one of the encrypted attributes of an instance is accessed after the
-instance has been flushed to the database (and thus encrypted), the value for
-that attribute will be crypted in the in-memory object in addition to the
-database row.
-'''
-
-from Crypto.Cipher import Blowfish
-from elixir.statements import Statement
-from sqlalchemy.orm import MapperExtension, EXT_CONTINUE, EXT_STOP
-
-try:
- from sqlalchemy.orm import EXT_PASS
- SA05orlater = False
-except ImportError:
- SA05orlater = True
-
-__all__ = ['acts_as_encrypted']
-__doc_all__ = []
-
-
-#
-# encryption and decryption functions
-#
-
-def encrypt_value(value, secret):
- return Blowfish.new(secret, Blowfish.MODE_CFB) \
- .encrypt(value).encode('string_escape')
-
-def decrypt_value(value, secret):
- return Blowfish.new(secret, Blowfish.MODE_CFB) \
- .decrypt(value.decode('string_escape'))
-
-
-#
-# acts_as_encrypted statement
-#
-
-class ActsAsEncrypted(object):
-
- def __init__(self, entity, for_fields=[], with_secret='abcdef'):
-
- def perform_encryption(instance, encrypt=True):
- encrypted = getattr(instance, '_elixir_encrypted', None)
- if encrypted is encrypt:
- # skipping encryption or decryption, as it is already done
- return
- else:
- # marking instance as already encrypted/decrypted
- instance._elixir_encrypted = encrypt
-
- if encrypt:
- func = encrypt_value
- else:
- func = decrypt_value
-
- for column_name in for_fields:
- current_value = getattr(instance, column_name)
- if current_value:
- setattr(instance, column_name,
- func(current_value, with_secret))
-
- def perform_decryption(instance):
- perform_encryption(instance, encrypt=False)
-
- class EncryptedMapperExtension(MapperExtension):
-
- def before_insert(self, mapper, connection, instance):
- perform_encryption(instance)
- return EXT_CONTINUE
-
- def before_update(self, mapper, connection, instance):
- perform_encryption(instance)
- return EXT_CONTINUE
-
- if SA05orlater:
- def reconstruct_instance(self, mapper, instance):
- perform_decryption(instance)
- # no special return value is required for
- # reconstruct_instance, but you never know...
- return EXT_CONTINUE
- else:
- def populate_instance(self, mapper, selectcontext, row,
- instance, *args, **kwargs):
- mapper.populate_instance(selectcontext, instance, row,
- *args, **kwargs)
- perform_decryption(instance)
- # EXT_STOP because we already did populate the instance and
- # the normal processing should not happen
- return EXT_STOP
-
- # make sure that the entity's mapper has our mapper extension
- entity._descriptor.add_mapper_extension(EncryptedMapperExtension())
-
-
-acts_as_encrypted = Statement(ActsAsEncrypted)
-
diff --git a/libs/elixir/ext/perform_ddl.py b/libs/elixir/ext/perform_ddl.py
deleted file mode 100644
index bb8528df..00000000
--- a/libs/elixir/ext/perform_ddl.py
+++ /dev/null
@@ -1,106 +0,0 @@
-'''
-DDL statements for Elixir.
-
-Entities having the perform_ddl statement, will automatically execute the
-given DDL statement, at the given moment: ether before or after the table
-creation in SQL.
-
-The 'when' argument can be either 'before-create' or 'after-create'.
-The 'statement' argument can be one of:
-
-- a single string statement
-- a list of string statements, in which case, each of them will be executed
- in turn.
-- a callable which should take no argument and return either a single string
- or a list of strings.
-
-In each string statement, you may use the special '%(fullname)s' construct,
-that will be replaced with the real table name including schema, if unknown
-to you. Also, self explained '%(table)s' and '%(schema)s' may be used here.
-
-You would use this extension to handle non elixir sql statemts, like triggers
-etc.
-
-.. sourcecode:: python
-
- class Movie(Entity):
- title = Field(Unicode(30), primary_key=True)
- year = Field(Integer)
-
- perform_ddl('after-create',
- "insert into %(fullname)s values ('Alien', 1979)")
-
-preload_data is a more specific statement meant to preload data in your
-entity table from a list of tuples (of fields values for each row).
-
-.. sourcecode:: python
-
- class Movie(Entity):
- title = Field(Unicode(30), primary_key=True)
- year = Field(Integer)
-
- preload_data(('title', 'year'),
- [(u'Alien', 1979), (u'Star Wars', 1977)])
- preload_data(('year', 'title'),
- [(1982, u'Blade Runner')])
- preload_data(data=[(u'Batman', 1966)])
-'''
-
-from elixir.statements import Statement
-from elixir.properties import EntityBuilder
-from sqlalchemy import DDL
-
-__all__ = ['perform_ddl', 'preload_data']
-__doc_all__ = []
-
-#
-# the perform_ddl statement
-#
-class PerformDDLEntityBuilder(EntityBuilder):
-
- def __init__(self, entity, when, statement, on=None, context=None):
- self.entity = entity
- self.when = when
- self.statement = statement
- self.on = on
- self.context = context
-
- def after_table(self):
- statement = self.statement
- if hasattr(statement, '__call__'):
- statement = statement()
- if not isinstance(statement, list):
- statement = [statement]
- for s in statement:
- ddl = DDL(s, self.on, self.context)
- ddl.execute_at(self.when, self.entity.table)
-
-perform_ddl = Statement(PerformDDLEntityBuilder)
-
-#
-# the preload_data statement
-#
-class PreloadDataEntityBuilder(EntityBuilder):
-
- def __init__(self, entity, columns=None, data=None):
- self.entity = entity
- self.columns = columns
- self.data = data
-
- def after_table(self):
- all_columns = [col.name for col in self.entity.table.columns]
- def onload(event, schema_item, connection):
- columns = self.columns
- if columns is None:
- columns = all_columns
- data = self.data
- if hasattr(data, '__call__'):
- data = data()
- insert = schema_item.insert()
- connection.execute(insert,
- [dict(zip(columns, values)) for values in data])
-
- self.entity.table.append_ddl_listener('after-create', onload)
-
-preload_data = Statement(PreloadDataEntityBuilder)
-
diff --git a/libs/elixir/ext/versioned.py b/libs/elixir/ext/versioned.py
deleted file mode 100644
index 75f406b0..00000000
--- a/libs/elixir/ext/versioned.py
+++ /dev/null
@@ -1,288 +0,0 @@
-'''
-A versioning plugin for Elixir.
-
-Entities that are marked as versioned with the `acts_as_versioned` statement
-will automatically have a history table created and a timestamp and version
-column added to their tables. In addition, versioned entities are provided
-with four new methods: revert, revert_to, compare_with and get_as_of, and one
-new attribute: versions. Entities with compound primary keys are supported.
-
-The `versions` attribute will contain a list of previous versions of the
-instance, in increasing version number order.
-
-The `get_as_of` method will retrieve a previous version of the instance "as of"
-a specified datetime. If the current version is the most recent, it will be
-returned.
-
-The `revert` method will rollback the current instance to its previous version,
-if possible. Once reverted, the current instance will be expired from the
-session, and you will need to fetch it again to retrieve the now reverted
-instance.
-
-The `revert_to` method will rollback the current instance to the specified
-version number, if possibe. Once reverted, the current instance will be expired
-from the session, and you will need to fetch it again to retrieve the now
-reverted instance.
-
-The `compare_with` method will compare the instance with a previous version. A
-dictionary will be returned with each field difference as an element in the
-dictionary where the key is the field name and the value is a tuple of the
-format (current_value, version_value). Version instances also have a
-`compare_with` method so that two versions can be compared.
-
-Also included in the module is a `after_revert` decorator that can be used to
-decorate methods on the versioned entity that will be called following that
-instance being reverted.
-
-The acts_as_versioned statement also accepts an optional `ignore` argument
-that consists of a list of strings, specifying names of fields. Changes in
-those fields will not result in a version increment. In addition, you can
-pass in an optional `check_concurrent` argument, which will use SQLAlchemy's
-built-in optimistic concurrency mechanisms.
-
-Note that relationships that are stored in mapping tables will not be included
-as part of the versioning process, and will need to be handled manually. Only
-values within the entity's main table will be versioned into the history table.
-'''
-
-from datetime import datetime
-import inspect
-
-from sqlalchemy import Table, Column, and_, desc
-from sqlalchemy.orm import mapper, MapperExtension, EXT_CONTINUE, \
- object_session
-
-from elixir import Integer, DateTime
-from elixir.statements import Statement
-from elixir.properties import EntityBuilder
-from elixir.entity import getmembers
-
-__all__ = ['acts_as_versioned', 'after_revert']
-__doc_all__ = []
-
-#
-# utility functions
-#
-
-def get_entity_where(instance):
- clauses = []
- for column in instance.table.primary_key.columns:
- instance_value = getattr(instance, column.name)
- clauses.append(column==instance_value)
- return and_(*clauses)
-
-
-def get_history_where(instance):
- clauses = []
- history_columns = instance.__history_table__.primary_key.columns
- for column in instance.table.primary_key.columns:
- instance_value = getattr(instance, column.name)
- history_column = getattr(history_columns, column.name)
- clauses.append(history_column==instance_value)
- return and_(*clauses)
-
-
-#
-# a mapper extension to track versions on insert, update, and delete
-#
-
-class VersionedMapperExtension(MapperExtension):
- def before_insert(self, mapper, connection, instance):
- version_colname, timestamp_colname = \
- instance.__class__.__versioned_column_names__
- setattr(instance, version_colname, 1)
- setattr(instance, timestamp_colname, datetime.now())
- return EXT_CONTINUE
-
- def before_update(self, mapper, connection, instance):
- old_values = instance.table.select(get_entity_where(instance)) \
- .execute().fetchone()
-
- # SA might've flagged this for an update even though it didn't change.
- # This occurs when a relation is updated, thus marking this instance
- # for a save/update operation. We check here against the last version
- # to ensure we really should save this version and update the version
- # data.
- ignored = instance.__class__.__ignored_fields__
- version_colname, timestamp_colname = \
- instance.__class__.__versioned_column_names__
- for key in instance.table.c.keys():
- if key in ignored:
- continue
- if getattr(instance, key) != old_values[key]:
- # the instance was really updated, so we create a new version
- dict_values = dict(old_values.items())
- connection.execute(
- instance.__class__.__history_table__.insert(), dict_values)
- old_version = getattr(instance, version_colname)
- setattr(instance, version_colname, old_version + 1)
- setattr(instance, timestamp_colname, datetime.now())
- break
-
- return EXT_CONTINUE
-
- def before_delete(self, mapper, connection, instance):
- connection.execute(instance.__history_table__.delete(
- get_history_where(instance)
- ))
- return EXT_CONTINUE
-
-
-versioned_mapper_extension = VersionedMapperExtension()
-
-
-#
-# the acts_as_versioned statement
-#
-
-class VersionedEntityBuilder(EntityBuilder):
-
- def __init__(self, entity, ignore=None, check_concurrent=False,
- column_names=None):
- self.entity = entity
- self.add_mapper_extension(versioned_mapper_extension)
- #TODO: we should rather check that the version_id_col isn't set
- # externally
- self.check_concurrent = check_concurrent
-
- # Changes in these fields will be ignored
- if column_names is None:
- column_names = ['version', 'timestamp']
- entity.__versioned_column_names__ = column_names
- if ignore is None:
- ignore = []
- ignore.extend(column_names)
- entity.__ignored_fields__ = ignore
-
- def create_non_pk_cols(self):
- # add a version column to the entity, along with a timestamp
- version_colname, timestamp_colname = \
- self.entity.__versioned_column_names__
- #XXX: fail in case the columns already exist?
- #col_names = [col.name for col in self.entity._descriptor.columns]
- #if version_colname not in col_names:
- self.add_table_column(Column(version_colname, Integer))
- #if timestamp_colname not in col_names:
- self.add_table_column(Column(timestamp_colname, DateTime))
-
- # add a concurrent_version column to the entity, if required
- if self.check_concurrent:
- self.entity._descriptor.version_id_col = 'concurrent_version'
-
- # we copy columns from the main entity table, so we need it to exist first
- def after_table(self):
- entity = self.entity
- version_colname, timestamp_colname = \
- entity.__versioned_column_names__
-
- # look for events
- after_revert_events = []
- for name, func in getmembers(entity, inspect.ismethod):
- if getattr(func, '_elixir_after_revert', False):
- after_revert_events.append(func)
-
- # create a history table for the entity
- skipped_columns = [version_colname]
- if self.check_concurrent:
- skipped_columns.append('concurrent_version')
-
- columns = [
- column.copy() for column in entity.table.c
- if column.name not in skipped_columns
- ]
- columns.append(Column(version_colname, Integer, primary_key=True))
- table = Table(entity.table.name + '_history', entity.table.metadata,
- *columns
- )
- entity.__history_table__ = table
-
- # create an object that represents a version of this entity
- class Version(object):
- pass
-
- # map the version class to the history table for this entity
- Version.__name__ = entity.__name__ + 'Version'
- Version.__versioned_entity__ = entity
- mapper(Version, entity.__history_table__)
-
- version_col = getattr(table.c, version_colname)
- timestamp_col = getattr(table.c, timestamp_colname)
-
- # attach utility methods and properties to the entity
- def get_versions(self):
- v = object_session(self).query(Version) \
- .filter(get_history_where(self)) \
- .order_by(version_col) \
- .all()
- # history contains all the previous records.
- # Add the current one to the list to get all the versions
- v.append(self)
- return v
-
- def get_as_of(self, dt):
- # if the passed in timestamp is older than our current version's
- # time stamp, then the most recent version is our current version
- if getattr(self, timestamp_colname) < dt:
- return self
-
- # otherwise, we need to look to the history table to get our
- # older version
- sess = object_session(self)
- query = sess.query(Version) \
- .filter(and_(get_history_where(self),
- timestamp_col <= dt)) \
- .order_by(desc(timestamp_col)).limit(1)
- return query.first()
-
- def revert_to(self, to_version):
- if isinstance(to_version, Version):
- to_version = getattr(to_version, version_colname)
-
- old_version = table.select(and_(
- get_history_where(self),
- version_col == to_version
- )).execute().fetchone()
-
- entity.table.update(get_entity_where(self)).execute(
- dict(old_version.items())
- )
-
- table.delete(and_(get_history_where(self),
- version_col >= to_version)).execute()
- self.expire()
- for event in after_revert_events:
- event(self)
-
- def revert(self):
- assert getattr(self, version_colname) > 1
- self.revert_to(getattr(self, version_colname) - 1)
-
- def compare_with(self, version):
- differences = {}
- for column in self.table.c:
- if column.name in (version_colname, 'concurrent_version'):
- continue
- this = getattr(self, column.name)
- that = getattr(version, column.name)
- if this != that:
- differences[column.name] = (this, that)
- return differences
-
- entity.versions = property(get_versions)
- entity.get_as_of = get_as_of
- entity.revert_to = revert_to
- entity.revert = revert
- entity.compare_with = compare_with
- Version.compare_with = compare_with
-
-acts_as_versioned = Statement(VersionedEntityBuilder)
-
-
-def after_revert(func):
- """
- Decorator for watching for revert events.
- """
- func._elixir_after_revert = True
- return func
-
-
diff --git a/libs/elixir/fields.py b/libs/elixir/fields.py
deleted file mode 100644
index 8659cdd8..00000000
--- a/libs/elixir/fields.py
+++ /dev/null
@@ -1,191 +0,0 @@
-'''
-This module provides support for defining the fields (columns) of your
-entities. Elixir currently supports two syntaxes to do so: the default
-`Attribute-based syntax`_ as well as the has_field_ DSL statement.
-
-Attribute-based syntax
-----------------------
-
-Here is a quick example of how to use the object-oriented syntax.
-
-.. sourcecode:: python
-
- class Person(Entity):
- id = Field(Integer, primary_key=True)
- name = Field(String(50), required=True)
- ssn = Field(String(50), unique=True)
- biography = Field(Text)
- join_date = Field(DateTime, default=datetime.datetime.now)
- photo = Field(Binary, deferred=True)
- _email = Field(String(20), colname='email', synonym='email')
-
- def _set_email(self, email):
- self._email = email
- def _get_email(self):
- return self._email
- email = property(_get_email, _set_email)
-
-
-The Field class takes one mandatory argument, which is its type. Please refer
-to SQLAlchemy documentation for a list of `types supported by SQLAlchemy
-`_.
-
-Following that first mandatory argument, fields can take any number of
-optional keyword arguments. Please note that all the **arguments** that are
-**not specifically processed by Elixir**, as mentioned in the documentation
-below **are passed on to the SQLAlchemy ``Column`` object**. Please refer to
-the `SQLAlchemy Column object's documentation
-`_ for more details about other
-supported keyword arguments.
-
-The following Elixir-specific arguments are supported:
-
-+-------------------+---------------------------------------------------------+
-| Argument Name | Description |
-+===================+=========================================================+
-| ``required`` | Specify whether or not this field can be set to None |
-| | (left without a value). Defaults to ``False``, unless |
-| | the field is a primary key. |
-+-------------------+---------------------------------------------------------+
-| ``colname`` | Specify a custom name for the column of this field. By |
-| | default the column will have the same name as the |
-| | attribute. |
-+-------------------+---------------------------------------------------------+
-| ``deferred`` | Specify whether this particular column should be |
-| | fetched by default (along with the other columns) when |
-| | an instance of the entity is fetched from the database |
-| | or rather only later on when this particular column is |
-| | first referenced. This can be useful when one wants to |
-| | avoid loading a large text or binary field into memory |
-| | when its not needed. Individual columns can be lazy |
-| | loaded by themselves (by using ``deferred=True``) |
-| | or placed into groups that lazy-load together (by using |
-| | ``deferred`` = `"group_name"`). |
-+-------------------+---------------------------------------------------------+
-| ``synonym`` | Specify a synonym name for this field. The field will |
-| | also be usable under that name in keyword-based Query |
-| | functions such as filter_by. The Synonym class (see the |
-| | `properties` module) provides a similar functionality |
-| | with an (arguably) nicer syntax, but a limited scope. |
-+-------------------+---------------------------------------------------------+
-
-has_field
----------
-
-The `has_field` statement allows you to define fields one at a time.
-
-The first argument is the name of the field, the second is its type. Following
-these, any number of keyword arguments can be specified for additional
-behavior. The following arguments are supported:
-
-+-------------------+---------------------------------------------------------+
-| Argument Name | Description |
-+===================+=========================================================+
-| ``through`` | Specify a relation name to go through. This field will |
-| | not exist as a column on the database but will be a |
-| | property which automatically proxy values to the |
-| | ``attribute`` attribute of the object pointed to by the |
-| | relation. If the ``attribute`` argument is not present, |
-| | the name of the current field will be used. In an |
-| | has_field statement, you can only proxy through a |
-| | belongs_to or an has_one relationship. |
-+-------------------+---------------------------------------------------------+
-| ``attribute`` | Name of the "endpoint" attribute to proxy to. This |
-| | should only be used in combination with the ``through`` |
-| | argument. |
-+-------------------+---------------------------------------------------------+
-
-
-Here is a quick example of how to use ``has_field``.
-
-.. sourcecode:: python
-
- class Person(Entity):
- has_field('id', Integer, primary_key=True)
- has_field('name', String(50))
-'''
-from sqlalchemy import Column
-from sqlalchemy.orm import deferred, synonym
-from sqlalchemy.ext.associationproxy import association_proxy
-
-from elixir.statements import ClassMutator
-from elixir.properties import Property
-
-__doc_all__ = ['Field']
-
-
-class Field(Property):
- '''
- Represents the definition of a 'field' on an entity.
-
- This class represents a column on the table where the entity is stored.
- '''
-
- def __init__(self, type, *args, **kwargs):
- super(Field, self).__init__()
-
- self.colname = kwargs.pop('colname', None)
- self.synonym = kwargs.pop('synonym', None)
- self.deferred = kwargs.pop('deferred', False)
- if 'required' in kwargs:
- kwargs['nullable'] = not kwargs.pop('required')
- self.type = type
- self.primary_key = kwargs.get('primary_key', False)
-
- self.column = None
- self.property = None
-
- self.args = args
- self.kwargs = kwargs
-
- def attach(self, entity, name):
- # If no colname was defined (through the 'colname' kwarg), set
- # it to the name of the attr.
- if self.colname is None:
- self.colname = name
- super(Field, self).attach(entity, name)
-
- def create_pk_cols(self):
- if self.primary_key:
- self.create_col()
-
- def create_non_pk_cols(self):
- if not self.primary_key:
- self.create_col()
-
- def create_col(self):
- self.column = Column(self.colname, self.type,
- *self.args, **self.kwargs)
- self.add_table_column(self.column)
-
- def create_properties(self):
- if self.deferred:
- group = None
- if isinstance(self.deferred, basestring):
- group = self.deferred
- self.property = deferred(self.column, group=group)
- elif self.name != self.colname:
- # if the property name is different from the column name, we need
- # to add an explicit property (otherwise nothing is needed as it's
- # done automatically by SA)
- self.property = self.column
-
- if self.property is not None:
- self.add_mapper_property(self.name, self.property)
-
- if self.synonym:
- self.add_mapper_property(self.synonym, synonym(self.name))
-
-
-def has_field_handler(entity, name, *args, **kwargs):
- if 'through' in kwargs:
- setattr(entity, name,
- association_proxy(kwargs.pop('through'),
- kwargs.pop('attribute', name),
- **kwargs))
- return
- field = Field(*args, **kwargs)
- field.attach(entity, name)
-
-has_field = ClassMutator(has_field_handler)
diff --git a/libs/elixir/options.py b/libs/elixir/options.py
deleted file mode 100644
index 27d7d195..00000000
--- a/libs/elixir/options.py
+++ /dev/null
@@ -1,274 +0,0 @@
-'''
-This module provides support for defining several options on your Elixir
-entities. There are three different kinds of options that can be set
-up, and for this there are three different statements: using_options_,
-using_table_options_ and using_mapper_options_.
-
-Alternatively, these options can be set on all Elixir entities by modifying
-the `options_defaults` dictionary before defining any entity.
-
-`using_options`
----------------
-The 'using_options' DSL statement allows you to set up some additional
-behaviors on your model objects, including table names, ordering, and
-more. To specify an option, simply supply the option as a keyword
-argument onto the statement, as follows:
-
-.. sourcecode:: python
-
- class Person(Entity):
- name = Field(Unicode(64))
-
- using_options(shortnames=True, order_by='name')
-
-The list of supported arguments are as follows:
-
-+---------------------+-------------------------------------------------------+
-| Option Name | Description |
-+=====================+=======================================================+
-| ``inheritance`` | Specify the type of inheritance this entity must use. |
-| | It can be one of ``single``, ``concrete`` or |
-| | ``multi``. Defaults to ``single``. |
-| | Note that polymorphic concrete inheritance is |
-| | currently not implemented. See: |
-| | http://www.sqlalchemy.org/docs/05/mappers.html |
-| | #mapping-class-inheritance-hierarchies for an |
-| | explanation of the different kinds of inheritances. |
-+---------------------+-------------------------------------------------------+
-| ``abstract`` | Set 'abstract'=True to declare abstract entity. |
-| | Abstract base classes are useful when you want to put |
-| | some common information into a number of other |
-| | entities. Abstract entity will not be used to create |
-| | any database table. Instead, when it is used as a base|
-| | class for other entity, its fields will be added to |
-| | those of the child class. |
-+---------------------+-------------------------------------------------------+
-| ``polymorphic`` | Whether the inheritance should be polymorphic or not. |
-| | Defaults to ``True``. The column used to store the |
-| | type of each row is named "row_type" by default. You |
-| | can change this by passing the desired name for the |
-| | column to this argument. |
-+---------------------+-------------------------------------------------------+
-| ``identity`` | Specify a custom polymorphic identity. When using |
-| | polymorphic inheritance, this value (usually a |
-| | string) will represent this particular entity (class) |
-| | . It will be used to differentiate it from other |
-| | entities (classes) in your inheritance hierarchy when |
-| | loading from the database instances of different |
-| | entities in that hierarchy at the same time. |
-| | This value will be stored by default in the |
-| | "row_type" column of the entity's table (see above). |
-| | You can either provide a |
-| | plain string or a callable. The callable will be |
-| | given the entity (ie class) as argument and must |
-| | return a value (usually a string) representing the |
-| | polymorphic identity of that entity. |
-| | By default, this value is automatically generated: it |
-| | is the name of the entity lower-cased. |
-+---------------------+-------------------------------------------------------+
-| ``metadata`` | Specify a custom MetaData for this entity. |
-| | By default, entities uses the global |
-| | ``elixir.metadata``. |
-| | This option can also be set for all entities of a |
-| | module by setting the ``__metadata__`` attribute of |
-| | that module. |
-+---------------------+-------------------------------------------------------+
-| ``autoload`` | Automatically load column definitions from the |
-| | existing database table. |
-+---------------------+-------------------------------------------------------+
-| ``tablename`` | Specify a custom tablename. You can either provide a |
-| | plain string or a callable. The callable will be |
-| | given the entity (ie class) as argument and must |
-| | return a string representing the name of the table |
-| | for that entity. By default, the tablename is |
-| | automatically generated: it is a concatenation of the |
-| | full module-path to the entity and the entity (class) |
-| | name itself. The result is lower-cased and separated |
-| | by underscores ("_"), eg.: for an entity named |
-| | "MyEntity" in the module "project1.model", the |
-| | generated table name will be |
-| | "project1_model_myentity". |
-+---------------------+-------------------------------------------------------+
-| ``shortnames`` | Specify whether or not the automatically generated |
-| | table names include the full module-path |
-| | to the entity. If ``shortnames`` is ``True``, only |
-| | the entity name is used. Defaults to ``False``. |
-+---------------------+-------------------------------------------------------+
-| ``auto_primarykey`` | If given as string, it will represent the |
-| | auto-primary-key's column name. If this option |
-| | is True, it will allow auto-creation of a primary |
-| | key if there's no primary key defined for the |
-| | corresponding entity. If this option is False, |
-| | it will disallow auto-creation of a primary key. |
-| | Defaults to ``True``. |
-+---------------------+-------------------------------------------------------+
-| ``version_id_col`` | If this option is True, it will create a version |
-| | column automatically using the default name. If given |
-| | as string, it will create the column using that name. |
-| | This can be used to prevent concurrent modifications |
-| | to the entity's table rows (i.e. it will raise an |
-| | exception if it happens). Defaults to ``False``. |
-+---------------------+-------------------------------------------------------+
-| ``order_by`` | How to order select results. Either a string or a |
-| | list of strings, composed of the field name, |
-| | optionally lead by a minus (for descending order). |
-+---------------------+-------------------------------------------------------+
-| ``session`` | Specify a custom contextual session for this entity. |
-| | By default, entities uses the global |
-| | ``elixir.session``. |
-| | This option takes a ``ScopedSession`` object or |
-| | ``None``. In the later case your entity will be |
-| | mapped using a non-contextual mapper which requires |
-| | manual session management, as seen in pure SQLAlchemy.|
-| | This option can also be set for all entities of a |
-| | module by setting the ``__session__`` attribute of |
-| | that module. |
-+---------------------+-------------------------------------------------------+
-| ``allowcoloverride``| Specify whether it is allowed to override columns. |
-| | By default, Elixir forbids you to add a column to an |
-| | entity's table which already exist in that table. If |
-| | you set this option to ``True`` it will skip that |
-| | check. Use with care as it is easy to shoot oneself |
-| | in the foot when overriding columns. |
-+---------------------+-------------------------------------------------------+
-
-For examples, please refer to the examples and unit tests.
-
-`using_table_options`
----------------------
-The 'using_table_options' DSL statement allows you to set up some
-additional options on your entity table. It is meant only to handle the
-options which are not supported directly by the 'using_options' statement.
-By opposition to the 'using_options' statement, these options are passed
-directly to the underlying SQLAlchemy Table object (both non-keyword arguments
-and keyword arguments) without any processing.
-
-For further information, please refer to the `SQLAlchemy table's documentation
-`_.
-
-You might also be interested in the section about `constraints
-`_.
-
-`using_mapper_options`
-----------------------
-The 'using_mapper_options' DSL statement allows you to set up some
-additional options on your entity mapper. It is meant only to handle the
-options which are not supported directly by the 'using_options' statement.
-By opposition to the 'using_options' statement, these options are passed
-directly to the underlying SQLAlchemy mapper (as keyword arguments)
-without any processing.
-
-For further information, please refer to the `SQLAlchemy mapper
-function's documentation
-`_.
-
-`using_options_defaults`
-------------------------
-The 'using_options_defaults' DSL statement allows you to set up some
-default options on a custom base class. These will be used as the default value
-for options of all its subclasses. Note that any option not set within the
-using_options_defaults (nor specifically on a particular Entity) will use the
-global defaults, so you don't have to provide a default value for all options,
-but only those you want to change. Please also note that this statement does
-not work on normal entities, and the normal using_options statement does not
-work on base classes (because normal options do not and should not propagate to
-the children classes).
-'''
-
-from sqlalchemy import Integer, String
-
-from elixir.statements import ClassMutator
-
-__doc_all__ = ['options_defaults']
-
-OLD_M2MCOL_NAMEFORMAT = "%(tablename)s_%(key)s%(numifself)s"
-ALTERNATE_M2MCOL_NAMEFORMAT = "%(inversename)s_%(key)s"
-
-def default_m2m_column_formatter(data):
- if data['selfref']:
- return ALTERNATE_M2MCOL_NAMEFORMAT % data
- else:
- return OLD_M2MCOL_NAMEFORMAT % data
-
-NEW_M2MCOL_NAMEFORMAT = default_m2m_column_formatter
-
-# format constants
-FKCOL_NAMEFORMAT = "%(relname)s_%(key)s"
-M2MCOL_NAMEFORMAT = NEW_M2MCOL_NAMEFORMAT
-CONSTRAINT_NAMEFORMAT = "%(tablename)s_%(colnames)s_fk"
-MULTIINHERITANCECOL_NAMEFORMAT = "%(entity)s_%(key)s"
-
-# other global constants
-DEFAULT_AUTO_PRIMARYKEY_NAME = "id"
-DEFAULT_AUTO_PRIMARYKEY_TYPE = Integer
-DEFAULT_VERSION_ID_COL_NAME = "row_version"
-DEFAULT_POLYMORPHIC_COL_NAME = "row_type"
-POLYMORPHIC_COL_SIZE = 40
-POLYMORPHIC_COL_TYPE = String(POLYMORPHIC_COL_SIZE)
-
-# debugging/migration help
-MIGRATION_TO_07_AID = False
-
-#
-options_defaults = dict(
- abstract=False,
- inheritance='single',
- polymorphic=True,
- identity=None,
- autoload=False,
- tablename=None,
- shortnames=False,
- auto_primarykey=True,
- version_id_col=False,
- allowcoloverride=False,
- order_by=None,
- resolve_root=None,
- mapper_options={},
- table_options={}
-)
-
-valid_options = options_defaults.keys() + [
- 'metadata',
- 'session',
- 'collection'
-]
-
-
-def using_options_defaults_handler(entity, **kwargs):
- for kwarg in kwargs:
- if kwarg not in valid_options:
- raise Exception("'%s' is not a valid option for Elixir entities."
- % kwarg)
-
- # We use __dict__ instead of hasattr to not check its presence within the
- # parent, and thus update the parent dict instead of creating a local dict.
- if not entity.__dict__.get('options_defaults'):
- entity.options_defaults = {}
- entity.options_defaults.update(kwargs)
-
-
-def using_options_handler(entity, *args, **kwargs):
- for kwarg in kwargs:
- if kwarg in valid_options:
- setattr(entity._descriptor, kwarg, kwargs[kwarg])
- else:
- raise Exception("'%s' is not a valid option for Elixir entities."
- % kwarg)
-
-
-def using_table_options_handler(entity, *args, **kwargs):
- entity._descriptor.table_args.extend(list(args))
- entity._descriptor.table_options.update(kwargs)
-
-
-def using_mapper_options_handler(entity, *args, **kwargs):
- entity._descriptor.mapper_options.update(kwargs)
-
-
-using_options_defaults = ClassMutator(using_options_defaults_handler)
-using_options = ClassMutator(using_options_handler)
-using_table_options = ClassMutator(using_table_options_handler)
-using_mapper_options = ClassMutator(using_mapper_options_handler)
diff --git a/libs/elixir/properties.py b/libs/elixir/properties.py
deleted file mode 100644
index 68ff8fab..00000000
--- a/libs/elixir/properties.py
+++ /dev/null
@@ -1,244 +0,0 @@
-'''
-This module provides support for defining properties on your entities. It both
-provides, the `Property` class which acts as a building block for common
-properties such as fields and relationships (for those, please consult the
-corresponding modules), but also provides some more specialized properties,
-such as `ColumnProperty` and `Synonym`. It also provides the GenericProperty
-class which allows you to wrap any SQLAlchemy property, and its DSL-syntax
-equivalent: has_property_.
-
-`has_property`
---------------
-The ``has_property`` statement allows you to define properties which rely on
-their entity's table (and columns) being defined before they can be declared
-themselves. The `has_property` statement takes two arguments: first the name of
-the property to be defined and second a function (often given as an anonymous
-lambda) taking one argument and returning the desired SQLAlchemy property. That
-function will be called whenever the entity table is completely defined, and
-will be given the .c attribute of the entity as argument (as a way to access
-the entity columns).
-
-Here is a quick example of how to use ``has_property``.
-
-.. sourcecode:: python
-
- class OrderLine(Entity):
- has_field('quantity', Float)
- has_field('unit_price', Float)
- has_property('price',
- lambda c: column_property(
- (c.quantity * c.unit_price).label('price')))
-'''
-
-from elixir.statements import PropertyStatement
-from sqlalchemy.orm import column_property, synonym
-
-__doc_all__ = ['EntityBuilder', 'Property', 'GenericProperty',
- 'ColumnProperty']
-
-class EntityBuilder(object):
- '''
- Abstract base class for all entity builders. An Entity builder is a class
- of objects which can be added to an Entity (usually by using special
- properties or statements) to "build" that entity. Building an entity,
- meaning to add columns to its "main" table, create other tables, add
- properties to its mapper, ... To do so an EntityBuilder must override the
- corresponding method(s). This is to ensure the different operations happen
- in the correct order (for example, that the table is fully created before
- the mapper that use it is defined).
- '''
- def create_pk_cols(self):
- pass
-
- def create_non_pk_cols(self):
- pass
-
- def before_table(self):
- pass
-
- def create_tables(self):
- '''
- Subclasses may override this method to create tables.
- '''
-
- def after_table(self):
- pass
-
- def create_properties(self):
- '''
- Subclasses may override this method to add properties to the involved
- entity.
- '''
-
- def before_mapper(self):
- pass
-
- def after_mapper(self):
- pass
-
- def finalize(self):
- pass
-
- # helper methods
- def add_table_column(self, column):
- self.entity._descriptor.add_column(column)
-
- def add_mapper_property(self, name, prop):
- self.entity._descriptor.add_property(name, prop)
-
- def add_mapper_extension(self, ext):
- self.entity._descriptor.add_mapper_extension(ext)
-
-
-class CounterMeta(type):
- '''
- A simple meta class which adds a ``_counter`` attribute to the instances of
- the classes it is used on. This counter is simply incremented for each new
- instance.
- '''
- counter = 0
-
- def __call__(self, *args, **kwargs):
- instance = type.__call__(self, *args, **kwargs)
- instance._counter = CounterMeta.counter
- CounterMeta.counter += 1
- return instance
-
-
-class Property(EntityBuilder):
- '''
- Abstract base class for all properties of an Entity.
- '''
- __metaclass__ = CounterMeta
-
- def __init__(self, *args, **kwargs):
- self.entity = None
- self.name = None
-
- def attach(self, entity, name):
- """Attach this property to its entity, using 'name' as name.
-
- Properties will be attached in the order they were declared.
- """
- self.entity = entity
- self.name = name
-
- # register this property as a builder
- entity._descriptor.builders.append(self)
-
- def __repr__(self):
- return "Property(%s, %s)" % (self.name, self.entity)
-
-
-class GenericProperty(Property):
- '''
- Generic catch-all class to wrap an SQLAlchemy property.
-
- .. sourcecode:: python
-
- class OrderLine(Entity):
- quantity = Field(Float)
- unit_price = Field(Numeric)
- price = GenericProperty(lambda c: column_property(
- (c.quantity * c.unit_price).label('price')))
- '''
-
- def __init__(self, prop, *args, **kwargs):
- super(GenericProperty, self).__init__(*args, **kwargs)
- self.prop = prop
- #XXX: move this to Property?
- self.args = args
- self.kwargs = kwargs
-
- def create_properties(self):
- if hasattr(self.prop, '__call__'):
- prop_value = self.prop(self.entity.table.c)
- else:
- prop_value = self.prop
- prop_value = self.evaluate_property(prop_value)
- self.add_mapper_property(self.name, prop_value)
-
- def evaluate_property(self, prop):
- if self.args or self.kwargs:
- raise Exception('superfluous arguments passed to GenericProperty')
- return prop
-
-
-class ColumnProperty(GenericProperty):
- '''
- A specialized form of the GenericProperty to generate SQLAlchemy
- ``column_property``'s.
-
- It takes a function (often given as an anonymous lambda) as its first
- argument. Other arguments and keyword arguments are forwarded to the
- column_property construct. That first-argument function must accept exactly
- one argument and must return the desired (scalar-returning) SQLAlchemy
- ClauseElement.
-
- The function will be called whenever the entity table is completely
- defined, and will be given
- the .c attribute of the table of the entity as argument (as a way to
- access the entity columns). The ColumnProperty will first wrap your
- ClauseElement in an
- "empty" label (ie it will be labelled automatically during queries),
- then wrap that in a column_property.
-
- .. sourcecode:: python
-
- class OrderLine(Entity):
- quantity = Field(Float)
- unit_price = Field(Numeric)
- price = ColumnProperty(lambda c: c.quantity * c.unit_price,
- deferred=True)
-
- Please look at the `corresponding SQLAlchemy
- documentation `_ for details.
- '''
-
- def evaluate_property(self, prop):
- return column_property(prop.label(None), *self.args, **self.kwargs)
-
-
-class Synonym(GenericProperty):
- '''
- This class represents a synonym property of another property (column, ...)
- of an entity. As opposed to the `synonym` kwarg to the Field class (which
- share the same goal), this class can be used to define a synonym of a
- property defined in a parent class (of the current class). On the other
- hand, it cannot define a synonym for the purpose of using a standard python
- property in queries. See the Field class for details on that usage.
-
- .. sourcecode:: python
-
- class Person(Entity):
- name = Field(String(30))
- primary_email = Field(String(100))
- email_address = Synonym('primary_email')
-
- class User(Person):
- user_name = Synonym('name')
- password = Field(String(20))
- '''
-
- def evaluate_property(self, prop):
- return synonym(prop, *self.args, **self.kwargs)
-
-#class Composite(GenericProperty):
-# def __init__(self, prop):
-# super(GenericProperty, self).__init__()
-# self.prop = prop
-
-# def evaluate_property(self, prop):
-# return composite(prop.label(self.name))
-
-#start = Composite(Point, lambda c: (c.x1, c.y1))
-
-#mapper(Vertex, vertices, properties={
-# 'start':composite(Point, vertices.c.x1, vertices.c.y1),
-# 'end':composite(Point, vertices.c.x2, vertices.c.y2)
-#})
-
-
-has_property = PropertyStatement(GenericProperty)
-
diff --git a/libs/elixir/relationships.py b/libs/elixir/relationships.py
deleted file mode 100644
index 6c14dbb6..00000000
--- a/libs/elixir/relationships.py
+++ /dev/null
@@ -1,1247 +0,0 @@
-'''
-This module provides support for defining relationships between your Elixir
-entities. Elixir currently supports two syntaxes to do so: the default
-`Attribute-based syntax`_ which supports the following types of relationships:
-ManyToOne_, OneToMany_, OneToOne_ and ManyToMany_, as well as a
-`DSL-based syntax`_ which provides the following statements: belongs_to_,
-has_many_, has_one_ and has_and_belongs_to_many_.
-
-======================
-Attribute-based syntax
-======================
-
-The first argument to all these "normal" relationship classes is the name of
-the class (entity) you are relating to.
-
-Following that first mandatory argument, any number of additional keyword
-arguments can be specified for advanced behavior. See each relationship type
-for a list of their specific keyword arguments. At this point, we'll just note
-that all the arguments that are not specifically processed by Elixir, as
-mentioned in the documentation below are passed on to the SQLAlchemy
-``relation`` function. So, please refer to the `SQLAlchemy relation function's
-documentation `_ for further detail about which
-keyword arguments are supported.
-
-You should keep in mind that the following
-keyword arguments are automatically generated by Elixir and should not be used
-unless you want to override the value provided by Elixir: ``uselist``,
-``remote_side``, ``secondary``, ``primaryjoin`` and ``secondaryjoin``.
-
-Additionally, if you want a bidirectionnal relationship, you should define the
-inverse relationship on the other entity explicitly (as opposed to how
-SQLAlchemy's backrefs are defined). In non-ambiguous situations, Elixir will
-match relationships together automatically. If there are several relationships
-of the same type between two entities, Elixir is not able to determine which
-relationship is the inverse of which, so you have to disambiguate the
-situation by giving the name of the inverse relationship in the ``inverse``
-keyword argument.
-
-Here is a detailed explanation of each relation type:
-
-`ManyToOne`
------------
-
-Describes the child's side of a parent-child relationship. For example,
-a `Pet` object may belong to its owner, who is a `Person`. This could be
-expressed like so:
-
-.. sourcecode:: python
-
- class Pet(Entity):
- owner = ManyToOne('Person')
-
-Behind the scene, assuming the primary key of the `Person` entity is
-an integer column named `id`, the ``ManyToOne`` relationship will
-automatically add an integer column named `owner_id` to the entity, with a
-foreign key referencing the `id` column of the `Person` entity.
-
-In addition to the keyword arguments inherited from SQLAlchemy's relation
-function, ``ManyToOne`` relationships accept the following optional arguments
-which will be directed to the created column:
-
-+----------------------+------------------------------------------------------+
-| Option Name | Description |
-+======================+======================================================+
-| ``colname`` | Specify a custom name for the foreign key column(s). |
-| | This argument accepts either a single string or a |
-| | list of strings. The number of strings passed must |
-| | match the number of primary key columns of the target|
-| | entity. If this argument is not used, the name of the|
-| | column(s) is generated with the pattern |
-| | defined in options.FKCOL_NAMEFORMAT, which is, by |
-| | default: "%(relname)s_%(key)s", where relname is the |
-| | name of the ManyToOne relationship, and 'key' is the |
-| | name (key) of the primary column in the target |
-| | entity. That's with, in the above Pet/owner example, |
-| | the name of the column would be: "owner_id". |
-+----------------------+------------------------------------------------------+
-| ``required`` | Specify whether or not this field can be set to None |
-| | (left without a value). Defaults to ``False``, |
-| | unless the field is a primary key. |
-+----------------------+------------------------------------------------------+
-| ``primary_key`` | Specify whether or not the column(s) created by this |
-| | relationship should act as a primary_key. |
-| | Defaults to ``False``. |
-+----------------------+------------------------------------------------------+
-| ``column_kwargs`` | A dictionary holding any other keyword argument you |
-| | might want to pass to the Column. |
-+----------------------+------------------------------------------------------+
-| ``target_column`` | Name (or list of names) of the target column(s). |
-| | If this argument is not specified, the target entity |
-| | primary key column(s) are used. |
-+----------------------+------------------------------------------------------+
-
-The following optional arguments are also supported to customize the
-ForeignKeyConstraint that is created:
-
-+----------------------+------------------------------------------------------+
-| Option Name | Description |
-+======================+======================================================+
-| ``use_alter`` | If True, SQLAlchemy will add the constraint in a |
-| | second SQL statement (as opposed to within the |
-| | create table statement). This permits to define |
-| | tables with a circular foreign key dependency |
-| | between them. |
-+----------------------+------------------------------------------------------+
-| ``ondelete`` | Value for the foreign key constraint ondelete clause.|
-| | May be one of: ``cascade``, ``restrict``, |
-| | ``set null``, or ``set default``. |
-+----------------------+------------------------------------------------------+
-| ``onupdate`` | Value for the foreign key constraint onupdate clause.|
-| | May be one of: ``cascade``, ``restrict``, |
-| | ``set null``, or ``set default``. |
-+----------------------+------------------------------------------------------+
-| ``constraint_kwargs``| A dictionary holding any other keyword argument you |
-| | might want to pass to the Constraint. |
-+----------------------+------------------------------------------------------+
-
-In some cases, you may want to declare the foreign key column explicitly,
-instead of letting it be generated automatically. There are several reasons to
-that: it could be because you want to declare it with precise arguments and
-using column_kwargs makes your code ugly, or because the name of
-your column conflicts with the property name (in which case an error is
-thrown). In those cases, you can use the ``field`` argument to specify an
-already-declared field to be used for the foreign key column.
-
-For example, for the Pet example above, if you want the database column
-(holding the foreign key) to be called 'owner', one should use the field
-parameter to specify the field manually.
-
-.. sourcecode:: python
-
- class Pet(Entity):
- owner_id = Field(Integer, colname='owner')
- owner = ManyToOne('Person', field=owner_id)
-
-+----------------------+------------------------------------------------------+
-| Option Name | Description |
-+======================+======================================================+
-| ``field`` | Specify the previously-declared field to be used for |
-| | the foreign key column. Use of this parameter is |
-| | mutually exclusive with the colname and column_kwargs|
-| | arguments. |
-+----------------------+------------------------------------------------------+
-
-
-Additionally, Elixir supports the belongs_to_ statement as an alternative,
-DSL-based, syntax to define ManyToOne_ relationships.
-
-
-`OneToMany`
------------
-
-Describes the parent's side of a parent-child relationship when there can be
-several children. For example, a `Person` object has many children, each of
-them being a `Person`. This could be expressed like so:
-
-.. sourcecode:: python
-
- class Person(Entity):
- parent = ManyToOne('Person')
- children = OneToMany('Person')
-
-Note that a ``OneToMany`` relationship **cannot exist** without a
-corresponding ``ManyToOne`` relationship in the other way. This is because the
-``OneToMany`` relationship needs the foreign key created by the ``ManyToOne``
-relationship.
-
-In addition to keyword arguments inherited from SQLAlchemy, ``OneToMany``
-relationships accept the following optional (keyword) arguments:
-
-+--------------------+--------------------------------------------------------+
-| Option Name | Description |
-+====================+========================================================+
-| ``order_by`` | Specify which field(s) should be used to sort the |
-| | results given by accessing the relation field. |
-| | Note that this sort order is only applied when loading |
-| | objects from the database. Objects appended to the |
-| | collection afterwards are not re-sorted in-memory on |
-| | the fly. |
-| | This argument accepts either a string or a list of |
-| | strings, each corresponding to the name of a field in |
-| | the target entity. These field names can optionally be |
-| | prefixed by a minus (for descending order). |
-+--------------------+--------------------------------------------------------+
-| ``filter`` | Specify a filter criterion (as a clause element) for |
-| | this relationship. This criterion will be ``and_`` ed |
-| | with the normal join criterion (primaryjoin) generated |
-| | by Elixir for the relationship. For example: |
-| | boston_addresses = |
-| | OneToMany('Address', filter=Address.city == 'Boston') |
-+--------------------+--------------------------------------------------------+
-
-Additionally, Elixir supports an alternate, DSL-based, syntax to define
-OneToMany_ relationships, with the has_many_ statement.
-
-
-`OneToOne`
-----------
-
-Describes the parent's side of a parent-child relationship when there is only
-one child. For example, a `Car` object has one gear stick, which is
-represented as a `GearStick` object. This could be expressed like so:
-
-.. sourcecode:: python
-
- class Car(Entity):
- gear_stick = OneToOne('GearStick', inverse='car')
-
- class GearStick(Entity):
- car = ManyToOne('Car')
-
-Note that a ``OneToOne`` relationship **cannot exist** without a corresponding
-``ManyToOne`` relationship in the other way. This is because the ``OneToOne``
-relationship needs the foreign_key created by the ``ManyToOne`` relationship.
-
-Additionally, Elixir supports an alternate, DSL-based, syntax to define
-OneToOne_ relationships, with the has_one_ statement.
-
-
-`ManyToMany`
-------------
-
-Describes a relationship in which one kind of entity can be related to several
-objects of the other kind but the objects of that other kind can be related to
-several objects of the first kind. For example, an `Article` can have several
-tags, but the same `Tag` can be used on several articles.
-
-.. sourcecode:: python
-
- class Article(Entity):
- tags = ManyToMany('Tag')
-
- class Tag(Entity):
- articles = ManyToMany('Article')
-
-Behind the scene, the ``ManyToMany`` relationship will automatically create an
-intermediate table to host its data.
-
-Note that you don't necessarily need to define the inverse relationship. In
-our example, even though we want tags to be usable on several articles, we
-might not be interested in which articles correspond to a particular tag. In
-that case, we could have omitted the `Tag` side of the relationship.
-
-If your ``ManyToMany`` relationship is self-referencial, the entity
-containing it is autoloaded (and you don't intend to specify both the
-primaryjoin and secondaryjoin arguments manually), you must specify at least
-one of either the ``remote_colname`` or ``local_colname`` argument.
-
-In addition to keyword arguments inherited from SQLAlchemy, ``ManyToMany``
-relationships accept the following optional (keyword) arguments:
-
-+--------------------+--------------------------------------------------------+
-| Option Name | Description |
-+====================+========================================================+
-| ``tablename`` | Specify a custom name for the intermediary table. This |
-| | can be used both when the tables needs to be created |
-| | and when the table is autoloaded/reflected from the |
-| | database. If this argument is not used, a name will be |
-| | automatically generated by Elixir depending on the name|
-| | of the tables of the two entities of the relationship, |
-| | the name of the relationship, and, if present, the name|
-| | of its inverse. Even though this argument is optional, |
-| | it is wise to use it if you are not sure what are the |
-| | exact consequence of using a generated table name. |
-+--------------------+--------------------------------------------------------+
-| ``schema`` | Specify a custom schema for the intermediate table. |
-| | This can be used both when the tables needs to |
-| | be created and when the table is autoloaded/reflected |
-| | from the database. |
-+--------------------+--------------------------------------------------------+
-| ``remote_colname`` | A string or list of strings specifying the names of |
-| | the column(s) in the intermediary table which |
-| | reference the "remote"/target entity's table. |
-+--------------------+--------------------------------------------------------+
-| ``local_colname`` | A string or list of strings specifying the names of |
-| | the column(s) in the intermediary table which |
-| | reference the "local"/current entity's table. |
-+--------------------+--------------------------------------------------------+
-| ``table`` | Use a manually created table. If this argument is |
-| | used, Elixir will not generate a table for this |
-| | relationship, and use the one given instead. This |
-| | argument only accepts SQLAlchemy's Table objects. |
-+--------------------+--------------------------------------------------------+
-| ``order_by`` | Specify which field(s) should be used to sort the |
-| | results given by accessing the relation field. |
-| | Note that this sort order is only applied when loading |
-| | objects from the database. Objects appended to the |
-| | collection afterwards are not re-sorted in-memory on |
-| | the fly. |
-| | This argument accepts either a string or a list of |
-| | strings, each corresponding to the name of a field in |
-| | the target entity. These field names can optionally be |
-| | prefixed by a minus (for descending order). |
-+----------------------+------------------------------------------------------+
-| ``ondelete`` | Value for the foreign key constraint ondelete clause. |
-| | May be one of: ``cascade``, ``restrict``, |
-| | ``set null``, or ``set default``. |
-+--------------------+--------------------------------------------------------+
-| ``onupdate`` | Value for the foreign key constraint onupdate clause. |
-| | May be one of: ``cascade``, ``restrict``, |
-| | ``set null``, or ``set default``. |
-+--------------------+--------------------------------------------------------+
-| ``table_kwargs`` | A dictionary holding any other keyword argument you |
-| | might want to pass to the underlying Table object. |
-+--------------------+--------------------------------------------------------+
-
-
-================
-DSL-based syntax
-================
-
-The following DSL statements provide an alternative way to define relationships
-between your entities. The first argument to all those statements is the name
-of the relationship, the second is the 'kind' of object you are relating to
-(it is usually given using the ``of_kind`` keyword).
-
-`belongs_to`
-------------
-
-The ``belongs_to`` statement is the DSL syntax equivalent to the ManyToOne_
-relationship. As such, it supports all the same arguments as ManyToOne_
-relationships.
-
-.. sourcecode:: python
-
- class Pet(Entity):
- belongs_to('feeder', of_kind='Person')
- belongs_to('owner', of_kind='Person', colname="owner_id")
-
-
-`has_many`
-----------
-
-The ``has_many`` statement is the DSL syntax equivalent to the OneToMany_
-relationship. As such, it supports all the same arguments as OneToMany_
-relationships.
-
-.. sourcecode:: python
-
- class Person(Entity):
- belongs_to('parent', of_kind='Person')
- has_many('children', of_kind='Person')
-
-There is also an alternate form of the ``has_many`` relationship that takes
-only two keyword arguments: ``through`` and ``via`` in order to encourage a
-richer form of many-to-many relationship that is an alternative to the
-``has_and_belongs_to_many`` statement. Here is an example:
-
-.. sourcecode:: python
-
- class Person(Entity):
- has_field('name', Unicode)
- has_many('assignments', of_kind='Assignment')
- has_many('projects', through='assignments', via='project')
-
- class Assignment(Entity):
- has_field('start_date', DateTime)
- belongs_to('person', of_kind='Person')
- belongs_to('project', of_kind='Project')
-
- class Project(Entity):
- has_field('title', Unicode)
- has_many('assignments', of_kind='Assignment')
-
-In the above example, a `Person` has many `projects` through the `Assignment`
-relationship object, via a `project` attribute.
-
-
-`has_one`
----------
-
-The ``has_one`` statement is the DSL syntax equivalent to the OneToOne_
-relationship. As such, it supports all the same arguments as OneToOne_
-relationships.
-
-.. sourcecode:: python
-
- class Car(Entity):
- has_one('gear_stick', of_kind='GearStick', inverse='car')
-
- class GearStick(Entity):
- belongs_to('car', of_kind='Car')
-
-
-`has_and_belongs_to_many`
--------------------------
-
-The ``has_and_belongs_to_many`` statement is the DSL syntax equivalent to the
-ManyToMany_ relationship. As such, it supports all the same arguments as
-ManyToMany_ relationships.
-
-.. sourcecode:: python
-
- class Article(Entity):
- has_and_belongs_to_many('tags', of_kind='Tag')
-
- class Tag(Entity):
- has_and_belongs_to_many('articles', of_kind='Article')
-
-'''
-
-import warnings
-
-from sqlalchemy import ForeignKeyConstraint, Column, Table, and_
-from sqlalchemy.orm import relation, backref, class_mapper
-from sqlalchemy.ext.associationproxy import association_proxy
-
-import options
-from elixir.statements import ClassMutator
-from elixir.properties import Property
-from elixir.entity import EntityMeta, DEBUG
-
-__doc_all__ = []
-
-
-class Relationship(Property):
- '''
- Base class for relationships.
- '''
-
- def __init__(self, of_kind, inverse=None, *args, **kwargs):
- super(Relationship, self).__init__()
-
- self.of_kind = of_kind
- self.inverse_name = inverse
-
- self._target = None
-
- self.property = None # sqlalchemy property
- self.backref = None # sqlalchemy backref
-
- #TODO: unused for now
- self.args = args
- self.kwargs = kwargs
-
- def attach(self, entity, name):
- super(Relationship, self).attach(entity, name)
- entity._descriptor.relationships.append(self)
-
- def create_pk_cols(self):
- self.create_keys(True)
-
- def create_non_pk_cols(self):
- self.create_keys(False)
-
- def create_keys(self, pk):
- '''
- Subclasses (ie. concrete relationships) may override this method to
- create foreign keys.
- '''
-
- def create_properties(self):
- if self.property or self.backref:
- return
-
- kwargs = self.get_prop_kwargs()
- if 'order_by' in kwargs:
- kwargs['order_by'] = \
- self.target._descriptor.translate_order_by(kwargs['order_by'])
-
- # transform callable arguments
- for arg in ('primaryjoin', 'secondaryjoin', 'remote_side',
- 'foreign_keys'):
- kwarg = kwargs.get(arg, None)
- if hasattr(kwarg, '__call__'):
- kwargs[arg] = kwarg()
-
- # viewonly relationships need to create "standalone" relations (ie
- # shouldn't be a backref of another relation).
- if self.inverse and not kwargs.get('viewonly', False):
- # check if the inverse was already processed (and thus has already
- # defined a backref we can use)
- if self.inverse.backref:
- # let the user override the backref argument
- if 'backref' not in kwargs:
- kwargs['backref'] = self.inverse.backref
- else:
- # SQLAlchemy doesn't like when 'secondary' is both defined on
- # the relation and the backref
- kwargs.pop('secondary', None)
-
- # define backref for use by the inverse
- self.backref = backref(self.name, **kwargs)
- return
-
- self.property = relation(self.target, **kwargs)
- self.add_mapper_property(self.name, self.property)
-
- @property
- def target(self):
- if not self._target:
- if isinstance(self.of_kind, basestring):
- collection = self.entity._descriptor.collection
- self._target = collection.resolve(self.of_kind, self.entity)
- else:
- self._target = self.of_kind
- return self._target
-
- @property
- def inverse(self):
- if not hasattr(self, '_inverse'):
- if self.inverse_name:
- desc = self.target._descriptor
- inverse = desc.find_relationship(self.inverse_name)
- if inverse is None:
- raise Exception(
- "Couldn't find a relationship named '%s' in "
- "entity '%s' or its parent entities."
- % (self.inverse_name, self.target.__name__))
- assert self.match_type_of(inverse), \
- "Relationships '%s' in entity '%s' and '%s' in entity " \
- "'%s' cannot be inverse of each other because their " \
- "types do not form a valid combination." % \
- (self.name, self.entity.__name__,
- self.inverse_name, self.target.__name__)
- else:
- check_reverse = not self.kwargs.get('viewonly', False)
- if isinstance(self.target, EntityMeta):
- inverse = self.target._descriptor.get_inverse_relation(
- self, check_reverse=check_reverse)
- else:
- inverse = None
- self._inverse = inverse
- if inverse and not self.kwargs.get('viewonly', False):
- inverse._inverse = self
-
- return self._inverse
-
- def match_type_of(self, other):
- return False
-
- def is_inverse(self, other):
- # viewonly relationships are not symmetrical: a viewonly relationship
- # should have exactly one inverse (a ManyToOne relationship), but that
- # inverse shouldn't have the viewonly relationship as its inverse.
- return not other.kwargs.get('viewonly', False) and \
- other is not self and \
- self.match_type_of(other) and \
- self.entity == other.target and \
- other.entity == self.target and \
- (self.inverse_name == other.name or not self.inverse_name) and \
- (other.inverse_name == self.name or not other.inverse_name)
-
-
-class ManyToOne(Relationship):
- '''
-
- '''
-
- def __init__(self, of_kind,
- column_kwargs=None,
- colname=None, required=None, primary_key=None,
- field=None,
- constraint_kwargs=None,
- use_alter=None, ondelete=None, onupdate=None,
- target_column=None,
- *args, **kwargs):
-
- # 1) handle column-related args
-
- # check that the column arguments don't conflict
- assert not (field and (column_kwargs or colname)), \
- "ManyToOne can accept the 'field' argument or column " \
- "arguments ('colname' or 'column_kwargs') but not both!"
-
- if colname and not isinstance(colname, list):
- colname = [colname]
- self.colname = colname or []
-
- column_kwargs = column_kwargs or {}
- # kwargs go by default to the relation(), so we need to manually
- # extract those targeting the Column
- if required is not None:
- column_kwargs['nullable'] = not required
- if primary_key is not None:
- column_kwargs['primary_key'] = primary_key
- # by default, created columns will have an index.
- column_kwargs.setdefault('index', True)
- self.column_kwargs = column_kwargs
-
- if field and not isinstance(field, list):
- field = [field]
- self.field = field or []
-
- # 2) handle constraint kwargs
- constraint_kwargs = constraint_kwargs or {}
- if use_alter is not None:
- constraint_kwargs['use_alter'] = use_alter
- if ondelete is not None:
- constraint_kwargs['ondelete'] = ondelete
- if onupdate is not None:
- constraint_kwargs['onupdate'] = onupdate
- self.constraint_kwargs = constraint_kwargs
-
- # 3) misc arguments
- if target_column and not isinstance(target_column, list):
- target_column = [target_column]
- self.target_column = target_column
-
- self.foreign_key = []
- self.primaryjoin_clauses = []
-
- super(ManyToOne, self).__init__(of_kind, *args, **kwargs)
-
- def match_type_of(self, other):
- return isinstance(other, (OneToMany, OneToOne))
-
- @property
- def target_table(self):
- if isinstance(self.target, EntityMeta):
- return self.target._descriptor.table
- else:
- return class_mapper(self.target).local_table
-
- def create_keys(self, pk):
- '''
- Find all primary keys on the target and create foreign keys on the
- source accordingly.
- '''
-
- if self.foreign_key:
- return
-
- if self.column_kwargs.get('primary_key', False) != pk:
- return
-
- source_desc = self.entity._descriptor
- if isinstance(self.target, EntityMeta):
- # make sure the target has all its pk set up
- #FIXME: this is not enough when specifying target_column manually,
- # on unique, non-pk col, see tests/test_m2o.py:test_non_pk_forward
- self.target._descriptor.create_pk_cols()
-
- #XXX: another option, instead of the FakeTable, would be to create an
- # EntityDescriptor for the SA class.
- target_table = self.target_table
-
- if source_desc.autoload:
- #TODO: allow target_column to be used as an alternative to
- # specifying primaryjoin, to be consistent with non-autoloaded
- # tables
- if self.colname:
- if 'primaryjoin' not in self.kwargs:
- self.primaryjoin_clauses = \
- _get_join_clauses(self.entity.table,
- self.colname, None,
- target_table)[0]
- if not self.primaryjoin_clauses:
- colnames = ', '.join(self.colname)
- raise Exception(
- "Couldn't find a foreign key constraint in table "
- "'%s' using the following columns: %s."
- % (self.entity.table.name, colnames))
- else:
- # in this case we let SA handle everything.
- # XXX: we might want to try to build join clauses anyway so
- # that we know whether there is an ambiguity or not, and
- # suggest using colname if there is one
- pass
- if self.field:
- raise NotImplementedError(
- "'field' argument not allowed on autoloaded table "
- "relationships.")
- else:
- fk_refcols = []
- fk_colnames = []
-
- if self.target_column is None:
- target_columns = target_table.primary_key.columns
- else:
- target_columns = [target_table.columns[col]
- for col in self.target_column]
-
- if not target_columns:
- raise Exception("No primary key found in target table ('%s') "
- "for the '%s' relationship of the '%s' entity."
- % (target_table.name, self.name,
- self.entity.__name__))
- if self.colname and \
- len(self.colname) != len(target_columns):
- raise Exception(
- "The number of column names provided in the colname "
- "keyword argument of the '%s' relationship of the "
- "'%s' entity is not the same as the number of columns "
- "of the primary key of '%s'."
- % (self.name, self.entity.__name__,
- self.target.__name__))
-
- for key_num, target_col in enumerate(target_columns):
- if self.field:
- col = self.field[key_num].column
- else:
- if self.colname:
- colname = self.colname[key_num]
- else:
- colname = options.FKCOL_NAMEFORMAT % \
- {'relname': self.name,
- 'key': target_col.key}
-
- # We can't add the column to the table directly as the
- # table might not be created yet.
- col = Column(colname, target_col.type,
- **self.column_kwargs)
- source_desc.add_column(col)
-
- # If the column name was specified, and it is the same as
- # this property's name, there is going to be a conflict.
- # Don't allow this to happen.
- if col.key == self.name:
- raise ValueError(
- "ManyToOne named '%s' in '%s' conficts "
- " with the column of the same name. "
- "You should probably define the foreign key "
- "field manually and use the 'field' "
- "argument on the ManyToOne relationship"
- % (self.name, self.entity.__name__))
-
- # Build the list of local columns which will be part of
- # the foreign key
- self.foreign_key.append(col)
-
- # Store the names of those columns
- fk_colnames.append(col.key)
-
- # Build the list of column "paths" the foreign key will
- # point to
- fk_refcols.append("%s.%s" % \
- (target_table.fullname, target_col.key))
-
- # Build up the primary join. This is needed when you have
- # several ManyToOne relationships between two objects
- self.primaryjoin_clauses.append(col == target_col)
-
- if 'name' not in self.constraint_kwargs:
- # In some databases (at least MySQL) the constraint name needs
- # to be unique for the whole database, instead of per table.
- fk_name = options.CONSTRAINT_NAMEFORMAT % \
- {'tablename': source_desc.tablename,
- 'colnames': '_'.join(fk_colnames)}
- self.constraint_kwargs['name'] = fk_name
-
- source_desc.add_constraint(
- ForeignKeyConstraint(fk_colnames, fk_refcols,
- **self.constraint_kwargs))
-
- def get_prop_kwargs(self):
- kwargs = {'uselist': False}
-
- if self.entity.table is self.target_table:
- # this is needed because otherwise SA has no way to know what is
- # the direction of the relationship since both columns present in
- # the primaryjoin belong to the same table. In other words, it is
- # necessary to know if this particular relation
- # is the many-to-one side, or the one-to-xxx side. The foreignkey
- # doesn't help in this case.
- kwargs['remote_side'] = \
- [col for col in self.target_table.primary_key.columns]
-
- if self.primaryjoin_clauses:
- kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
-
- kwargs.update(self.kwargs)
-
- return kwargs
-
-
-class OneToOne(Relationship):
- uselist = False
-
- def __init__(self, of_kind, filter=None, *args, **kwargs):
- self.filter = filter
- if filter is not None:
- # We set viewonly to True by default for filtered relationships,
- # unless manually overridden.
- # This is not strictly necessary, as SQLAlchemy allows non viewonly
- # relationships with a custom join/filter. The example at:
- # SADOCS/05/mappers.html#advdatamapping_relation_customjoin
- # is not viewonly. Those relationships can be used as if the extra
- # filter wasn't present when inserting. This can lead to a
- # confusing behavior (if you insert data which doesn't match the
- # extra criterion it'll get inserted anyway but you won't see it
- # when you query back the attribute after a round-trip to the
- # database).
- if 'viewonly' not in kwargs:
- kwargs['viewonly'] = True
- super(OneToOne, self).__init__(of_kind, *args, **kwargs)
-
- def match_type_of(self, other):
- return isinstance(other, ManyToOne)
-
- def create_keys(self, pk):
- # make sure an inverse relationship exists
- if self.inverse is None:
- raise Exception(
- "Couldn't find any relationship in '%s' which "
- "match as inverse of the '%s' relationship "
- "defined in the '%s' entity. If you are using "
- "inheritance you "
- "might need to specify inverse relationships "
- "manually by using the 'inverse' argument."
- % (self.target, self.name,
- self.entity))
-
- def get_prop_kwargs(self):
- kwargs = {'uselist': self.uselist}
-
- #TODO: for now, we don't break any test if we remove those 2 lines.
- # So, we should either complete the selfref test to prove that they
- # are indeed useful, or remove them. It might be they are indeed
- # useless because the remote_side is already setup in the other way
- # (ManyToOne).
- if self.entity.table is self.target.table:
- # When using a manual/autoloaded table, it will be assigned
- # an empty list, which doesn't seem to upset SQLAlchemy
- kwargs['remote_side'] = self.inverse.foreign_key
-
- # Contrary to ManyToMany relationships, we need to specify the join
- # clauses even if this relationship is not self-referencial because
- # there could be several ManyToOne from the target class to us.
- joinclauses = self.inverse.primaryjoin_clauses
- if self.filter:
- # We need to make a copy of the joinclauses, to not add the filter
- # on the backref
- joinclauses = joinclauses[:] + [self.filter(self.target.table.c)]
- if joinclauses:
- kwargs['primaryjoin'] = and_(*joinclauses)
-
- kwargs.update(self.kwargs)
-
- return kwargs
-
-
-class OneToMany(OneToOne):
- uselist = True
-
-
-class ManyToMany(Relationship):
- uselist = True
-
- def __init__(self, of_kind, tablename=None,
- local_colname=None, remote_colname=None,
- ondelete=None, onupdate=None,
- table=None, schema=None,
- filter=None,
- table_kwargs=None,
- *args, **kwargs):
- self.user_tablename = tablename
-
- if local_colname and not isinstance(local_colname, list):
- local_colname = [local_colname]
- self.local_colname = local_colname or []
- if remote_colname and not isinstance(remote_colname, list):
- remote_colname = [remote_colname]
- self.remote_colname = remote_colname or []
-
- self.ondelete = ondelete
- self.onupdate = onupdate
-
- self.table = table
- self.schema = schema
-
- #TODO: this can probably be simplified/moved elsewhere since the
- #argument disappeared
- self.column_format = options.M2MCOL_NAMEFORMAT
- if not hasattr(self.column_format, '__call__'):
- # we need to store the format in a variable so that the
- # closure of the lambda is correct
- format = self.column_format
- self.column_format = lambda data: format % data
- if options.MIGRATION_TO_07_AID:
- self.column_format = \
- migration_aid_m2m_column_formatter(
- lambda data: options.OLD_M2MCOL_NAMEFORMAT % data,
- self.column_format)
-
- self.filter = filter
- if filter is not None:
- # We set viewonly to True by default for filtered relationships,
- # unless manually overridden.
- if 'viewonly' not in kwargs:
- kwargs['viewonly'] = True
-
- self.table_kwargs = table_kwargs or {}
-
- self.primaryjoin_clauses = []
- self.secondaryjoin_clauses = []
-
- super(ManyToMany, self).__init__(of_kind, *args, **kwargs)
-
- def match_type_of(self, other):
- return isinstance(other, ManyToMany)
-
- def create_tables(self):
- if self.table is not None:
- if 'primaryjoin' not in self.kwargs or \
- 'secondaryjoin' not in self.kwargs:
- self._build_join_clauses()
- assert self.inverse is None or self.inverse.table is None or \
- self.inverse.table is self.table
- return
-
- if self.inverse:
- inverse = self.inverse
- if inverse.table is not None:
- self.table = inverse.table
- self.primaryjoin_clauses = inverse.secondaryjoin_clauses
- self.secondaryjoin_clauses = inverse.primaryjoin_clauses
- return
-
- assert not inverse.user_tablename or not self.user_tablename or \
- inverse.user_tablename == self.user_tablename
- assert not inverse.remote_colname or not self.local_colname or \
- inverse.remote_colname == self.local_colname
- assert not inverse.local_colname or not self.remote_colname or \
- inverse.local_colname == self.remote_colname
- assert not inverse.schema or not self.schema or \
- inverse.schema == self.schema
- assert not inverse.table_kwargs or not self.table_kwargs or \
- inverse.table_kwargs == self.table_kwargs
-
- self.user_tablename = inverse.user_tablename or self.user_tablename
- self.local_colname = inverse.remote_colname or self.local_colname
- self.remote_colname = inverse.local_colname or self.remote_colname
- self.schema = inverse.schema or self.schema
- self.local_colname = inverse.remote_colname or self.local_colname
-
- # compute table_kwargs
- complete_kwargs = options.options_defaults['table_options'].copy()
- complete_kwargs.update(self.table_kwargs)
-
- #needs: table_options['schema'], autoload, tablename, primary_keys,
- #entity.__name__, table_fullname
- e1_desc = self.entity._descriptor
- e2_desc = self.target._descriptor
-
- e1_schema = e1_desc.table_options.get('schema', None)
- e2_schema = e2_desc.table_options.get('schema', None)
- schema = (self.schema is not None) and self.schema or e1_schema
-
- assert e1_schema == e2_schema or self.schema, \
- "Schema %r for entity %s differs from schema %r of entity %s." \
- " Consider using the schema-parameter. "\
- % (e1_schema, self.entity.__name__,
- e2_schema, self.target.__name__)
-
- # First, we compute the name of the table. Note that some of the
- # intermediary variables are reused later for the constraint
- # names.
-
- # We use the name of the relation for the first entity
- # (instead of the name of its primary key), so that we can
- # have two many-to-many relations between the same objects
- # without having a table name collision.
- source_part = "%s_%s" % (e1_desc.tablename, self.name)
-
- # And we use only the name of the table of the second entity
- # when there is no inverse, so that a many-to-many relation
- # can be defined without an inverse.
- if self.inverse:
- target_part = "%s_%s" % (e2_desc.tablename, self.inverse.name)
- else:
- target_part = e2_desc.tablename
-
- if self.user_tablename:
- tablename = self.user_tablename
- else:
- # We need to keep the table name consistent (independant of
- # whether this relation or its inverse is setup first).
- if self.inverse and source_part < target_part:
- #XXX: use a different scheme for selfref (to not include the
- # table name twice)?
- tablename = "%s__%s" % (target_part, source_part)
- else:
- tablename = "%s__%s" % (source_part, target_part)
-
- if options.MIGRATION_TO_07_AID:
- oldname = (self.inverse and
- e1_desc.tablename < e2_desc.tablename) and \
- "%s__%s" % (target_part, source_part) or \
- "%s__%s" % (source_part, target_part)
- if oldname != tablename:
- warnings.warn(
- "The generated table name for the '%s' relationship "
- "on the '%s' entity changed from '%s' (the name "
- "generated by Elixir 0.6.1 and earlier) to '%s'. "
- "You should either rename the table in the database "
- "to the new name or use the tablename argument on the "
- "relationship to force the old name: tablename='%s'!"
- % (self.name, self.entity.__name__, oldname,
- tablename, oldname))
-
- if e1_desc.autoload:
- if not e2_desc.autoload:
- raise Exception(
- "Entity '%s' is autoloaded and its '%s' "
- "ManyToMany relationship points to "
- "the '%s' entity which is not autoloaded"
- % (self.entity.__name__, self.name,
- self.target.__name__))
-
- self.table = Table(tablename, e1_desc.metadata, autoload=True,
- **complete_kwargs)
- if 'primaryjoin' not in self.kwargs or \
- 'secondaryjoin' not in self.kwargs:
- self._build_join_clauses()
- else:
- # We pre-compute the names of the foreign key constraints
- # pointing to the source (local) entity's table and to the
- # target's table
-
- # In some databases (at least MySQL) the constraint names need
- # to be unique for the whole database, instead of per table.
- source_fk_name = "%s_fk" % source_part
- if self.inverse:
- target_fk_name = "%s_fk" % target_part
- else:
- target_fk_name = "%s_inverse_fk" % source_part
-
- columns = []
- constraints = []
-
- for num, desc, fk_name, rel, inverse, colnames, join_clauses in (
- (0, e1_desc, source_fk_name, self, self.inverse,
- self.local_colname, self.primaryjoin_clauses),
- (1, e2_desc, target_fk_name, self.inverse, self,
- self.remote_colname, self.secondaryjoin_clauses)):
-
- fk_colnames = []
- fk_refcols = []
- if colnames:
- assert len(colnames) == len(desc.primary_keys)
- else:
- # The data generated here will be fed to the M2M column
- # formatter to generate the name of the columns of the
- # intermediate table for *one* side of the relationship,
- # that is, from the intermediate table to the current
- # entity, as stored in the "desc" variable.
- data = {# A) relationships info
-
- # the name of the rel going *from* the entity
- # we are currently generating a column pointing
- # *to*. This is generally *not* what you want to
- # use. eg in a "Post" and "Tag" example, with
- # relationships named 'tags' and 'posts', when
- # creating the columns from the intermediate
- # table to the "Post" entity, 'relname' will
- # contain 'tags'.
- 'relname': rel and rel.name or 'inverse',
-
- # the name of the inverse relationship. In the
- # above example, 'inversename' will contain
- # 'posts'.
- 'inversename': inverse and inverse.name
- or 'inverse',
- # is A == B?
- 'selfref': e1_desc is e2_desc,
- # provided for backward compatibility, DO NOT USE!
- 'num': num,
- # provided for backward compatibility, DO NOT USE!
- 'numifself': e1_desc is e2_desc and str(num + 1)
- or '',
- # B) target information (from the perspective of
- # the intermediate table)
- 'target': desc.entity,
- 'entity': desc.entity.__name__.lower(),
- 'tablename': desc.tablename,
-
- # C) current (intermediate) table name
- 'current_table': tablename
- }
- colnames = []
- for pk_col in desc.primary_keys:
- data.update(key=pk_col.key)
- colnames.append(self.column_format(data))
-
- for pk_col, colname in zip(desc.primary_keys, colnames):
- col = Column(colname, pk_col.type, primary_key=True)
- columns.append(col)
-
- # Build the list of local columns which will be part
- # of the foreign key.
- fk_colnames.append(colname)
-
- # Build the list of column "paths" the foreign key will
- # point to
- target_path = "%s.%s" % (desc.table_fullname, pk_col.key)
- fk_refcols.append(target_path)
-
- # Build join clauses (in case we have a self-ref)
- if self.entity is self.target:
- join_clauses.append(col == pk_col)
-
- onupdate = rel and rel.onupdate
- ondelete = rel and rel.ondelete
-
- #FIXME: fk_name is misleading
- constraints.append(
- ForeignKeyConstraint(fk_colnames, fk_refcols,
- name=fk_name, onupdate=onupdate,
- ondelete=ondelete))
-
- args = columns + constraints
-
- self.table = Table(tablename, e1_desc.metadata,
- schema=schema, *args, **complete_kwargs)
- if DEBUG:
- print self.table.repr2()
-
- def _build_join_clauses(self):
- # In the case we have a self-reference, we need to build join clauses
- if self.entity is self.target:
- if not self.local_colname and not self.remote_colname:
- raise Exception(
- "Self-referential ManyToMany "
- "relationships in autoloaded entities need to have at "
- "least one of either 'local_colname' or 'remote_colname' "
- "argument specified. The '%s' relationship in the '%s' "
- "entity doesn't have either."
- % (self.name, self.entity.__name__))
-
- self.primaryjoin_clauses, self.secondaryjoin_clauses = \
- _get_join_clauses(self.table,
- self.local_colname, self.remote_colname,
- self.entity.table)
-
- def get_prop_kwargs(self):
- kwargs = {'secondary': self.table,
- 'uselist': self.uselist}
-
- if self.filter:
- # we need to make a copy of the joinclauses
- secondaryjoin_clauses = self.secondaryjoin_clauses[:] + \
- [self.filter(self.target.table.c)]
- else:
- secondaryjoin_clauses = self.secondaryjoin_clauses
-
- if self.target is self.entity or self.filter:
- kwargs['primaryjoin'] = and_(*self.primaryjoin_clauses)
- kwargs['secondaryjoin'] = and_(*secondaryjoin_clauses)
-
- kwargs.update(self.kwargs)
-
- return kwargs
-
- def is_inverse(self, other):
- return super(ManyToMany, self).is_inverse(other) and \
- (self.user_tablename == other.user_tablename or
- (not self.user_tablename and not other.user_tablename))
-
-
-def migration_aid_m2m_column_formatter(oldformatter, newformatter):
- def debug_formatter(data):
- old_name = oldformatter(data)
- new_name = newformatter(data)
- if new_name != old_name:
- complete_data = data.copy()
- complete_data.update(old_name=old_name,
- new_name=new_name,
- targetname=data['target'].__name__)
- # Specifying a stacklevel is useless in this case as the name
- # generation is triggered by setup_all(), not by the declaration
- # of the offending relationship.
- warnings.warn("The '%(old_name)s' column in the "
- "'%(current_table)s' table, used as the "
- "intermediate table for the '%(relname)s' "
- "relationship on the '%(targetname)s' entity "
- "was renamed to '%(new_name)s'."
- % complete_data)
- return new_name
- return debug_formatter
-
-
-def _get_join_clauses(local_table, local_cols1, local_cols2, target_table):
- primary_join, secondary_join = [], []
- cols1 = local_cols1[:]
- cols1.sort()
- cols1 = tuple(cols1)
-
- if local_cols2 is not None:
- cols2 = local_cols2[:]
- cols2.sort()
- cols2 = tuple(cols2)
- else:
- cols2 = None
-
- # Build a map of fk constraints pointing to the correct table.
- # The map is indexed on the local col names.
- constraint_map = {}
- for constraint in local_table.constraints:
- if isinstance(constraint, ForeignKeyConstraint):
- use_constraint = True
- fk_colnames = []
-
- # if all columns point to the correct table, we use the constraint
- #TODO: check that it contains as many columns as the pk of the
- #target entity, or even that it points to the actual pk columns
- for fk in constraint.elements:
- if fk.references(target_table):
- # local column key
- fk_colnames.append(fk.parent.key)
- else:
- use_constraint = False
- if use_constraint:
- fk_colnames.sort()
- constraint_map[tuple(fk_colnames)] = constraint
-
- # Either the fk column names match explicitely with the columns given for
- # one of the joins (primary or secondary), or we assume the current
- # columns match because the columns for this join were not given and we
- # know the other join is either not used (is None) or has an explicit
- # match.
-
-#TODO: rewrite this. Even with the comment, I don't even understand it myself.
- for cols, constraint in constraint_map.iteritems():
- if cols == cols1 or (cols != cols2 and
- not cols1 and (cols2 in constraint_map or
- cols2 is None)):
- join = primary_join
- elif cols == cols2 or (cols2 == () and cols1 in constraint_map):
- join = secondary_join
- else:
- continue
- for fk in constraint.elements:
- join.append(fk.parent == fk.column)
- return primary_join, secondary_join
-
-
-def rel_mutator_handler(target):
- def handler(entity, name, of_kind=None, through=None, via=None,
- *args, **kwargs):
- if through and via:
- setattr(entity, name,
- association_proxy(through, via, **kwargs))
- return
- elif through or via:
- raise Exception("'through' and 'via' relationship keyword "
- "arguments should be used in combination.")
- rel = target(of_kind, *args, **kwargs)
- rel.attach(entity, name)
- return handler
-
-
-belongs_to = ClassMutator(rel_mutator_handler(ManyToOne))
-has_one = ClassMutator(rel_mutator_handler(OneToOne))
-has_many = ClassMutator(rel_mutator_handler(OneToMany))
-has_and_belongs_to_many = ClassMutator(rel_mutator_handler(ManyToMany))
diff --git a/libs/elixir/statements.py b/libs/elixir/statements.py
deleted file mode 100644
index c21bf305..00000000
--- a/libs/elixir/statements.py
+++ /dev/null
@@ -1,59 +0,0 @@
-import sys
-
-MUTATORS = '__elixir_mutators__'
-
-class ClassMutator(object):
- '''
- DSL-style syntax
-
- A ``ClassMutator`` object represents a DSL term.
- '''
-
- def __init__(self, handler):
- '''
- Create a new ClassMutator, using the `handler` callable to process it
- when the time will come.
- '''
- self.handler = handler
-
- # called when a mutator (eg. "has_field(...)") is parsed
- def __call__(self, *args, **kwargs):
- # self in this case is the "generic" mutator (eg "has_field")
-
- # jam this mutator into the class's mutator list
- class_locals = sys._getframe(1).f_locals
- mutators = class_locals.setdefault(MUTATORS, [])
- mutators.append((self, args, kwargs))
-
- def process(self, entity, *args, **kwargs):
- '''
- Process one mutator. This version simply calls the handler callable,
- but another mutator (sub)class could do more processing.
- '''
- self.handler(entity, *args, **kwargs)
-
-
-#TODO: move this to the super class (to be created here) of EntityMeta
-def process_mutators(entity):
- '''
- Apply all mutators of the given entity. That is, loop over all mutators
- in the class's mutator list and process them.
- '''
- # we don't use getattr here to not inherit from the parent mutators
- # inadvertantly if the current entity hasn't defined any mutator.
- mutators = entity.__dict__.get(MUTATORS, [])
- for mutator, args, kwargs in mutators:
- mutator.process(entity, *args, **kwargs)
-
-class Statement(ClassMutator):
-
- def process(self, entity, *args, **kwargs):
- builder = self.handler(entity, *args, **kwargs)
- entity._descriptor.builders.append(builder)
-
-class PropertyStatement(ClassMutator):
-
- def process(self, entity, name, *args, **kwargs):
- prop = self.handler(*args, **kwargs)
- prop.attach(entity, name)
-
diff --git a/libs/sqlalchemy/__init__.py b/libs/sqlalchemy/__init__.py
index 9a21a70f..f6667f7d 100644
--- a/libs/sqlalchemy/__init__.py
+++ b/libs/sqlalchemy/__init__.py
@@ -1,15 +1,11 @@
# sqlalchemy/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-import inspect
-import sys
-import sqlalchemy.exc as exceptions
-
-from sqlalchemy.sql import (
+from .sql import (
alias,
and_,
asc,
@@ -25,6 +21,7 @@ from sqlalchemy.sql import (
except_all,
exists,
extract,
+ false,
func,
insert,
intersect,
@@ -42,6 +39,7 @@ from sqlalchemy.sql import (
select,
subquery,
text,
+ true,
tuple_,
type_coerce,
union,
@@ -49,7 +47,7 @@ from sqlalchemy.sql import (
update,
)
-from sqlalchemy.types import (
+from .types import (
BIGINT,
BINARY,
BLOB,
@@ -94,12 +92,11 @@ from sqlalchemy.types import (
)
-from sqlalchemy.schema import (
+from .schema import (
CheckConstraint,
Column,
ColumnDefault,
Constraint,
- DDL,
DefaultClause,
FetchedValue,
ForeignKey,
@@ -112,17 +109,25 @@ from sqlalchemy.schema import (
Table,
ThreadLocalMetaData,
UniqueConstraint,
- )
-
-from sqlalchemy.engine import create_engine, engine_from_config
+ DDL,
+)
-__all__ = sorted(name for name, obj in locals().items()
- if not (name.startswith('_') or inspect.ismodule(obj)))
+from .inspection import inspect
+from .engine import create_engine, engine_from_config
-__version__ = '0.7.10'
+__version__ = '0.9.1'
-del inspect, sys
+def __go(lcls):
+ global __all__
-from sqlalchemy import util as _sa_util
-_sa_util.importlater.resolve_all()
+ from . import events
+ from . import util as _sa_util
+
+ import inspect as _inspect
+
+ __all__ = sorted(name for name, obj in lcls.items()
+ if not (name.startswith('_') or _inspect.ismodule(obj)))
+
+ _sa_util.dependencies.resolve_all("sqlalchemy")
+__go(locals())
\ No newline at end of file
diff --git a/libs/sqlalchemy/cextension/processors.c b/libs/sqlalchemy/cextension/processors.c
index 427db5d8..003c2e32 100644
--- a/libs/sqlalchemy/cextension/processors.c
+++ b/libs/sqlalchemy/cextension/processors.c
@@ -1,6 +1,7 @@
/*
processors.c
-Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com
+Copyright (C) 2010-2014 the SQLAlchemy authors and contributors
+Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com
This module is part of SQLAlchemy and is released under
the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -9,13 +10,15 @@ the MIT License: http://www.opensource.org/licenses/mit-license.php
#include
#include
+#define MODULE_NAME "cprocessors"
+#define MODULE_DOC "Module containing C versions of data processing functions."
+
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
#define PY_SSIZE_T_MIN INT_MIN
#endif
-
static PyObject *
int_to_boolean(PyObject *self, PyObject *arg)
{
@@ -25,7 +28,12 @@ int_to_boolean(PyObject *self, PyObject *arg)
if (arg == Py_None)
Py_RETURN_NONE;
+
+#if PY_MAJOR_VERSION >= 3
+ l = PyLong_AsLong(arg);
+#else
l = PyInt_AsLong(arg);
+#endif
if (l == 0) {
res = Py_False;
} else if (l == 1) {
@@ -64,23 +72,48 @@ to_float(PyObject *self, PyObject *arg)
static PyObject *
str_to_datetime(PyObject *self, PyObject *arg)
{
+#if PY_MAJOR_VERSION >= 3
+ PyObject *bytes;
+ PyObject *err_bytes;
+#endif
const char *str;
+ int numparsed;
unsigned int year, month, day, hour, minute, second, microsecond = 0;
PyObject *err_repr;
if (arg == Py_None)
Py_RETURN_NONE;
+#if PY_MAJOR_VERSION >= 3
+ bytes = PyUnicode_AsASCIIString(arg);
+ if (bytes == NULL)
+ str = NULL;
+ else
+ str = PyBytes_AS_STRING(bytes);
+#else
str = PyString_AsString(arg);
+#endif
if (str == NULL) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse datetime string '%.200s' "
+ "- value is not a string.",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse datetime string '%.200s' "
"- value is not a string.",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
@@ -91,15 +124,30 @@ str_to_datetime(PyObject *self, PyObject *arg)
not accept "2000-01-01 00:00:00.". I don't know which is better, but they
should be coherent.
*/
- if (sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day,
- &hour, &minute, &second, µsecond) < 6) {
+ numparsed = sscanf(str, "%4u-%2u-%2u %2u:%2u:%2u.%6u", &year, &month, &day,
+ &hour, &minute, &second, µsecond);
+#if PY_MAJOR_VERSION >= 3
+ Py_DECREF(bytes);
+#endif
+ if (numparsed < 6) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse datetime string: %.200s",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse datetime string: %.200s",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
@@ -110,22 +158,47 @@ str_to_datetime(PyObject *self, PyObject *arg)
static PyObject *
str_to_time(PyObject *self, PyObject *arg)
{
+#if PY_MAJOR_VERSION >= 3
+ PyObject *bytes;
+ PyObject *err_bytes;
+#endif
const char *str;
+ int numparsed;
unsigned int hour, minute, second, microsecond = 0;
PyObject *err_repr;
if (arg == Py_None)
Py_RETURN_NONE;
+#if PY_MAJOR_VERSION >= 3
+ bytes = PyUnicode_AsASCIIString(arg);
+ if (bytes == NULL)
+ str = NULL;
+ else
+ str = PyBytes_AS_STRING(bytes);
+#else
str = PyString_AsString(arg);
+#endif
if (str == NULL) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse time string '%.200s' - value is not a string.",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse time string '%.200s' - value is not a string.",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
@@ -136,15 +209,30 @@ str_to_time(PyObject *self, PyObject *arg)
not accept "00:00:00.". I don't know which is better, but they should be
coherent.
*/
- if (sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second,
- µsecond) < 3) {
+ numparsed = sscanf(str, "%2u:%2u:%2u.%6u", &hour, &minute, &second,
+ µsecond);
+#if PY_MAJOR_VERSION >= 3
+ Py_DECREF(bytes);
+#endif
+ if (numparsed < 3) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse time string: %.200s",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse time string: %.200s",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
@@ -154,34 +242,73 @@ str_to_time(PyObject *self, PyObject *arg)
static PyObject *
str_to_date(PyObject *self, PyObject *arg)
{
+#if PY_MAJOR_VERSION >= 3
+ PyObject *bytes;
+ PyObject *err_bytes;
+#endif
const char *str;
+ int numparsed;
unsigned int year, month, day;
PyObject *err_repr;
if (arg == Py_None)
Py_RETURN_NONE;
+#if PY_MAJOR_VERSION >= 3
+ bytes = PyUnicode_AsASCIIString(arg);
+ if (bytes == NULL)
+ str = NULL;
+ else
+ str = PyBytes_AS_STRING(bytes);
+#else
str = PyString_AsString(arg);
+#endif
if (str == NULL) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse date string '%.200s' - value is not a string.",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse date string '%.200s' - value is not a string.",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
- if (sscanf(str, "%4u-%2u-%2u", &year, &month, &day) != 3) {
+ numparsed = sscanf(str, "%4u-%2u-%2u", &year, &month, &day);
+#if PY_MAJOR_VERSION >= 3
+ Py_DECREF(bytes);
+#endif
+ if (numparsed != 3) {
err_repr = PyObject_Repr(arg);
if (err_repr == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(err_repr);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_ValueError,
+ "Couldn't parse date string: %.200s",
+ PyBytes_AS_STRING(err_bytes));
+ Py_DECREF(err_bytes);
+#else
PyErr_Format(
PyExc_ValueError,
"Couldn't parse date string: %.200s",
PyString_AsString(err_repr));
+#endif
Py_DECREF(err_repr);
return NULL;
}
@@ -218,17 +345,35 @@ UnicodeResultProcessor_init(UnicodeResultProcessor *self, PyObject *args,
PyObject *encoding, *errors = NULL;
static char *kwlist[] = {"encoding", "errors", NULL};
+#if PY_MAJOR_VERSION >= 3
+ if (!PyArg_ParseTupleAndKeywords(args, kwds, "U|U:__init__", kwlist,
+ &encoding, &errors))
+ return -1;
+#else
if (!PyArg_ParseTupleAndKeywords(args, kwds, "S|S:__init__", kwlist,
&encoding, &errors))
return -1;
+#endif
+#if PY_MAJOR_VERSION >= 3
+ encoding = PyUnicode_AsASCIIString(encoding);
+#else
Py_INCREF(encoding);
+#endif
self->encoding = encoding;
if (errors) {
+#if PY_MAJOR_VERSION >= 3
+ errors = PyUnicode_AsASCIIString(errors);
+#else
Py_INCREF(errors);
+#endif
} else {
+#if PY_MAJOR_VERSION >= 3
+ errors = PyBytes_FromString("strict");
+#else
errors = PyString_FromString("strict");
+#endif
if (errors == NULL)
return -1;
}
@@ -247,11 +392,19 @@ UnicodeResultProcessor_process(UnicodeResultProcessor *self, PyObject *value)
if (value == Py_None)
Py_RETURN_NONE;
+#if PY_MAJOR_VERSION >= 3
+ if (PyBytes_AsStringAndSize(value, &str, &len))
+ return NULL;
+
+ encoding = PyBytes_AS_STRING(self->encoding);
+ errors = PyBytes_AS_STRING(self->errors);
+#else
if (PyString_AsStringAndSize(value, &str, &len))
return NULL;
encoding = PyString_AS_STRING(self->encoding);
errors = PyString_AS_STRING(self->errors);
+#endif
return PyUnicode_Decode(str, len, encoding, errors);
}
@@ -261,7 +414,11 @@ UnicodeResultProcessor_dealloc(UnicodeResultProcessor *self)
{
Py_XDECREF(self->encoding);
Py_XDECREF(self->errors);
+#if PY_MAJOR_VERSION >= 3
+ Py_TYPE(self)->tp_free((PyObject*)self);
+#else
self->ob_type->tp_free((PyObject*)self);
+#endif
}
static PyMethodDef UnicodeResultProcessor_methods[] = {
@@ -271,8 +428,7 @@ static PyMethodDef UnicodeResultProcessor_methods[] = {
};
static PyTypeObject UnicodeResultProcessorType = {
- PyObject_HEAD_INIT(NULL)
- 0, /* ob_size */
+ PyVarObject_HEAD_INIT(NULL, 0)
"sqlalchemy.cprocessors.UnicodeResultProcessor", /* tp_name */
sizeof(UnicodeResultProcessor), /* tp_basicsize */
0, /* tp_itemsize */
@@ -322,7 +478,11 @@ DecimalResultProcessor_init(DecimalResultProcessor *self, PyObject *args,
{
PyObject *type, *format;
+#if PY_MAJOR_VERSION >= 3
+ if (!PyArg_ParseTuple(args, "OU", &type, &format))
+#else
if (!PyArg_ParseTuple(args, "OS", &type, &format))
+#endif
return -1;
Py_INCREF(type);
@@ -342,11 +502,21 @@ DecimalResultProcessor_process(DecimalResultProcessor *self, PyObject *value)
if (value == Py_None)
Py_RETURN_NONE;
+ /* Decimal does not accept float values directly */
+ /* SQLite can also give us an integer here (see [ticket:2432]) */
+ /* XXX: starting with Python 3.1, we could use Decimal.from_float(f),
+ but the result wouldn't be the same */
+
args = PyTuple_Pack(1, value);
if (args == NULL)
return NULL;
+#if PY_MAJOR_VERSION >= 3
+ str = PyUnicode_Format(self->format, args);
+#else
str = PyString_Format(self->format, args);
+#endif
+
Py_DECREF(args);
if (str == NULL)
return NULL;
@@ -361,7 +531,11 @@ DecimalResultProcessor_dealloc(DecimalResultProcessor *self)
{
Py_XDECREF(self->type);
Py_XDECREF(self->format);
+#if PY_MAJOR_VERSION >= 3
+ Py_TYPE(self)->tp_free((PyObject*)self);
+#else
self->ob_type->tp_free((PyObject*)self);
+#endif
}
static PyMethodDef DecimalResultProcessor_methods[] = {
@@ -371,8 +545,7 @@ static PyMethodDef DecimalResultProcessor_methods[] = {
};
static PyTypeObject DecimalResultProcessorType = {
- PyObject_HEAD_INIT(NULL)
- 0, /* ob_size */
+ PyVarObject_HEAD_INIT(NULL, 0)
"sqlalchemy.DecimalResultProcessor", /* tp_name */
sizeof(DecimalResultProcessor), /* tp_basicsize */
0, /* tp_itemsize */
@@ -412,11 +585,6 @@ static PyTypeObject DecimalResultProcessorType = {
0, /* tp_new */
};
-#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
-#define PyMODINIT_FUNC void
-#endif
-
-
static PyMethodDef module_methods[] = {
{"int_to_boolean", int_to_boolean, METH_O,
"Convert an integer to a boolean."},
@@ -433,23 +601,53 @@ static PyMethodDef module_methods[] = {
{NULL, NULL, 0, NULL} /* Sentinel */
};
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+
+static struct PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ MODULE_NAME,
+ MODULE_DOC,
+ -1,
+ module_methods
+};
+
+#define INITERROR return NULL
+
+PyObject *
+PyInit_cprocessors(void)
+
+#else
+
+#define INITERROR return
+
PyMODINIT_FUNC
initcprocessors(void)
+
+#endif
+
{
PyObject *m;
UnicodeResultProcessorType.tp_new = PyType_GenericNew;
if (PyType_Ready(&UnicodeResultProcessorType) < 0)
- return;
+ INITERROR;
DecimalResultProcessorType.tp_new = PyType_GenericNew;
if (PyType_Ready(&DecimalResultProcessorType) < 0)
- return;
+ INITERROR;
- m = Py_InitModule3("cprocessors", module_methods,
- "Module containing C versions of data processing functions.");
+#if PY_MAJOR_VERSION >= 3
+ m = PyModule_Create(&module_def);
+#else
+ m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC);
+#endif
if (m == NULL)
- return;
+ INITERROR;
PyDateTime_IMPORT;
@@ -460,5 +658,8 @@ initcprocessors(void)
Py_INCREF(&DecimalResultProcessorType);
PyModule_AddObject(m, "DecimalResultProcessor",
(PyObject *)&DecimalResultProcessorType);
-}
+#if PY_MAJOR_VERSION >= 3
+ return m;
+#endif
+}
diff --git a/libs/sqlalchemy/cextension/resultproxy.c b/libs/sqlalchemy/cextension/resultproxy.c
index ca9d28e6..481352a4 100644
--- a/libs/sqlalchemy/cextension/resultproxy.c
+++ b/libs/sqlalchemy/cextension/resultproxy.c
@@ -1,6 +1,7 @@
/*
resultproxy.c
-Copyright (C) 2010 Gaetan de Menten gdementen@gmail.com
+Copyright (C) 2010-2014 the SQLAlchemy authors and contributors
+Copyright (C) 2010-2011 Gaetan de Menten gdementen@gmail.com
This module is part of SQLAlchemy and is released under
the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -8,6 +9,9 @@ the MIT License: http://www.opensource.org/licenses/mit-license.php
#include
+#define MODULE_NAME "cresultproxy"
+#define MODULE_DOC "Module containing C versions of core ResultProxy classes."
+
#if PY_VERSION_HEX < 0x02050000 && !defined(PY_SSIZE_T_MIN)
typedef int Py_ssize_t;
#define PY_SSIZE_T_MAX INT_MAX
@@ -121,7 +125,7 @@ BaseRowProxy_reduce(PyObject *self)
if (state == NULL)
return NULL;
- module = PyImport_ImportModule("sqlalchemy.engine.base");
+ module = PyImport_ImportModule("sqlalchemy.engine.result");
if (module == NULL)
return NULL;
@@ -149,7 +153,11 @@ BaseRowProxy_dealloc(BaseRowProxy *self)
Py_XDECREF(self->row);
Py_XDECREF(self->processors);
Py_XDECREF(self->keymap);
+#if PY_MAJOR_VERSION >= 3
+ Py_TYPE(self)->tp_free((PyObject *)self);
+#else
self->ob_type->tp_free((PyObject *)self);
+#endif
}
static PyObject *
@@ -244,14 +252,21 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
PyObject *processor, *value, *processed_value;
PyObject *row, *record, *result, *indexobject;
PyObject *exc_module, *exception, *cstr_obj;
+#if PY_MAJOR_VERSION >= 3
+ PyObject *bytes;
+#endif
char *cstr_key;
long index;
int key_fallback = 0;
int tuple_check = 0;
+#if PY_MAJOR_VERSION < 3
if (PyInt_CheckExact(key)) {
index = PyInt_AS_LONG(key);
- } else if (PyLong_CheckExact(key)) {
+ }
+#endif
+
+ if (PyLong_CheckExact(key)) {
index = PyLong_AsLong(key);
if ((index == -1) && PyErr_Occurred())
/* -1 can be either the actual value, or an error flag. */
@@ -304,7 +319,21 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
cstr_obj = PyObject_Str(key);
if (cstr_obj == NULL)
return NULL;
+
+/*
+ FIXME: raise encoding error exception (in both versions below)
+ if the key contains non-ascii chars, instead of an
+ InvalidRequestError without any message like in the
+ python version.
+*/
+#if PY_MAJOR_VERSION >= 3
+ bytes = PyUnicode_AsASCIIString(cstr_obj);
+ if (bytes == NULL)
+ return NULL;
+ cstr_key = PyBytes_AS_STRING(bytes);
+#else
cstr_key = PyString_AsString(cstr_obj);
+#endif
if (cstr_key == NULL) {
Py_DECREF(cstr_obj);
return NULL;
@@ -317,7 +346,11 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
return NULL;
}
+#if PY_MAJOR_VERSION >= 3
+ index = PyLong_AsLong(indexobject);
+#else
index = PyInt_AsLong(indexobject);
+#endif
if ((index == -1) && PyErr_Occurred())
/* -1 can be either the actual value, or an error flag. */
return NULL;
@@ -356,13 +389,23 @@ BaseRowProxy_subscript(BaseRowProxy *self, PyObject *key)
static PyObject *
BaseRowProxy_getitem(PyObject *self, Py_ssize_t i)
{
- return BaseRowProxy_subscript((BaseRowProxy*)self, PyInt_FromSsize_t(i));
+ PyObject *index;
+
+#if PY_MAJOR_VERSION >= 3
+ index = PyLong_FromSsize_t(i);
+#else
+ index = PyInt_FromSsize_t(i);
+#endif
+ return BaseRowProxy_subscript((BaseRowProxy*)self, index);
}
static PyObject *
BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name)
{
PyObject *tmp;
+#if PY_MAJOR_VERSION >= 3
+ PyObject *err_bytes;
+#endif
if (!(tmp = PyObject_GenericGetAttr((PyObject *)self, name))) {
if (!PyErr_ExceptionMatches(PyExc_AttributeError))
@@ -374,11 +417,23 @@ BaseRowProxy_getattro(BaseRowProxy *self, PyObject *name)
tmp = BaseRowProxy_subscript(self, name);
if (tmp == NULL && PyErr_ExceptionMatches(PyExc_KeyError)) {
+
+#if PY_MAJOR_VERSION >= 3
+ err_bytes = PyUnicode_AsASCIIString(name);
+ if (err_bytes == NULL)
+ return NULL;
+ PyErr_Format(
+ PyExc_AttributeError,
+ "Could not locate column in row for column '%.200s'",
+ PyBytes_AS_STRING(err_bytes)
+ );
+#else
PyErr_Format(
PyExc_AttributeError,
"Could not locate column in row for column '%.200s'",
PyString_AsString(name)
);
+#endif
return NULL;
}
return tmp;
@@ -406,7 +461,7 @@ BaseRowProxy_setparent(BaseRowProxy *self, PyObject *value, void *closure)
return -1;
}
- module = PyImport_ImportModule("sqlalchemy.engine.base");
+ module = PyImport_ImportModule("sqlalchemy.engine.result");
if (module == NULL)
return -1;
@@ -564,8 +619,7 @@ static PyMappingMethods BaseRowProxy_as_mapping = {
};
static PyTypeObject BaseRowProxyType = {
- PyObject_HEAD_INIT(NULL)
- 0, /* ob_size */
+ PyVarObject_HEAD_INIT(NULL, 0)
"sqlalchemy.cresultproxy.BaseRowProxy", /* tp_name */
sizeof(BaseRowProxy), /* tp_basicsize */
0, /* tp_itemsize */
@@ -605,34 +659,60 @@ static PyTypeObject BaseRowProxyType = {
0 /* tp_new */
};
-
-#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
-#define PyMODINIT_FUNC void
-#endif
-
-
static PyMethodDef module_methods[] = {
{"safe_rowproxy_reconstructor", safe_rowproxy_reconstructor, METH_VARARGS,
"reconstruct a RowProxy instance from its pickled form."},
{NULL, NULL, 0, NULL} /* Sentinel */
};
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+
+static struct PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ MODULE_NAME,
+ MODULE_DOC,
+ -1,
+ module_methods
+};
+
+#define INITERROR return NULL
+
+PyObject *
+PyInit_cresultproxy(void)
+
+#else
+
+#define INITERROR return
+
PyMODINIT_FUNC
initcresultproxy(void)
+
+#endif
+
{
PyObject *m;
BaseRowProxyType.tp_new = PyType_GenericNew;
if (PyType_Ready(&BaseRowProxyType) < 0)
- return;
+ INITERROR;
- m = Py_InitModule3("cresultproxy", module_methods,
- "Module containing C versions of core ResultProxy classes.");
+#if PY_MAJOR_VERSION >= 3
+ m = PyModule_Create(&module_def);
+#else
+ m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC);
+#endif
if (m == NULL)
- return;
+ INITERROR;
Py_INCREF(&BaseRowProxyType);
PyModule_AddObject(m, "BaseRowProxy", (PyObject *)&BaseRowProxyType);
+#if PY_MAJOR_VERSION >= 3
+ return m;
+#endif
}
-
diff --git a/libs/sqlalchemy/cextension/utils.c b/libs/sqlalchemy/cextension/utils.c
new file mode 100644
index 00000000..3c556e1c
--- /dev/null
+++ b/libs/sqlalchemy/cextension/utils.c
@@ -0,0 +1,225 @@
+/*
+utils.c
+Copyright (C) 2012-2014 the SQLAlchemy authors and contributors
+
+This module is part of SQLAlchemy and is released under
+the MIT License: http://www.opensource.org/licenses/mit-license.php
+*/
+
+#include
+
+#define MODULE_NAME "cutils"
+#define MODULE_DOC "Module containing C versions of utility functions."
+
+/*
+ Given arguments from the calling form *multiparams, **params,
+ return a list of bind parameter structures, usually a list of
+ dictionaries.
+
+ In the case of 'raw' execution which accepts positional parameters,
+ it may be a list of tuples or lists.
+
+ */
+static PyObject *
+distill_params(PyObject *self, PyObject *args)
+{
+ PyObject *multiparams, *params;
+ PyObject *enclosing_list, *double_enclosing_list;
+ PyObject *zero_element, *zero_element_item;
+ Py_ssize_t multiparam_size, zero_element_length;
+
+ if (!PyArg_UnpackTuple(args, "_distill_params", 2, 2, &multiparams, ¶ms)) {
+ return NULL;
+ }
+
+ if (multiparams != Py_None) {
+ multiparam_size = PyTuple_Size(multiparams);
+ if (multiparam_size < 0) {
+ return NULL;
+ }
+ }
+ else {
+ multiparam_size = 0;
+ }
+
+ if (multiparam_size == 0) {
+ if (params != Py_None && PyDict_Size(params) != 0) {
+ enclosing_list = PyList_New(1);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ Py_INCREF(params);
+ if (PyList_SetItem(enclosing_list, 0, params) == -1) {
+ Py_DECREF(params);
+ Py_DECREF(enclosing_list);
+ return NULL;
+ }
+ }
+ else {
+ enclosing_list = PyList_New(0);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ }
+ return enclosing_list;
+ }
+ else if (multiparam_size == 1) {
+ zero_element = PyTuple_GetItem(multiparams, 0);
+ if (PyTuple_Check(zero_element) || PyList_Check(zero_element)) {
+ zero_element_length = PySequence_Length(zero_element);
+
+ if (zero_element_length != 0) {
+ zero_element_item = PySequence_GetItem(zero_element, 0);
+ if (zero_element_item == NULL) {
+ return NULL;
+ }
+ }
+ else {
+ zero_element_item = NULL;
+ }
+
+ if (zero_element_length == 0 ||
+ (
+ PyObject_HasAttrString(zero_element_item, "__iter__") &&
+ !PyObject_HasAttrString(zero_element_item, "strip")
+ )
+ ) {
+ /*
+ * execute(stmt, [{}, {}, {}, ...])
+ * execute(stmt, [(), (), (), ...])
+ */
+ Py_XDECREF(zero_element_item);
+ Py_INCREF(zero_element);
+ return zero_element;
+ }
+ else {
+ /*
+ * execute(stmt, ("value", "value"))
+ */
+ Py_XDECREF(zero_element_item);
+ enclosing_list = PyList_New(1);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ Py_INCREF(zero_element);
+ if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) {
+ Py_DECREF(zero_element);
+ Py_DECREF(enclosing_list);
+ return NULL;
+ }
+ return enclosing_list;
+ }
+ }
+ else if (PyObject_HasAttrString(zero_element, "keys")) {
+ /*
+ * execute(stmt, {"key":"value"})
+ */
+ enclosing_list = PyList_New(1);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ Py_INCREF(zero_element);
+ if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) {
+ Py_DECREF(zero_element);
+ Py_DECREF(enclosing_list);
+ return NULL;
+ }
+ return enclosing_list;
+ } else {
+ enclosing_list = PyList_New(1);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ double_enclosing_list = PyList_New(1);
+ if (double_enclosing_list == NULL) {
+ Py_DECREF(enclosing_list);
+ return NULL;
+ }
+ Py_INCREF(zero_element);
+ if (PyList_SetItem(enclosing_list, 0, zero_element) == -1) {
+ Py_DECREF(zero_element);
+ Py_DECREF(enclosing_list);
+ Py_DECREF(double_enclosing_list);
+ return NULL;
+ }
+ if (PyList_SetItem(double_enclosing_list, 0, enclosing_list) == -1) {
+ Py_DECREF(zero_element);
+ Py_DECREF(enclosing_list);
+ Py_DECREF(double_enclosing_list);
+ return NULL;
+ }
+ return double_enclosing_list;
+ }
+ }
+ else {
+ zero_element = PyTuple_GetItem(multiparams, 0);
+ if (PyObject_HasAttrString(zero_element, "__iter__") &&
+ !PyObject_HasAttrString(zero_element, "strip")
+ ) {
+ Py_INCREF(multiparams);
+ return multiparams;
+ }
+ else {
+ enclosing_list = PyList_New(1);
+ if (enclosing_list == NULL) {
+ return NULL;
+ }
+ Py_INCREF(multiparams);
+ if (PyList_SetItem(enclosing_list, 0, multiparams) == -1) {
+ Py_DECREF(multiparams);
+ Py_DECREF(enclosing_list);
+ return NULL;
+ }
+ return enclosing_list;
+ }
+ }
+}
+
+static PyMethodDef module_methods[] = {
+ {"_distill_params", distill_params, METH_VARARGS,
+ "Distill an execute() parameter structure."},
+ {NULL, NULL, 0, NULL} /* Sentinel */
+};
+
+#ifndef PyMODINIT_FUNC /* declarations for DLL import/export */
+#define PyMODINIT_FUNC void
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+
+static struct PyModuleDef module_def = {
+ PyModuleDef_HEAD_INIT,
+ MODULE_NAME,
+ MODULE_DOC,
+ -1,
+ module_methods
+ };
+#endif
+
+
+#if PY_MAJOR_VERSION >= 3
+PyObject *
+PyInit_cutils(void)
+#else
+PyMODINIT_FUNC
+initcutils(void)
+#endif
+{
+ PyObject *m;
+
+#if PY_MAJOR_VERSION >= 3
+ m = PyModule_Create(&module_def);
+#else
+ m = Py_InitModule3(MODULE_NAME, module_methods, MODULE_DOC);
+#endif
+
+#if PY_MAJOR_VERSION >= 3
+ if (m == NULL)
+ return NULL;
+ return m;
+#else
+ if (m == NULL)
+ return;
+#endif
+}
+
diff --git a/libs/sqlalchemy/connectors/__init__.py b/libs/sqlalchemy/connectors/__init__.py
index a4e017c4..761024fe 100644
--- a/libs/sqlalchemy/connectors/__init__.py
+++ b/libs/sqlalchemy/connectors/__init__.py
@@ -1,5 +1,5 @@
# connectors/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -7,4 +7,3 @@
class Connector(object):
pass
-
diff --git a/libs/sqlalchemy/connectors/mxodbc.py b/libs/sqlalchemy/connectors/mxodbc.py
index 2848f200..e5562a25 100644
--- a/libs/sqlalchemy/connectors/mxodbc.py
+++ b/libs/sqlalchemy/connectors/mxodbc.py
@@ -1,5 +1,5 @@
# connectors/mxodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -22,14 +22,15 @@ import sys
import re
import warnings
-from sqlalchemy.connectors import Connector
+from . import Connector
+
class MxODBCConnector(Connector):
- driver='mxodbc'
+ driver = 'mxodbc'
supports_sane_multi_rowcount = False
- supports_unicode_statements = False
- supports_unicode_binds = False
+ supports_unicode_statements = True
+ supports_unicode_binds = True
supports_native_decimal = True
@@ -47,7 +48,7 @@ class MxODBCConnector(Connector):
elif platform == 'darwin':
from mx.ODBC import iODBC as module
else:
- raise ImportError, "Unrecognized platform for mxODBC import"
+ raise ImportError("Unrecognized platform for mxODBC import")
return module
@classmethod
@@ -73,15 +74,15 @@ class MxODBCConnector(Connector):
emit Python standard warnings.
"""
from mx.ODBC.Error import Warning as MxOdbcWarning
- def error_handler(connection, cursor, errorclass, errorvalue):
+ def error_handler(connection, cursor, errorclass, errorvalue):
if issubclass(errorclass, MxOdbcWarning):
errorclass.__bases__ = (Warning,)
warnings.warn(message=str(errorvalue),
category=errorclass,
stacklevel=2)
else:
- raise errorclass, errorvalue
+ raise errorclass(errorvalue)
return error_handler
def create_connect_args(self, url):
@@ -130,21 +131,19 @@ class MxODBCConnector(Connector):
version.append(n)
return tuple(version)
- def do_execute(self, cursor, statement, parameters, context=None):
+ def _get_direct(self, context):
if context:
native_odbc_execute = context.execution_options.\
get('native_odbc_execute', 'auto')
- if native_odbc_execute is True:
- # user specified native_odbc_execute=True
- cursor.execute(statement, parameters)
- elif native_odbc_execute is False:
- # user specified native_odbc_execute=False
- cursor.executedirect(statement, parameters)
- elif context.is_crud:
- # statement is UPDATE, DELETE, INSERT
- cursor.execute(statement, parameters)
- else:
- # all other statements
- cursor.executedirect(statement, parameters)
+ # default to direct=True in all cases, is more generally
+ # compatible especially with SQL Server
+ return False if native_odbc_execute is True else True
else:
- cursor.executedirect(statement, parameters)
+ return True
+
+ def do_executemany(self, cursor, statement, parameters, context=None):
+ cursor.executemany(
+ statement, parameters, direct=self._get_direct(context))
+
+ def do_execute(self, cursor, statement, parameters, context=None):
+ cursor.execute(statement, parameters, direct=self._get_direct(context))
diff --git a/libs/sqlalchemy/connectors/mysqldb.py b/libs/sqlalchemy/connectors/mysqldb.py
index be1f3530..0f250dfd 100644
--- a/libs/sqlalchemy/connectors/mysqldb.py
+++ b/libs/sqlalchemy/connectors/mysqldb.py
@@ -1,19 +1,26 @@
+# connectors/mysqldb.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
"""Define behaviors common to MySQLdb dialects.
Currently includes MySQL and Drizzle.
"""
-from sqlalchemy.connectors import Connector
-from sqlalchemy.engine import base as engine_base, default
-from sqlalchemy.sql import operators as sql_operators
-from sqlalchemy import exc, log, schema, sql, types as sqltypes, util
-from sqlalchemy import processors
+from . import Connector
+from ..engine import base as engine_base, default
+from ..sql import operators as sql_operators
+from .. import exc, log, schema, sql, types as sqltypes, util, processors
import re
+
# the subclassing of Connector by all classes
# here is not strictly necessary
+
class MySQLDBExecutionContext(Connector):
@property
@@ -23,19 +30,23 @@ class MySQLDBExecutionContext(Connector):
else:
return self.cursor.rowcount
+
class MySQLDBCompiler(Connector):
- def visit_mod(self, binary, **kw):
- return self.process(binary.left) + " %% " + self.process(binary.right)
+ def visit_mod_binary(self, binary, operator, **kw):
+ return self.process(binary.left, **kw) + " %% " + \
+ self.process(binary.right, **kw)
def post_process_text(self, text):
return text.replace('%', '%%')
+
class MySQLDBIdentifierPreparer(Connector):
def _escape_identifier(self, value):
value = value.replace(self.escape_quote, self.escape_to_quote)
return value.replace("%", "%%")
+
class MySQLDBConnector(Connector):
driver = 'mysqldb'
supports_unicode_statements = False
@@ -76,7 +87,8 @@ class MySQLDBConnector(Connector):
# query string.
ssl = {}
- for key in ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']:
+ keys = ['ssl_ca', 'ssl_key', 'ssl_cert', 'ssl_capath', 'ssl_cipher']
+ for key in keys:
if key in opts:
ssl[key[4:]] = opts[key]
util.coerce_kw_type(ssl, key[4:], str)
@@ -148,4 +160,3 @@ class MySQLDBConnector(Connector):
"combination of MySQL server and MySQL-python. "
"MySQL-python >= 1.2.2 is recommended. Assuming latin1.")
return 'latin1'
-
diff --git a/libs/sqlalchemy/connectors/pyodbc.py b/libs/sqlalchemy/connectors/pyodbc.py
index 5be65d2d..284de288 100644
--- a/libs/sqlalchemy/connectors/pyodbc.py
+++ b/libs/sqlalchemy/connectors/pyodbc.py
@@ -1,23 +1,27 @@
# connectors/pyodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from sqlalchemy.connectors import Connector
-from sqlalchemy.util import asbool
+from . import Connector
+from .. import util
+
import sys
import re
-import urllib
+
class PyODBCConnector(Connector):
- driver='pyodbc'
+ driver = 'pyodbc'
supports_sane_multi_rowcount = False
- # PyODBC unicode is broken on UCS-4 builds
- supports_unicode = sys.maxunicode == 65535
- supports_unicode_statements = supports_unicode
+
+ if util.py2k:
+ # PyODBC unicode is broken on UCS-4 builds
+ supports_unicode = sys.maxunicode == 65535
+ supports_unicode_statements = supports_unicode
+
supports_native_decimal = True
default_paramstyle = 'named'
@@ -55,15 +59,15 @@ class PyODBCConnector(Connector):
connect_args = {}
for param in ('ansi', 'unicode_results', 'autocommit'):
if param in keys:
- connect_args[param] = asbool(keys.pop(param))
+ connect_args[param] = util.asbool(keys.pop(param))
if 'odbc_connect' in keys:
- connectors = [urllib.unquote_plus(keys.pop('odbc_connect'))]
+ connectors = [util.unquote_plus(keys.pop('odbc_connect'))]
else:
dsn_connection = 'dsn' in keys or \
('host' in keys and 'database' not in keys)
if dsn_connection:
- connectors= ['dsn=%s' % (keys.pop('host', '') or \
+ connectors = ['dsn=%s' % (keys.pop('host', '') or \
keys.pop('dsn', ''))]
else:
port = ''
@@ -73,7 +77,7 @@ class PyODBCConnector(Connector):
connectors = ["DRIVER={%s}" %
keys.pop('driver', self.pyodbc_driver_name),
'Server=%s%s' % (keys.pop('host', ''), port),
- 'Database=%s' % keys.pop('database', '') ]
+ 'Database=%s' % keys.pop('database', '')]
user = keys.pop("user", None)
if user:
@@ -90,8 +94,8 @@ class PyODBCConnector(Connector):
connectors.append("AutoTranslate=%s" %
keys.pop("odbc_autotranslate"))
- connectors.extend(['%s=%s' % (k,v) for k,v in keys.iteritems()])
- return [[";".join (connectors)], connect_args]
+ connectors.extend(['%s=%s' % (k, v) for k, v in keys.items()])
+ return [[";".join(connectors)], connect_args]
def is_disconnect(self, e, connection, cursor):
if isinstance(e, self.dbapi.ProgrammingError):
@@ -117,19 +121,22 @@ class PyODBCConnector(Connector):
))
if self.freetds:
- self.freetds_driver_version = dbapi_con.getinfo(pyodbc.SQL_DRIVER_VER)
+ self.freetds_driver_version = dbapi_con.getinfo(
+ pyodbc.SQL_DRIVER_VER)
+
+ self.supports_unicode_statements = (
+ not util.py2k or
+ (not self.freetds and not self.easysoft)
+ )
- # the "Py2K only" part here is theoretical.
- # have not tried pyodbc + python3.1 yet.
- # Py2K
- self.supports_unicode_statements = not self.freetds and not self.easysoft
if self._user_supports_unicode_binds is not None:
self.supports_unicode_binds = self._user_supports_unicode_binds
+ elif util.py2k:
+ self.supports_unicode_binds = (
+ not self.freetds or self.freetds_driver_version >= '0.91'
+ ) and not self.easysoft
else:
- self.supports_unicode_binds = (not self.freetds or
- self.freetds_driver_version >= '0.91'
- ) and not self.easysoft
- # end Py2K
+ self.supports_unicode_binds = True
# run other initialization which asks for user name, etc.
super(PyODBCConnector, self).initialize(connection)
diff --git a/libs/sqlalchemy/connectors/zxJDBC.py b/libs/sqlalchemy/connectors/zxJDBC.py
index e2bfed2e..e0bbc573 100644
--- a/libs/sqlalchemy/connectors/zxJDBC.py
+++ b/libs/sqlalchemy/connectors/zxJDBC.py
@@ -1,11 +1,12 @@
# connectors/zxJDBC.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
import sys
-from sqlalchemy.connectors import Connector
+from . import Connector
+
class ZxJDBCConnector(Connector):
driver = 'zxjdbc'
diff --git a/libs/sqlalchemy/databases/__init__.py b/libs/sqlalchemy/databases/__init__.py
index bb0b370e..915eefa4 100644
--- a/libs/sqlalchemy/databases/__init__.py
+++ b/libs/sqlalchemy/databases/__init__.py
@@ -1,5 +1,5 @@
# databases/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -8,26 +8,20 @@
compatibility with pre 0.6 versions.
"""
-from sqlalchemy.dialects.sqlite import base as sqlite
-from sqlalchemy.dialects.postgresql import base as postgresql
+from ..dialects.sqlite import base as sqlite
+from ..dialects.postgresql import base as postgresql
postgres = postgresql
-from sqlalchemy.dialects.mysql import base as mysql
-from sqlalchemy.dialects.drizzle import base as drizzle
-from sqlalchemy.dialects.oracle import base as oracle
-from sqlalchemy.dialects.firebird import base as firebird
-from sqlalchemy.dialects.maxdb import base as maxdb
-from sqlalchemy.dialects.informix import base as informix
-from sqlalchemy.dialects.mssql import base as mssql
-from sqlalchemy.dialects.access import base as access
-from sqlalchemy.dialects.sybase import base as sybase
+from ..dialects.mysql import base as mysql
+from ..dialects.drizzle import base as drizzle
+from ..dialects.oracle import base as oracle
+from ..dialects.firebird import base as firebird
+from ..dialects.mssql import base as mssql
+from ..dialects.sybase import base as sybase
__all__ = (
- 'access',
'drizzle',
'firebird',
- 'informix',
- 'maxdb',
'mssql',
'mysql',
'postgresql',
diff --git a/libs/sqlalchemy/dialects/__init__.py b/libs/sqlalchemy/dialects/__init__.py
index a427cde4..974d4f78 100644
--- a/libs/sqlalchemy/dialects/__init__.py
+++ b/libs/sqlalchemy/dialects/__init__.py
@@ -1,15 +1,12 @@
# dialects/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
__all__ = (
-# 'access',
'drizzle',
'firebird',
-# 'informix',
-# 'maxdb',
'mssql',
'mysql',
'oracle',
@@ -17,3 +14,31 @@ __all__ = (
'sqlite',
'sybase',
)
+
+from .. import util
+
+def _auto_fn(name):
+ """default dialect importer.
+
+ plugs into the :class:`.PluginLoader`
+ as a first-hit system.
+
+ """
+ if "." in name:
+ dialect, driver = name.split(".")
+ else:
+ dialect = name
+ driver = "base"
+ try:
+ module = __import__('sqlalchemy.dialects.%s' % (dialect, )).dialects
+ except ImportError:
+ return None
+
+ module = getattr(module, dialect)
+ if hasattr(module, driver):
+ module = getattr(module, driver)
+ return lambda: module.dialect
+ else:
+ return None
+
+registry = util.PluginLoader("sqlalchemy.dialects", auto_fn=_auto_fn)
diff --git a/libs/sqlalchemy/dialects/access/__init__.py b/libs/sqlalchemy/dialects/access/__init__.py
deleted file mode 100644
index e69de29b..00000000
diff --git a/libs/sqlalchemy/dialects/access/base.py b/libs/sqlalchemy/dialects/access/base.py
deleted file mode 100644
index f107c9c8..00000000
--- a/libs/sqlalchemy/dialects/access/base.py
+++ /dev/null
@@ -1,451 +0,0 @@
-# access/base.py
-# Copyright (C) 2007-2011 the SQLAlchemy authors and contributors
-# Copyright (C) 2007 Paul Johnston, paj@pajhome.org.uk
-# Portions derived from jet2sql.py by Matt Keranen, mksql@yahoo.com
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"""
-Support for the Microsoft Access database.
-
-.. note::
-
- The Access dialect is **non-functional as of SQLAlchemy 0.6**,
- pending development efforts to bring it up-to-date.
-
-
-"""
-from sqlalchemy import sql, schema, types, exc, pool
-from sqlalchemy.sql import compiler, expression
-from sqlalchemy.engine import default, base, reflection
-from sqlalchemy import processors
-
-class AcNumeric(types.Numeric):
- def get_col_spec(self):
- return "NUMERIC"
-
- def bind_processor(self, dialect):
- return processors.to_str
-
- def result_processor(self, dialect, coltype):
- return None
-
-class AcFloat(types.Float):
- def get_col_spec(self):
- return "FLOAT"
-
- def bind_processor(self, dialect):
- """By converting to string, we can use Decimal types round-trip."""
- return processors.to_str
-
-class AcInteger(types.Integer):
- def get_col_spec(self):
- return "INTEGER"
-
-class AcTinyInteger(types.Integer):
- def get_col_spec(self):
- return "TINYINT"
-
-class AcSmallInteger(types.SmallInteger):
- def get_col_spec(self):
- return "SMALLINT"
-
-class AcDateTime(types.DateTime):
- def get_col_spec(self):
- return "DATETIME"
-
-class AcDate(types.Date):
-
- def get_col_spec(self):
- return "DATETIME"
-
-class AcText(types.Text):
- def get_col_spec(self):
- return "MEMO"
-
-class AcString(types.String):
- def get_col_spec(self):
- return "TEXT" + (self.length and ("(%d)" % self.length) or "")
-
-class AcUnicode(types.Unicode):
- def get_col_spec(self):
- return "TEXT" + (self.length and ("(%d)" % self.length) or "")
-
- def bind_processor(self, dialect):
- return None
-
- def result_processor(self, dialect, coltype):
- return None
-
-class AcChar(types.CHAR):
- def get_col_spec(self):
- return "TEXT" + (self.length and ("(%d)" % self.length) or "")
-
-class AcBinary(types.LargeBinary):
- def get_col_spec(self):
- return "BINARY"
-
-class AcBoolean(types.Boolean):
- def get_col_spec(self):
- return "YESNO"
-
-class AcTimeStamp(types.TIMESTAMP):
- def get_col_spec(self):
- return "TIMESTAMP"
-
-class AccessExecutionContext(default.DefaultExecutionContext):
- def _has_implicit_sequence(self, column):
- if column.primary_key and column.autoincrement:
- if isinstance(column.type, types.Integer) and \
- not column.foreign_keys:
- if column.default is None or \
- (isinstance(column.default, schema.Sequence) and \
- column.default.optional):
- return True
- return False
-
- def post_exec(self):
- """If we inserted into a row with a COUNTER column, fetch the ID"""
-
- if self.compiled.isinsert:
- tbl = self.compiled.statement.table
- if not hasattr(tbl, 'has_sequence'):
- tbl.has_sequence = None
- for column in tbl.c:
- if getattr(column, 'sequence', False) or \
- self._has_implicit_sequence(column):
- tbl.has_sequence = column
- break
-
- if bool(tbl.has_sequence):
- # TBD: for some reason _last_inserted_ids doesn't exist here
- # (but it does at corresponding point in mssql???)
- #if not len(self._last_inserted_ids) or
- # self._last_inserted_ids[0] is None:
- self.cursor.execute("SELECT @@identity AS lastrowid")
- row = self.cursor.fetchone()
- self._last_inserted_ids = [int(row[0])]
- #+ self._last_inserted_ids[1:]
- # print "LAST ROW ID", self._last_inserted_ids
-
- super(AccessExecutionContext, self).post_exec()
-
-
-const, daoEngine = None, None
-class AccessDialect(default.DefaultDialect):
- colspecs = {
- types.Unicode : AcUnicode,
- types.Integer : AcInteger,
- types.SmallInteger: AcSmallInteger,
- types.Numeric : AcNumeric,
- types.Float : AcFloat,
- types.DateTime : AcDateTime,
- types.Date : AcDate,
- types.String : AcString,
- types.LargeBinary : AcBinary,
- types.Boolean : AcBoolean,
- types.Text : AcText,
- types.CHAR: AcChar,
- types.TIMESTAMP: AcTimeStamp,
- }
- name = 'access'
- supports_sane_rowcount = False
- supports_sane_multi_rowcount = False
-
- ported_sqla_06 = False
-
- def type_descriptor(self, typeobj):
- newobj = types.adapt_type(typeobj, self.colspecs)
- return newobj
-
- def __init__(self, **params):
- super(AccessDialect, self).__init__(**params)
- self.text_as_varchar = False
- self._dtbs = None
-
- @classmethod
- def dbapi(cls):
- import win32com.client, pythoncom
-
- global const, daoEngine
- if const is None:
- const = win32com.client.constants
- for suffix in (".36", ".35", ".30"):
- try:
- daoEngine = win32com.client.\
- gencache.\
- EnsureDispatch("DAO.DBEngine" + suffix)
- break
- except pythoncom.com_error:
- pass
- else:
- raise exc.InvalidRequestError(
- "Can't find a DB engine. Check "
- "http://support.microsoft.com/kb/239114 for details.")
-
- import pyodbc as module
- return module
-
- def create_connect_args(self, url):
- opts = url.translate_connect_args()
- connectors = ["Driver={Microsoft Access Driver (*.mdb)}"]
- connectors.append("Dbq=%s" % opts["database"])
- user = opts.get("username", None)
- if user:
- connectors.append("UID=%s" % user)
- connectors.append("PWD=%s" % opts.get("password", ""))
- return [[";".join(connectors)], {}]
-
- def last_inserted_ids(self):
- return self.context.last_inserted_ids
-
- def do_execute(self, cursor, statement, params, context=None):
- if params == {}:
- params = ()
- super(AccessDialect, self).\
- do_execute(cursor, statement, params, **kwargs)
-
- def _execute(self, c, statement, parameters):
- try:
- if parameters == {}:
- parameters = ()
- c.execute(statement, parameters)
- self.context.rowcount = c.rowcount
- except Exception, e:
- raise exc.DBAPIError.instance(statement, parameters, e)
-
- def has_table(self, connection, tablename, schema=None):
- # This approach seems to be more reliable that using DAO
- try:
- connection.execute('select top 1 * from [%s]' % tablename)
- return True
- except Exception, e:
- return False
-
- def reflecttable(self, connection, table, include_columns):
- # This is defined in the function, as it relies on win32com constants,
- # that aren't imported until dbapi method is called
- if not hasattr(self, 'ischema_names'):
- self.ischema_names = {
- const.dbByte: AcBinary,
- const.dbInteger: AcInteger,
- const.dbLong: AcInteger,
- const.dbSingle: AcFloat,
- const.dbDouble: AcFloat,
- const.dbDate: AcDateTime,
- const.dbLongBinary: AcBinary,
- const.dbMemo: AcText,
- const.dbBoolean: AcBoolean,
- const.dbText: AcUnicode, # All Access strings are
- # unicode
- const.dbCurrency: AcNumeric,
- }
-
- # A fresh DAO connection is opened for each reflection
- # This is necessary, so we get the latest updates
- dtbs = daoEngine.OpenDatabase(connection.engine.url.database)
-
- try:
- for tbl in dtbs.TableDefs:
- if tbl.Name.lower() == table.name.lower():
- break
- else:
- raise exc.NoSuchTableError(table.name)
-
- for col in tbl.Fields:
- coltype = self.ischema_names[col.Type]
- if col.Type == const.dbText:
- coltype = coltype(col.Size)
-
- colargs = \
- {
- 'nullable': not(col.Required or
- col.Attributes & const.dbAutoIncrField),
- }
- default = col.DefaultValue
-
- if col.Attributes & const.dbAutoIncrField:
- colargs['default'] = schema.Sequence(col.Name + '_seq')
- elif default:
- if col.Type == const.dbBoolean:
- default = default == 'Yes' and '1' or '0'
- colargs['server_default'] = \
- schema.DefaultClause(sql.text(default))
-
- table.append_column(
- schema.Column(col.Name, coltype, **colargs))
-
- # TBD: check constraints
-
- # Find primary key columns first
- for idx in tbl.Indexes:
- if idx.Primary:
- for col in idx.Fields:
- thecol = table.c[col.Name]
- table.primary_key.add(thecol)
- if isinstance(thecol.type, AcInteger) and \
- not (thecol.default and
- isinstance(
- thecol.default.arg,
- schema.Sequence
- )):
- thecol.autoincrement = False
-
- # Then add other indexes
- for idx in tbl.Indexes:
- if not idx.Primary:
- if len(idx.Fields) == 1:
- col = table.c[idx.Fields[0].Name]
- if not col.primary_key:
- col.index = True
- col.unique = idx.Unique
- else:
- pass # TBD: multi-column indexes
-
-
- for fk in dtbs.Relations:
- if fk.ForeignTable != table.name:
- continue
- scols = [c.ForeignName for c in fk.Fields]
- rcols = ['%s.%s' % (fk.Table, c.Name) for c in fk.Fields]
- table.append_constraint(
- schema.ForeignKeyConstraint(scols, rcols,\
- link_to_name=True))
-
- finally:
- dtbs.Close()
-
- @reflection.cache
- def get_table_names(self, connection, schema=None, **kw):
- # A fresh DAO connection is opened for each reflection
- # This is necessary, so we get the latest updates
- dtbs = daoEngine.OpenDatabase(connection.engine.url.database)
-
- names = [t.Name for t in dtbs.TableDefs
- if t.Name[:4] != "MSys" and t.Name[:4] != "~TMP"]
- dtbs.Close()
- return names
-
-
-class AccessCompiler(compiler.SQLCompiler):
- extract_map = compiler.SQLCompiler.extract_map.copy()
- extract_map.update ({
- 'month': 'm',
- 'day': 'd',
- 'year': 'yyyy',
- 'second': 's',
- 'hour': 'h',
- 'doy': 'y',
- 'minute': 'n',
- 'quarter': 'q',
- 'dow': 'w',
- 'week': 'ww'
- })
-
- def visit_select_precolumns(self, select):
- """Access puts TOP, it's version of LIMIT here """
- s = select.distinct and "DISTINCT " or ""
- if select.limit:
- s += "TOP %s " % (select.limit)
- if select.offset:
- raise exc.InvalidRequestError(
- 'Access does not support LIMIT with an offset')
- return s
-
- def limit_clause(self, select):
- """Limit in access is after the select keyword"""
- return ""
-
- def binary_operator_string(self, binary):
- """Access uses "mod" instead of "%" """
- return binary.operator == '%' and 'mod' or binary.operator
-
- def label_select_column(self, select, column, asfrom):
- if isinstance(column, expression.Function):
- return column.label()
- else:
- return super(AccessCompiler, self).\
- label_select_column(select, column, asfrom)
-
- function_rewrites = {'current_date': 'now',
- 'current_timestamp': 'now',
- 'length': 'len',
- }
- def visit_function(self, func):
- """Access function names differ from the ANSI SQL names;
- rewrite common ones"""
- func.name = self.function_rewrites.get(func.name, func.name)
- return super(AccessCompiler, self).visit_function(func)
-
- def for_update_clause(self, select):
- """FOR UPDATE is not supported by Access; silently ignore"""
- return ''
-
- # Strip schema
- def visit_table(self, table, asfrom=False, **kwargs):
- if asfrom:
- return self.preparer.quote(table.name, table.quote)
- else:
- return ""
-
- def visit_join(self, join, asfrom=False, **kwargs):
- return (self.process(join.left, asfrom=True) + \
- (join.isouter and " LEFT OUTER JOIN " or " INNER JOIN ") + \
- self.process(join.right, asfrom=True) + " ON " + \
- self.process(join.onclause))
-
- def visit_extract(self, extract, **kw):
- field = self.extract_map.get(extract.field, extract.field)
- return 'DATEPART("%s", %s)' % \
- (field, self.process(extract.expr, **kw))
-
-class AccessDDLCompiler(compiler.DDLCompiler):
- def get_column_specification(self, column, **kwargs):
- colspec = self.preparer.format_column(column) + " " + \
- column.type.dialect_impl(self.dialect).get_col_spec()
-
- # install a sequence if we have an implicit IDENTITY column
- if (not getattr(column.table, 'has_sequence', False)) and \
- column.primary_key and \
- column.autoincrement and \
- isinstance(column.type, types.Integer) and \
- not column.foreign_keys:
- if column.default is None or \
- (isinstance(column.default, schema.Sequence) and
- column.default.optional):
- column.sequence = schema.Sequence(column.name + '_seq')
-
- if not column.nullable:
- colspec += " NOT NULL"
-
- if hasattr(column, 'sequence'):
- column.table.has_sequence = column
- colspec = self.preparer.format_column(column) + " counter"
- else:
- default = self.get_column_default_string(column)
- if default is not None:
- colspec += " DEFAULT " + default
-
- return colspec
-
- def visit_drop_index(self, drop):
- index = drop.element
- self.append("\nDROP INDEX [%s].[%s]" % \
- (index.table.name,
- self._index_identifier(index.name)))
-
-class AccessIdentifierPreparer(compiler.IdentifierPreparer):
- reserved_words = compiler.RESERVED_WORDS.copy()
- reserved_words.update(['value', 'text'])
- def __init__(self, dialect):
- super(AccessIdentifierPreparer, self).\
- __init__(dialect, initial_quote='[', final_quote=']')
-
-
-dialect = AccessDialect
-dialect.poolclass = pool.SingletonThreadPool
-dialect.statement_compiler = AccessCompiler
-dialect.ddlcompiler = AccessDDLCompiler
-dialect.preparer = AccessIdentifierPreparer
-dialect.execution_ctx_cls = AccessExecutionContext
diff --git a/libs/sqlalchemy/dialects/drizzle/base.py b/libs/sqlalchemy/dialects/drizzle/base.py
index 0165a2aa..b5addb42 100644
--- a/libs/sqlalchemy/dialects/drizzle/base.py
+++ b/libs/sqlalchemy/dialects/drizzle/base.py
@@ -1,12 +1,15 @@
# drizzle/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
# Copyright (C) 2010-2011 Monty Taylor
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Drizzle database.
+"""
+
+.. dialect:: drizzle
+ :name: Drizzle
Drizzle is a variant of MySQL. Unlike MySQL, Drizzle's default storage engine
is InnoDB (transactions, foreign-keys) rather than MyISAM. For more
@@ -16,10 +19,6 @@ the `Drizzle Documentation `_.
The SQLAlchemy Drizzle dialect leans heavily on the MySQL dialect, so much of
the :doc:`SQLAlchemy MySQL ` documentation is also relevant.
-Connecting
-----------
-
-See the individual driver sections below for details on connecting.
"""
@@ -183,7 +182,7 @@ class BIGINT(sqltypes.BIGINT):
super(BIGINT, self).__init__(**kw)
-class _DrizzleTime(mysql_dialect._MSTime):
+class TIME(mysql_dialect.TIME):
"""Drizzle TIME type."""
@@ -317,7 +316,7 @@ class _DrizzleBoolean(sqltypes.Boolean):
colspecs = {
sqltypes.Numeric: NUMERIC,
sqltypes.Float: FLOAT,
- sqltypes.Time: _DrizzleTime,
+ sqltypes.Time: TIME,
sqltypes.Enum: ENUM,
sqltypes.Boolean: _DrizzleBoolean,
}
@@ -418,6 +417,7 @@ class DrizzleIdentifierPreparer(mysql_dialect.MySQLIdentifierPreparer):
pass
+@log.class_logger
class DrizzleDialect(mysql_dialect.MySQLDialect):
"""Details of the Drizzle dialect.
@@ -447,16 +447,6 @@ class DrizzleDialect(mysql_dialect.MySQLDialect):
conn.autocommit(False)
return connect
- def do_commit(self, connection):
- """Execute a COMMIT."""
-
- connection.commit()
-
- def do_rollback(self, connection):
- """Execute a ROLLBACK."""
-
- connection.rollback()
-
@reflection.cache
def get_table_names(self, connection, schema=None, **kw):
"""Return a Unicode SHOW TABLES from a given schema."""
@@ -506,4 +496,3 @@ class DrizzleDialect(mysql_dialect.MySQLDialect):
self._backslash_escapes = False
-log.class_logger(DrizzleDialect)
diff --git a/libs/sqlalchemy/dialects/drizzle/mysqldb.py b/libs/sqlalchemy/dialects/drizzle/mysqldb.py
index ce9518a8..7d91cc36 100644
--- a/libs/sqlalchemy/dialects/drizzle/mysqldb.py
+++ b/libs/sqlalchemy/dialects/drizzle/mysqldb.py
@@ -1,15 +1,10 @@
-"""Support for the Drizzle database via the mysql-python adapter.
+"""
+.. dialect:: drizzle+mysqldb
+ :name: MySQL-Python
+ :dbapi: mysqldb
+ :connectstring: drizzle+mysqldb://:@[:]/
+ :url: http://sourceforge.net/projects/mysql-python
-MySQL-Python is available at:
-
- http://sourceforge.net/projects/mysql-python
-
-Connecting
------------
-
-Connect string format::
-
- drizzle+mysqldb://:@[:]/
"""
diff --git a/libs/sqlalchemy/dialects/firebird/__init__.py b/libs/sqlalchemy/dialects/firebird/__init__.py
index 2a3b756f..094ac3e8 100644
--- a/libs/sqlalchemy/dialects/firebird/__init__.py
+++ b/libs/sqlalchemy/dialects/firebird/__init__.py
@@ -1,12 +1,12 @@
# firebird/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from sqlalchemy.dialects.firebird import base, kinterbasdb
+from sqlalchemy.dialects.firebird import base, kinterbasdb, fdb
-base.dialect = kinterbasdb.dialect
+base.dialect = fdb.dialect
from sqlalchemy.dialects.firebird.base import \
SMALLINT, BIGINT, FLOAT, FLOAT, DATE, TIME, \
@@ -18,5 +18,3 @@ __all__ = (
'TEXT', 'NUMERIC', 'FLOAT', 'TIMESTAMP', 'VARCHAR', 'CHAR', 'BLOB',
'dialect'
)
-
-
diff --git a/libs/sqlalchemy/dialects/firebird/base.py b/libs/sqlalchemy/dialects/firebird/base.py
index a0bb9c20..b9af6e58 100644
--- a/libs/sqlalchemy/dialects/firebird/base.py
+++ b/libs/sqlalchemy/dialects/firebird/base.py
@@ -1,16 +1,16 @@
# firebird/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for the Firebird database.
-Connectivity is usually supplied via the kinterbasdb_ DBAPI module.
+.. dialect:: firebird
+ :name: Firebird
-Dialects
-~~~~~~~~
+Firebird Dialects
+-----------------
Firebird offers two distinct dialects_ (not to be confused with a
SQLAlchemy ``Dialect``):
@@ -27,7 +27,7 @@ support for dialect 1 is not well tested and probably has
incompatibilities.
Locking Behavior
-~~~~~~~~~~~~~~~~
+----------------
Firebird locks tables aggressively. For this reason, a DROP TABLE may
hang until other transactions are released. SQLAlchemy does its best
@@ -47,7 +47,7 @@ The above use case can be alleviated by calling ``first()`` on the
all remaining cursor/connection resources.
RETURNING support
-~~~~~~~~~~~~~~~~~
+-----------------
Firebird 2.0 supports returning a result set from inserts, and 2.1
extends that to deletes and updates. This is generically exposed by
@@ -69,7 +69,7 @@ the SQLAlchemy ``returning()`` method, such as::
"""
-import datetime, re
+import datetime
from sqlalchemy import schema as sa_schema
from sqlalchemy import exc, types as sqltypes, sql, util
@@ -78,9 +78,8 @@ from sqlalchemy.engine import base, default, reflection
from sqlalchemy.sql import compiler
-from sqlalchemy.types import (BIGINT, BLOB, BOOLEAN, DATE,
- FLOAT, INTEGER, NUMERIC, SMALLINT,
- TEXT, TIME, TIMESTAMP)
+from sqlalchemy.types import (BIGINT, BLOB, DATE, FLOAT, INTEGER, NUMERIC,
+ SMALLINT, TEXT, TIME, TIMESTAMP, Integer)
RESERVED_WORDS = set([
@@ -126,36 +125,49 @@ RESERVED_WORDS = set([
class _StringType(sqltypes.String):
"""Base for Firebird string types."""
- def __init__(self, charset = None, **kw):
+ def __init__(self, charset=None, **kw):
self.charset = charset
super(_StringType, self).__init__(**kw)
+
class VARCHAR(_StringType, sqltypes.VARCHAR):
"""Firebird VARCHAR type"""
__visit_name__ = 'VARCHAR'
- def __init__(self, length = None, **kwargs):
+ def __init__(self, length=None, **kwargs):
super(VARCHAR, self).__init__(length=length, **kwargs)
+
class CHAR(_StringType, sqltypes.CHAR):
"""Firebird CHAR type"""
__visit_name__ = 'CHAR'
- def __init__(self, length = None, **kwargs):
+ def __init__(self, length=None, **kwargs):
super(CHAR, self).__init__(length=length, **kwargs)
+
+class _FBDateTime(sqltypes.DateTime):
+ def bind_processor(self, dialect):
+ def process(value):
+ if type(value) == datetime.date:
+ return datetime.datetime(value.year, value.month, value.day)
+ else:
+ return value
+ return process
+
colspecs = {
+ sqltypes.DateTime: _FBDateTime
}
ischema_names = {
'SHORT': SMALLINT,
- 'LONG': BIGINT,
+ 'LONG': INTEGER,
'QUAD': FLOAT,
'FLOAT': FLOAT,
'DATE': DATE,
'TIME': TIME,
'TEXT': TEXT,
- 'INT64': NUMERIC,
+ 'INT64': BIGINT,
'DOUBLE': FLOAT,
'TIMESTAMP': TIMESTAMP,
'VARYING': VARCHAR,
@@ -192,20 +204,42 @@ class FBTypeCompiler(compiler.GenericTypeCompiler):
return self._extend_string(type_, basic)
def visit_VARCHAR(self, type_):
+ if not type_.length:
+ raise exc.CompileError(
+ "VARCHAR requires a length on dialect %s" %
+ self.dialect.name)
basic = super(FBTypeCompiler, self).visit_VARCHAR(type_)
return self._extend_string(type_, basic)
-
class FBCompiler(sql.compiler.SQLCompiler):
"""Firebird specific idiosyncrasies"""
- def visit_mod(self, binary, **kw):
- # Firebird lacks a builtin modulo operator, but there is
- # an equivalent function in the ib_udf library.
+ ansi_bind_rules = True
+
+ #def visit_contains_op_binary(self, binary, operator, **kw):
+ # cant use CONTAINING b.c. it's case insensitive.
+
+ #def visit_notcontains_op_binary(self, binary, operator, **kw):
+ # cant use NOT CONTAINING b.c. it's case insensitive.
+
+ def visit_now_func(self, fn, **kw):
+ return "CURRENT_TIMESTAMP"
+
+ def visit_startswith_op_binary(self, binary, operator, **kw):
+ return '%s STARTING WITH %s' % (
+ binary.left._compiler_dispatch(self, **kw),
+ binary.right._compiler_dispatch(self, **kw))
+
+ def visit_notstartswith_op_binary(self, binary, operator, **kw):
+ return '%s NOT STARTING WITH %s' % (
+ binary.left._compiler_dispatch(self, **kw),
+ binary.right._compiler_dispatch(self, **kw))
+
+ def visit_mod_binary(self, binary, operator, **kw):
return "mod(%s, %s)" % (
- self.process(binary.left),
- self.process(binary.right))
+ self.process(binary.left, **kw),
+ self.process(binary.right, **kw))
def visit_alias(self, alias, asfrom=False, **kwargs):
if self.dialect._version_two:
@@ -249,7 +283,7 @@ class FBCompiler(sql.compiler.SQLCompiler):
# may require parens - see similar example in the oracle
# dialect
if func.clauses is not None and len(func.clauses):
- return self.process(func.clause_expr)
+ return self.process(func.clause_expr, **kw)
else:
return ""
@@ -267,9 +301,9 @@ class FBCompiler(sql.compiler.SQLCompiler):
result = ""
if select._limit:
- result += "FIRST %s " % self.process(sql.literal(select._limit))
+ result += "FIRST %s " % self.process(sql.literal(select._limit))
if select._offset:
- result +="SKIP %s " % self.process(sql.literal(select._offset))
+ result += "SKIP %s " % self.process(sql.literal(select._offset))
if select._distinct:
result += "DISTINCT "
return result
@@ -280,15 +314,11 @@ class FBCompiler(sql.compiler.SQLCompiler):
return ""
def returning_clause(self, stmt, returning_cols):
-
columns = [
- self.process(
- self.label_select_column(None, c, asfrom=False),
- within_columns_clause=True,
- result_map=self.result_map
- )
+ self._label_select_column(None, c, True, False, {})
for c in expression._select_iterables(returning_cols)
]
+
return 'RETURNING ' + ', '.join(columns)
@@ -329,6 +359,7 @@ class FBIdentifierPreparer(sql.compiler.IdentifierPreparer):
"""Install Firebird specific reserved words."""
reserved_words = RESERVED_WORDS
+ illegal_initial_characters = compiler.ILLEGAL_INITIAL_CHARACTERS.union(['_'])
def __init__(self, dialect):
super(FBIdentifierPreparer, self).__init__(dialect, omit_schema=True)
@@ -444,18 +475,34 @@ class FBDialect(default.DefaultDialect):
@reflection.cache
def get_table_names(self, connection, schema=None, **kw):
+ # there are two queries commonly mentioned for this.
+ # this one, using view_blr, is at the Firebird FAQ among other places:
+ # http://www.firebirdfaq.org/faq174/
s = """
- SELECT DISTINCT rdb$relation_name
- FROM rdb$relation_fields
- WHERE rdb$system_flag=0 AND rdb$view_context IS NULL
+ select rdb$relation_name
+ from rdb$relations
+ where rdb$view_blr is null
+ and (rdb$system_flag is null or rdb$system_flag = 0);
"""
+
+ # the other query is this one. It's not clear if there's really
+ # any difference between these two. This link:
+ # http://www.alberton.info/firebird_sql_meta_info.html#.Ur3vXfZGni8
+ # states them as interchangeable. Some discussion at [ticket:2898]
+ # SELECT DISTINCT rdb$relation_name
+ # FROM rdb$relation_fields
+ # WHERE rdb$system_flag=0 AND rdb$view_context IS NULL
+
return [self.normalize_name(row[0]) for row in connection.execute(s)]
@reflection.cache
def get_view_names(self, connection, schema=None, **kw):
+ # see http://www.firebirdfaq.org/faq174/
s = """
- SELECT distinct rdb$view_name
- FROM rdb$view_relations
+ select rdb$relation_name
+ from rdb$relations
+ where rdb$view_blr is not null
+ and (rdb$system_flag is null or rdb$system_flag = 0);
"""
return [self.normalize_name(row[0]) for row in connection.execute(s)]
@@ -474,7 +521,7 @@ class FBDialect(default.DefaultDialect):
return None
@reflection.cache
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
# Query to extract the PK/FK constrained fields of the given table
keyqry = """
SELECT se.rdb$field_name AS fname
@@ -486,7 +533,7 @@ class FBDialect(default.DefaultDialect):
# get primary key fields
c = connection.execute(keyqry, ["PRIMARY KEY", tablename])
pkfields = [self.normalize_name(r['fname']) for r in c.fetchall()]
- return pkfields
+ return {'constrained_columns': pkfields, 'name': None}
@reflection.cache
def get_column_sequence(self, connection,
@@ -541,7 +588,8 @@ class FBDialect(default.DefaultDialect):
ORDER BY r.rdb$field_position
"""
# get the PK, used to determine the eventual associated sequence
- pkey_cols = self.get_primary_keys(connection, table_name)
+ pk_constraint = self.get_pk_constraint(connection, table_name)
+ pkey_cols = pk_constraint['constrained_columns']
tablename = self.denormalize_name(table_name)
# get all of the fields for this table
@@ -561,8 +609,8 @@ class FBDialect(default.DefaultDialect):
util.warn("Did not recognize type '%s' of column '%s'" %
(colspec, name))
coltype = sqltypes.NULLTYPE
- elif colspec == 'INT64':
- coltype = coltype(
+ elif issubclass(coltype, Integer) and row['fprec'] != 0:
+ coltype = NUMERIC(
precision=row['fprec'],
scale=row['fscale'] * -1)
elif colspec in ('VARYING', 'CSTRING'):
@@ -593,11 +641,11 @@ class FBDialect(default.DefaultDialect):
# Redundant
defvalue = None
col_d = {
- 'name' : name,
- 'type' : coltype,
- 'nullable' : not bool(row['null_flag']),
- 'default' : defvalue,
- 'autoincrement':defvalue is None
+ 'name': name,
+ 'type': coltype,
+ 'nullable': not bool(row['null_flag']),
+ 'default': defvalue,
+ 'autoincrement': defvalue is None
}
if orig_colname.lower() == orig_colname:
@@ -605,7 +653,7 @@ class FBDialect(default.DefaultDialect):
# if the PK is a single field, try to see if its linked to
# a sequence thru a trigger
- if len(pkey_cols)==1 and name==pkey_cols[0]:
+ if len(pkey_cols) == 1 and name == pkey_cols[0]:
seq_d = self.get_column_sequence(connection, tablename, name)
if seq_d is not None:
col_d['sequence'] = seq_d
@@ -635,12 +683,12 @@ class FBDialect(default.DefaultDialect):
tablename = self.denormalize_name(table_name)
c = connection.execute(fkqry, ["FOREIGN KEY", tablename])
- fks = util.defaultdict(lambda:{
- 'name' : None,
- 'constrained_columns' : [],
- 'referred_schema' : None,
- 'referred_table' : None,
- 'referred_columns' : []
+ fks = util.defaultdict(lambda: {
+ 'name': None,
+ 'constrained_columns': [],
+ 'referred_schema': None,
+ 'referred_table': None,
+ 'referred_columns': []
})
for row in c:
@@ -653,7 +701,7 @@ class FBDialect(default.DefaultDialect):
self.normalize_name(row['fname']))
fk['referred_columns'].append(
self.normalize_name(row['targetfname']))
- return fks.values()
+ return list(fks.values())
@reflection.cache
def get_indexes(self, connection, table_name, schema=None, **kw):
@@ -669,7 +717,7 @@ class FBDialect(default.DefaultDialect):
ic.rdb$index_name
WHERE ix.rdb$relation_name=? AND ix.rdb$foreign_key IS NULL
AND rdb$relation_constraints.rdb$constraint_type IS NULL
- ORDER BY index_name, field_name
+ ORDER BY index_name, ic.rdb$field_position
"""
c = connection.execute(qry, [self.denormalize_name(table_name)])
@@ -684,17 +732,5 @@ class FBDialect(default.DefaultDialect):
indexrec['column_names'].append(
self.normalize_name(row['field_name']))
- return indexes.values()
+ return list(indexes.values())
- def do_execute(self, cursor, statement, parameters, context=None):
- # kinterbase does not accept a None, but wants an empty list
- # when there are no arguments.
- cursor.execute(statement, parameters or [])
-
- def do_rollback(self, connection):
- # Use the retaining feature, that keeps the transaction going
- connection.rollback(True)
-
- def do_commit(self, connection):
- # Use the retaining feature, that keeps the transaction going
- connection.commit(True)
diff --git a/libs/sqlalchemy/dialects/firebird/fdb.py b/libs/sqlalchemy/dialects/firebird/fdb.py
new file mode 100644
index 00000000..4d94ef0d
--- /dev/null
+++ b/libs/sqlalchemy/dialects/firebird/fdb.py
@@ -0,0 +1,115 @@
+# firebird/fdb.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""
+.. dialect:: firebird+fdb
+ :name: fdb
+ :dbapi: pyodbc
+ :connectstring: firebird+fdb://user:password@host:port/path/to/db[?key=value&key=value...]
+ :url: http://pypi.python.org/pypi/fdb/
+
+ fdb is a kinterbasdb compatible DBAPI for Firebird.
+
+ .. versionadded:: 0.8 - Support for the fdb Firebird driver.
+
+ .. versionchanged:: 0.9 - The fdb dialect is now the default dialect
+ under the ``firebird://`` URL space, as ``fdb`` is now the official
+ Python driver for Firebird.
+
+Arguments
+----------
+
+The ``fdb`` dialect is based on the :mod:`sqlalchemy.dialects.firebird.kinterbasdb`
+dialect, however does not accept every argument that Kinterbasdb does.
+
+* ``enable_rowcount`` - True by default, setting this to False disables
+ the usage of "cursor.rowcount" with the
+ Kinterbasdb dialect, which SQLAlchemy ordinarily calls upon automatically
+ after any UPDATE or DELETE statement. When disabled, SQLAlchemy's
+ ResultProxy will return -1 for result.rowcount. The rationale here is
+ that Kinterbasdb requires a second round trip to the database when
+ .rowcount is called - since SQLA's resultproxy automatically closes
+ the cursor after a non-result-returning statement, rowcount must be
+ called, if at all, before the result object is returned. Additionally,
+ cursor.rowcount may not return correct results with older versions
+ of Firebird, and setting this flag to False will also cause the
+ SQLAlchemy ORM to ignore its usage. The behavior can also be controlled on a
+ per-execution basis using the ``enable_rowcount`` option with
+ :meth:`.Connection.execution_options`::
+
+ conn = engine.connect().execution_options(enable_rowcount=True)
+ r = conn.execute(stmt)
+ print r.rowcount
+
+* ``retaining`` - False by default. Setting this to True will pass the
+ ``retaining=True`` keyword argument to the ``.commit()`` and ``.rollback()``
+ methods of the DBAPI connection, which can improve performance in some
+ situations, but apparently with significant caveats.
+ Please read the fdb and/or kinterbasdb DBAPI documentation in order to
+ understand the implications of this flag.
+
+ .. versionadded:: 0.8.2 - ``retaining`` keyword argument specifying
+ transaction retaining behavior - in 0.8 it defaults to ``True``
+ for backwards compatibility.
+
+ .. versionchanged:: 0.9.0 - the ``retaining`` flag defaults to ``False``.
+ In 0.8 it defaulted to ``True``.
+
+ .. seealso::
+
+ http://pythonhosted.org/fdb/usage-guide.html#retaining-transactions - information
+ on the "retaining" flag.
+
+"""
+
+from .kinterbasdb import FBDialect_kinterbasdb
+from ... import util
+
+
+class FBDialect_fdb(FBDialect_kinterbasdb):
+
+ def __init__(self, enable_rowcount=True,
+ retaining=False, **kwargs):
+ super(FBDialect_fdb, self).__init__(
+ enable_rowcount=enable_rowcount,
+ retaining=retaining, **kwargs)
+
+ @classmethod
+ def dbapi(cls):
+ return __import__('fdb')
+
+ def create_connect_args(self, url):
+ opts = url.translate_connect_args(username='user')
+ if opts.get('port'):
+ opts['host'] = "%s/%s" % (opts['host'], opts['port'])
+ del opts['port']
+ opts.update(url.query)
+
+ util.coerce_kw_type(opts, 'type_conv', int)
+
+ return ([], opts)
+
+ def _get_server_version_info(self, connection):
+ """Get the version of the Firebird server used by a connection.
+
+ Returns a tuple of (`major`, `minor`, `build`), three integers
+ representing the version of the attached server.
+ """
+
+ # This is the simpler approach (the other uses the services api),
+ # that for backward compatibility reasons returns a string like
+ # LI-V6.3.3.12981 Firebird 2.0
+ # where the first version is a fake one resembling the old
+ # Interbase signature.
+
+ isc_info_firebird_version = 103
+ fbconn = connection.connection
+
+ version = fbconn.db_info(isc_info_firebird_version)
+
+ return self._parse_version_info(version)
+
+dialect = FBDialect_fdb
diff --git a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py b/libs/sqlalchemy/dialects/firebird/kinterbasdb.py
index ddca91db..b8a83a07 100644
--- a/libs/sqlalchemy/dialects/firebird/kinterbasdb.py
+++ b/libs/sqlalchemy/dialects/firebird/kinterbasdb.py
@@ -1,58 +1,48 @@
# firebird/kinterbasdb.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-The most common way to connect to a Firebird engine is implemented by
-kinterbasdb__, currently maintained__ directly by the Firebird people.
+.. dialect:: firebird+kinterbasdb
+ :name: kinterbasdb
+ :dbapi: kinterbasdb
+ :connectstring: firebird+kinterbasdb://user:password@host:port/path/to/db[?key=value&key=value...]
+ :url: http://firebirdsql.org/index.php?op=devel&sub=python
-The connection URL is of the form
-``firebird[+kinterbasdb]://user:password@host:port/path/to/db[?key=value&key=value...]``.
+Arguments
+----------
-Kinterbasedb backend specific keyword arguments are:
+The Kinterbasdb backend accepts the ``enable_rowcount`` and ``retaining``
+arguments accepted by the :mod:`sqlalchemy.dialects.firebird.fdb` dialect. In addition, it
+also accepts the following:
-* type_conv - select the kind of mapping done on the types: by default
- SQLAlchemy uses 200 with Unicode, datetime and decimal support (see
- details__).
+* ``type_conv`` - select the kind of mapping done on the types: by default
+ SQLAlchemy uses 200 with Unicode, datetime and decimal support. See
+ the linked documents below for further information.
-* concurrency_level - set the backend policy with regards to threading
- issues: by default SQLAlchemy uses policy 1 (see details__).
+* ``concurrency_level`` - set the backend policy with regards to threading
+ issues: by default SQLAlchemy uses policy 1. See the linked documents
+ below for futher information.
-* enable_rowcount - True by default, setting this to False disables
- the usage of "cursor.rowcount" with the
- Kinterbasdb dialect, which SQLAlchemy ordinarily calls upon automatically
- after any UPDATE or DELETE statement. When disabled, SQLAlchemy's
- ResultProxy will return -1 for result.rowcount. The rationale here is
- that Kinterbasdb requires a second round trip to the database when
- .rowcount is called - since SQLA's resultproxy automatically closes
- the cursor after a non-result-returning statement, rowcount must be
- called, if at all, before the result object is returned. Additionally,
- cursor.rowcount may not return correct results with older versions
- of Firebird, and setting this flag to False will also cause the
- SQLAlchemy ORM to ignore its usage. The behavior can also be controlled on a
- per-execution basis using the `enable_rowcount` option with
- :meth:`execution_options()`::
+.. seealso::
- conn = engine.connect().execution_options(enable_rowcount=True)
- r = conn.execute(stmt)
- print r.rowcount
+ http://sourceforge.net/projects/kinterbasdb
+
+ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_param_conv_dynamic_type_translation
+
+ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurrency
-__ http://sourceforge.net/projects/kinterbasdb
-__ http://firebirdsql.org/index.php?op=devel&sub=python
-__ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#adv_param_conv_dynamic_type_translation
-__ http://kinterbasdb.sourceforge.net/dist_docs/usage.html#special_issue_concurrency
"""
-from sqlalchemy.dialects.firebird.base import FBDialect, \
- FBCompiler, FBExecutionContext
-from sqlalchemy import util, types as sqltypes
-from sqlalchemy.util.compat import decimal
+from .base import FBDialect, FBExecutionContext
+from ... import util, types as sqltypes
from re import match
+import decimal
-class _FBNumeric_kinterbasdb(sqltypes.Numeric):
+class _kinterbasdb_numeric(object):
def bind_processor(self, dialect):
def process(value):
if isinstance(value, decimal.Decimal):
@@ -61,6 +51,13 @@ class _FBNumeric_kinterbasdb(sqltypes.Numeric):
return value
return process
+class _FBNumeric_kinterbasdb(_kinterbasdb_numeric, sqltypes.Numeric):
+ pass
+
+class _FBFloat_kinterbasdb(_kinterbasdb_numeric, sqltypes.Float):
+ pass
+
+
class FBExecutionContext_kinterbasdb(FBExecutionContext):
@property
def rowcount(self):
@@ -70,6 +67,7 @@ class FBExecutionContext_kinterbasdb(FBExecutionContext):
else:
return -1
+
class FBDialect_kinterbasdb(FBDialect):
driver = 'kinterbasdb'
supports_sane_rowcount = False
@@ -81,24 +79,37 @@ class FBDialect_kinterbasdb(FBDialect):
colspecs = util.update_copy(
FBDialect.colspecs,
{
- sqltypes.Numeric:_FBNumeric_kinterbasdb,
+ sqltypes.Numeric: _FBNumeric_kinterbasdb,
+ sqltypes.Float: _FBFloat_kinterbasdb,
}
)
def __init__(self, type_conv=200, concurrency_level=1,
- enable_rowcount=True, **kwargs):
+ enable_rowcount=True,
+ retaining=False, **kwargs):
super(FBDialect_kinterbasdb, self).__init__(**kwargs)
self.enable_rowcount = enable_rowcount
self.type_conv = type_conv
self.concurrency_level = concurrency_level
+ self.retaining = retaining
if enable_rowcount:
self.supports_sane_rowcount = True
@classmethod
def dbapi(cls):
- k = __import__('kinterbasdb')
- return k
+ return __import__('kinterbasdb')
+
+ def do_execute(self, cursor, statement, parameters, context=None):
+ # kinterbase does not accept a None, but wants an empty list
+ # when there are no arguments.
+ cursor.execute(statement, parameters or [])
+
+ def do_rollback(self, dbapi_connection):
+ dbapi_connection.rollback(self.retaining)
+
+ def do_commit(self, dbapi_connection):
+ dbapi_connection.commit(self.retaining)
def create_connect_args(self, url):
opts = url.translate_connect_args(username='user')
@@ -117,7 +128,8 @@ class FBDialect_kinterbasdb(FBDialect):
initialized = getattr(self.dbapi, 'initialized', None)
if initialized is None:
# CVS rev 1.96 changed the name of the attribute:
- # http://kinterbasdb.cvs.sourceforge.net/viewvc/kinterbasdb/Kinterbasdb-3.0/__init__.py?r1=1.95&r2=1.96
+ # http://kinterbasdb.cvs.sourceforge.net/viewvc/kinterbasdb/
+ # Kinterbasdb-3.0/__init__.py?r1=1.95&r2=1.96
initialized = getattr(self.dbapi, '_initialized', False)
if not initialized:
self.dbapi.init(type_conv=type_conv,
diff --git a/libs/sqlalchemy/dialects/informix/__init__.py b/libs/sqlalchemy/dialects/informix/__init__.py
deleted file mode 100644
index bd633da5..00000000
--- a/libs/sqlalchemy/dialects/informix/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# informix/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-from sqlalchemy.dialects.informix import base, informixdb
-
-base.dialect = informixdb.dialect
\ No newline at end of file
diff --git a/libs/sqlalchemy/dialects/informix/base.py b/libs/sqlalchemy/dialects/informix/base.py
deleted file mode 100644
index 07561f8d..00000000
--- a/libs/sqlalchemy/dialects/informix/base.py
+++ /dev/null
@@ -1,596 +0,0 @@
-# informix/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-# coding: gbk
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"""Support for the Informix database.
-
-.. note::
-
- The Informix dialect functions on current SQLAlchemy versions
- but is not regularly tested, and may have many issues and
- caveats not currently handled.
-
-"""
-
-
-import datetime
-
-from sqlalchemy import sql, schema, exc, pool, util
-from sqlalchemy.sql import compiler, text
-from sqlalchemy.engine import default, reflection
-from sqlalchemy import types as sqltypes
-
-RESERVED_WORDS = set(
- ["abs", "absolute", "access", "access_method", "acos", "active", "add",
- "address", "add_months", "admin", "after", "aggregate", "alignment",
- "all", "allocate", "all_rows", "alter", "and", "ansi", "any", "append",
- "array", "as", "asc", "ascii", "asin", "at", "atan", "atan2", "attach",
- "attributes", "audit", "authentication", "authid", "authorization",
- "authorized", "auto", "autofree", "auto_reprepare", "auto_stat_mode",
- "avg", "avoid_execute", "avoid_fact", "avoid_full", "avoid_hash",
- "avoid_index", "avoid_index_sj", "avoid_multi_index", "avoid_nl",
- "avoid_star_join", "avoid_subqf", "based", "before", "begin",
- "between", "bigint", "bigserial", "binary", "bitand", "bitandnot",
- "bitnot", "bitor", "bitxor", "blob", "blobdir", "boolean", "both",
- "bound_impl_pdq", "buffered", "builtin", "by", "byte", "cache", "call",
- "cannothash", "cardinality", "cascade", "case", "cast", "ceil", "char",
- "character", "character_length", "char_length", "check", "class",
- "class_origin", "client", "clob", "clobdir", "close", "cluster",
- "clustersize", "cobol", "codeset", "collation", "collection",
- "column", "columns", "commit", "committed", "commutator", "component",
- "components", "concat", "concurrent", "connect", "connection",
- "connection_name", "connect_by_iscycle", "connect_by_isleaf",
- "connect_by_rootconst", "constraint", "constraints", "constructor",
- "context", "continue", "copy", "cos", "costfunc", "count", "crcols",
- "create", "cross", "current", "current_role", "currval", "cursor",
- "cycle", "database", "datafiles", "dataskip", "date", "datetime",
- "day", "dba", "dbdate", "dbinfo", "dbpassword", "dbsecadm",
- "dbservername", "deallocate", "debug", "debugmode", "debug_env", "dec",
- "decimal", "declare", "decode", "decrypt_binary", "decrypt_char",
- "dec_t", "default", "default_role", "deferred", "deferred_prepare",
- "define", "delay", "delete", "deleting", "delimited", "delimiter",
- "deluxe", "desc", "describe", "descriptor", "detach", "diagnostics",
- "directives", "dirty", "disable", "disabled", "disconnect", "disk",
- "distinct", "distributebinary", "distributesreferences",
- "distributions", "document", "domain", "donotdistribute", "dormant",
- "double", "drop", "dtime_t", "each", "elif", "else", "enabled",
- "encryption", "encrypt_aes", "encrypt_tdes", "end", "enum",
- "environment", "error", "escape", "exception", "exclusive", "exec",
- "execute", "executeanywhere", "exemption", "exists", "exit", "exp",
- "explain", "explicit", "express", "expression", "extdirectives",
- "extend", "extent", "external", "fact", "false", "far", "fetch",
- "file", "filetoblob", "filetoclob", "fillfactor", "filtering", "first",
- "first_rows", "fixchar", "fixed", "float", "floor", "flush", "for",
- "force", "forced", "force_ddl_exec", "foreach", "foreign", "format",
- "format_units", "fortran", "found", "fraction", "fragment",
- "fragments", "free", "from", "full", "function", "general", "get",
- "gethint", "global", "go", "goto", "grant", "greaterthan",
- "greaterthanorequal", "group", "handlesnulls", "hash", "having", "hdr",
- "hex", "high", "hint", "hold", "home", "hour", "idslbacreadarray",
- "idslbacreadset", "idslbacreadtree", "idslbacrules",
- "idslbacwritearray", "idslbacwriteset", "idslbacwritetree",
- "idssecuritylabel", "if", "ifx_auto_reprepare", "ifx_batchedread_table",
- "ifx_int8_t", "ifx_lo_create_spec_t", "ifx_lo_stat_t", "immediate",
- "implicit", "implicit_pdq", "in", "inactive", "increment", "index",
- "indexes", "index_all", "index_sj", "indicator", "informix", "init",
- "initcap", "inline", "inner", "inout", "insert", "inserting", "instead",
- "int", "int8", "integ", "integer", "internal", "internallength",
- "interval", "into", "intrvl_t", "is", "iscanonical", "isolation",
- "item", "iterator", "java", "join", "keep", "key", "label", "labeleq",
- "labelge", "labelglb", "labelgt", "labelle", "labellt", "labellub",
- "labeltostring", "language", "last", "last_day", "leading", "left",
- "length", "lessthan", "lessthanorequal", "let", "level", "like",
- "limit", "list", "listing", "load", "local", "locator", "lock", "locks",
- "locopy", "loc_t", "log", "log10", "logn", "long", "loop", "lotofile",
- "low", "lower", "lpad", "ltrim", "lvarchar", "matched", "matches",
- "max", "maxerrors", "maxlen", "maxvalue", "mdy", "median", "medium",
- "memory", "memory_resident", "merge", "message_length", "message_text",
- "middle", "min", "minute", "minvalue", "mod", "mode", "moderate",
- "modify", "module", "money", "month", "months_between", "mounting",
- "multiset", "multi_index", "name", "nchar", "negator", "new", "next",
- "nextval", "next_day", "no", "nocache", "nocycle", "nomaxvalue",
- "nomigrate", "nominvalue", "none", "non_dim", "non_resident", "noorder",
- "normal", "not", "notemplatearg", "notequal", "null", "nullif",
- "numeric", "numrows", "numtodsinterval", "numtoyminterval", "nvarchar",
- "nvl", "octet_length", "of", "off", "old", "on", "online", "only",
- "opaque", "opclass", "open", "optcompind", "optical", "optimization",
- "option", "or", "order", "ordered", "out", "outer", "output",
- "override", "page", "parallelizable", "parameter", "partition",
- "pascal", "passedbyvalue", "password", "pdqpriority", "percaltl_cos",
- "pipe", "pli", "pload", "policy", "pow", "power", "precision",
- "prepare", "previous", "primary", "prior", "private", "privileges",
- "procedure", "properties", "public", "put", "raise", "range", "raw",
- "read", "real", "recordend", "references", "referencing", "register",
- "rejectfile", "relative", "release", "remainder", "rename",
- "reoptimization", "repeatable", "replace", "replication", "reserve",
- "resolution", "resource", "restart", "restrict", "resume", "retain",
- "retainupdatelocks", "return", "returned_sqlstate", "returning",
- "returns", "reuse", "revoke", "right", "robin", "role", "rollback",
- "rollforward", "root", "round", "routine", "row", "rowid", "rowids",
- "rows", "row_count", "rpad", "rtrim", "rule", "sameas", "samples",
- "sampling", "save", "savepoint", "schema", "scroll", "seclabel_by_comp",
- "seclabel_by_name", "seclabel_to_char", "second", "secondary",
- "section", "secured", "security", "selconst", "select", "selecting",
- "selfunc", "selfuncargs", "sequence", "serial", "serial8",
- "serializable", "serveruuid", "server_name", "session", "set",
- "setsessionauth", "share", "short", "siblings", "signed", "sin",
- "sitename", "size", "skall", "skinhibit", "skip", "skshow",
- "smallfloat", "smallint", "some", "specific", "sql", "sqlcode",
- "sqlcontext", "sqlerror", "sqlstate", "sqlwarning", "sqrt",
- "stability", "stack", "standard", "start", "star_join", "statchange",
- "statement", "static", "statistics", "statlevel", "status", "stdev",
- "step", "stop", "storage", "store", "strategies", "string",
- "stringtolabel", "struct", "style", "subclass_origin", "substr",
- "substring", "sum", "support", "sync", "synonym", "sysdate",
- "sysdbclose", "sysdbopen", "system", "sys_connect_by_path", "table",
- "tables", "tan", "task", "temp", "template", "test", "text", "then",
- "time", "timeout", "to", "today", "to_char", "to_date",
- "to_dsinterval", "to_number", "to_yminterval", "trace", "trailing",
- "transaction", "transition", "tree", "trigger", "triggers", "trim",
- "true", "trunc", "truncate", "trusted", "type", "typedef", "typeid",
- "typename", "typeof", "uid", "uncommitted", "under", "union",
- "unique", "units", "unknown", "unload", "unlock", "unsigned",
- "update", "updating", "upon", "upper", "usage", "use",
- "uselastcommitted", "user", "use_hash", "use_nl", "use_subqf",
- "using", "value", "values", "var", "varchar", "variable", "variance",
- "variant", "varying", "vercols", "view", "violations", "void",
- "volatile", "wait", "warning", "weekday", "when", "whenever", "where",
- "while", "with", "without", "work", "write", "writedown", "writeup",
- "xadatasource", "xid", "xload", "xunload", "year"
- ])
-
-class InfoDateTime(sqltypes.DateTime):
- def bind_processor(self, dialect):
- def process(value):
- if value is not None:
- if value.microsecond:
- value = value.replace(microsecond=0)
- return value
- return process
-
-class InfoTime(sqltypes.Time):
- def bind_processor(self, dialect):
- def process(value):
- if value is not None:
- if value.microsecond:
- value = value.replace(microsecond=0)
- return value
- return process
-
- def result_processor(self, dialect, coltype):
- def process(value):
- if isinstance(value, datetime.datetime):
- return value.time()
- else:
- return value
- return process
-
-colspecs = {
- sqltypes.DateTime : InfoDateTime,
- sqltypes.TIMESTAMP: InfoDateTime,
- sqltypes.Time: InfoTime,
-}
-
-
-ischema_names = {
- 0 : sqltypes.CHAR, # CHAR
- 1 : sqltypes.SMALLINT, # SMALLINT
- 2 : sqltypes.INTEGER, # INT
- 3 : sqltypes.FLOAT, # Float
- 3 : sqltypes.Float, # SmallFloat
- 5 : sqltypes.DECIMAL, # DECIMAL
- 6 : sqltypes.Integer, # Serial
- 7 : sqltypes.DATE, # DATE
- 8 : sqltypes.Numeric, # MONEY
- 10 : sqltypes.DATETIME, # DATETIME
- 11 : sqltypes.LargeBinary, # BYTE
- 12 : sqltypes.TEXT, # TEXT
- 13 : sqltypes.VARCHAR, # VARCHAR
- 15 : sqltypes.NCHAR, # NCHAR
- 16 : sqltypes.NVARCHAR, # NVARCHAR
- 17 : sqltypes.Integer, # INT8
- 18 : sqltypes.Integer, # Serial8
- 43 : sqltypes.String, # LVARCHAR
- -1 : sqltypes.BLOB, # BLOB
- -1 : sqltypes.CLOB, # CLOB
-}
-
-
-class InfoTypeCompiler(compiler.GenericTypeCompiler):
- def visit_DATETIME(self, type_):
- return "DATETIME YEAR TO SECOND"
-
- def visit_TIME(self, type_):
- return "DATETIME HOUR TO SECOND"
-
- def visit_TIMESTAMP(self, type_):
- return "DATETIME YEAR TO SECOND"
-
- def visit_large_binary(self, type_):
- return "BYTE"
-
- def visit_boolean(self, type_):
- return "SMALLINT"
-
-class InfoSQLCompiler(compiler.SQLCompiler):
- def default_from(self):
- return " from systables where tabname = 'systables' "
-
- def get_select_precolumns(self, select):
- s = ""
- if select._offset:
- s += "SKIP %s " % select._offset
- if select._limit:
- s += "FIRST %s " % select._limit
- s += select._distinct and "DISTINCT " or ""
- return s
-
- def visit_select(self, select, asfrom=False, parens=True, **kw):
- text = compiler.SQLCompiler.visit_select(self, select, asfrom, parens, **kw)
- if asfrom and parens and self.dialect.server_version_info < (11,):
- #assuming that 11 version doesn't need this, not tested
- return "table(multiset" + text + ")"
- else:
- return text
-
- def limit_clause(self, select):
- return ""
-
- def visit_function(self, func, **kw):
- if func.name.lower() == 'current_date':
- return "today"
- elif func.name.lower() == 'current_time':
- return "CURRENT HOUR TO SECOND"
- elif func.name.lower() in ('current_timestamp', 'now'):
- return "CURRENT YEAR TO SECOND"
- else:
- return compiler.SQLCompiler.visit_function(self, func, **kw)
-
- def visit_mod(self, binary, **kw):
- return "MOD(%s, %s)" % (self.process(binary.left), self.process(binary.right))
-
-
-class InfoDDLCompiler(compiler.DDLCompiler):
-
- def visit_add_constraint(self, create):
- preparer = self.preparer
- return "ALTER TABLE %s ADD CONSTRAINT %s" % (
- self.preparer.format_table(create.element.table),
- self.process(create.element)
- )
-
- def get_column_specification(self, column, **kw):
- colspec = self.preparer.format_column(column)
- first = None
- if column.primary_key and column.autoincrement:
- try:
- first = [c for c in column.table.primary_key.columns
- if (c.autoincrement and
- isinstance(c.type, sqltypes.Integer) and
- not c.foreign_keys)].pop(0)
- except IndexError:
- pass
-
- if column is first:
- colspec += " SERIAL"
- else:
- colspec += " " + self.dialect.type_compiler.process(column.type)
- default = self.get_column_default_string(column)
- if default is not None:
- colspec += " DEFAULT " + default
-
- if not column.nullable:
- colspec += " NOT NULL"
-
- return colspec
-
- def get_column_default_string(self, column):
- if (isinstance(column.server_default, schema.DefaultClause) and
- isinstance(column.server_default.arg, basestring)):
- if isinstance(column.type, (sqltypes.Integer, sqltypes.Numeric)):
- return self.sql_compiler.process(text(column.server_default.arg))
-
- return super(InfoDDLCompiler, self).get_column_default_string(column)
-
- ### Informix wants the constraint name at the end, hence this ist c&p from sql/compiler.py
- def visit_primary_key_constraint(self, constraint):
- if len(constraint) == 0:
- return ''
- text = "PRIMARY KEY "
- text += "(%s)" % ', '.join(self.preparer.quote(c.name, c.quote)
- for c in constraint)
- text += self.define_constraint_deferrability(constraint)
-
- if constraint.name is not None:
- text += " CONSTRAINT %s" % self.preparer.format_constraint(constraint)
- return text
-
- def visit_foreign_key_constraint(self, constraint):
- preparer = self.dialect.identifier_preparer
- remote_table = list(constraint._elements.values())[0].column.table
- text = "FOREIGN KEY (%s) REFERENCES %s (%s)" % (
- ', '.join(preparer.quote(f.parent.name, f.parent.quote)
- for f in constraint._elements.values()),
- preparer.format_table(remote_table),
- ', '.join(preparer.quote(f.column.name, f.column.quote)
- for f in constraint._elements.values())
- )
- text += self.define_constraint_cascades(constraint)
- text += self.define_constraint_deferrability(constraint)
-
- if constraint.name is not None:
- text += " CONSTRAINT %s " % \
- preparer.format_constraint(constraint)
- return text
-
- def visit_unique_constraint(self, constraint):
- text = "UNIQUE (%s)" % (', '.join(self.preparer.quote(c.name, c.quote) for c in constraint))
- text += self.define_constraint_deferrability(constraint)
-
- if constraint.name is not None:
- text += "CONSTRAINT %s " % self.preparer.format_constraint(constraint)
- return text
-
-class InformixIdentifierPreparer(compiler.IdentifierPreparer):
-
- reserved_words = RESERVED_WORDS
-
-
-class InformixDialect(default.DefaultDialect):
- name = 'informix'
-
- max_identifier_length = 128 # adjusts at runtime based on server version
-
- type_compiler = InfoTypeCompiler
- statement_compiler = InfoSQLCompiler
- ddl_compiler = InfoDDLCompiler
- colspecs = colspecs
- ischema_names = ischema_names
- preparer = InformixIdentifierPreparer
- default_paramstyle = 'qmark'
-
- def __init__(self, has_transactions=True, *args, **kwargs):
- self.has_transactions = has_transactions
- default.DefaultDialect.__init__(self, *args, **kwargs)
-
- def initialize(self, connection):
- super(InformixDialect, self).initialize(connection)
-
- # http://www.querix.com/support/knowledge-base/error_number_message/error_200
- if self.server_version_info < (9, 2):
- self.max_identifier_length = 18
- else:
- self.max_identifier_length = 128
-
- def do_begin(self, connection):
- cu = connection.cursor()
- cu.execute('SET LOCK MODE TO WAIT')
- if self.has_transactions:
- cu.execute('SET ISOLATION TO REPEATABLE READ')
-
- def do_commit(self, connection):
- if self.has_transactions:
- connection.commit()
-
- def do_rollback(self, connection):
- if self.has_transactions:
- connection.rollback()
-
- def _get_table_names(self, connection, schema, type, **kw):
- schema = schema or self.default_schema_name
- s = "select tabname, owner from systables where owner=? and tabtype=?"
- return [row[0] for row in connection.execute(s, schema, type)]
-
- @reflection.cache
- def get_table_names(self, connection, schema=None, **kw):
- return self._get_table_names(connection, schema, 'T', **kw)
-
- @reflection.cache
- def get_view_names(self, connection, schema=None, **kw):
- return self._get_table_names(connection, schema, 'V', **kw)
-
- @reflection.cache
- def get_schema_names(self, connection, **kw):
- s = "select owner from systables"
- return [row[0] for row in connection.execute(s)]
-
- def has_table(self, connection, table_name, schema=None):
- schema = schema or self.default_schema_name
- cursor = connection.execute(
- """select tabname from systables where tabname=? and owner=?""",
- table_name, schema)
- return cursor.first() is not None
-
- @reflection.cache
- def get_columns(self, connection, table_name, schema=None, **kw):
- schema = schema or self.default_schema_name
- c = connection.execute(
- """select colname, coltype, collength, t3.default, t1.colno from
- syscolumns as t1 , systables as t2 , OUTER sysdefaults as t3
- where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=?
- and t3.tabid = t2.tabid and t3.colno = t1.colno
- order by t1.colno""", table_name, schema)
-
- primary_cols = self.get_primary_keys(connection, table_name, schema, **kw)
-
- columns = []
- rows = c.fetchall()
- for name, colattr, collength, default, colno in rows:
- name = name.lower()
-
- autoincrement = False
- primary_key = False
-
- if name in primary_cols:
- primary_key = True
-
- # in 7.31, coltype = 0x000
- # ^^-- column type
- # ^-- 1 not null, 0 null
- not_nullable, coltype = divmod(colattr, 256)
- if coltype not in (0, 13) and default:
- default = default.split()[-1]
-
- if coltype == 6: # Serial, mark as autoincrement
- autoincrement = True
-
- if coltype == 0 or coltype == 13: # char, varchar
- coltype = ischema_names[coltype](collength)
- if default:
- default = "'%s'" % default
- elif coltype == 5: # decimal
- precision, scale = (collength & 0xFF00) >> 8, collength & 0xFF
- if scale == 255:
- scale = 0
- coltype = sqltypes.Numeric(precision, scale)
- else:
- try:
- coltype = ischema_names[coltype]
- except KeyError:
- util.warn("Did not recognize type '%s' of column '%s'" %
- (coltype, name))
- coltype = sqltypes.NULLTYPE
-
- column_info = dict(name=name, type=coltype, nullable=not not_nullable,
- default=default, autoincrement=autoincrement,
- primary_key=primary_key)
- columns.append(column_info)
- return columns
-
- @reflection.cache
- def get_foreign_keys(self, connection, table_name, schema=None, **kw):
- schema_sel = schema or self.default_schema_name
- c = connection.execute(
- """select t1.constrname as cons_name,
- t4.colname as local_column, t7.tabname as remote_table,
- t6.colname as remote_column, t7.owner as remote_owner
- from sysconstraints as t1 , systables as t2 ,
- sysindexes as t3 , syscolumns as t4 ,
- sysreferences as t5 , syscolumns as t6 , systables as t7 ,
- sysconstraints as t8 , sysindexes as t9
- where t1.tabid = t2.tabid and t2.tabname=? and t2.owner=? and t1.constrtype = 'R'
- and t3.tabid = t2.tabid and t3.idxname = t1.idxname
- and t4.tabid = t2.tabid and t4.colno in (t3.part1, t3.part2, t3.part3,
- t3.part4, t3.part5, t3.part6, t3.part7, t3.part8, t3.part9, t3.part10,
- t3.part11, t3.part11, t3.part12, t3.part13, t3.part4, t3.part15, t3.part16)
- and t5.constrid = t1.constrid and t8.constrid = t5.primary
- and t6.tabid = t5.ptabid and t6.colno in (t9.part1, t9.part2, t9.part3,
- t9.part4, t9.part5, t9.part6, t9.part7, t9.part8, t9.part9, t9.part10,
- t9.part11, t9.part11, t9.part12, t9.part13, t9.part4, t9.part15, t9.part16) and t9.idxname =
- t8.idxname
- and t7.tabid = t5.ptabid""", table_name, schema_sel)
-
-
- def fkey_rec():
- return {
- 'name' : None,
- 'constrained_columns' : [],
- 'referred_schema' : None,
- 'referred_table' : None,
- 'referred_columns' : []
- }
-
- fkeys = util.defaultdict(fkey_rec)
-
- rows = c.fetchall()
- for cons_name, local_column, \
- remote_table, remote_column, remote_owner in rows:
-
- rec = fkeys[cons_name]
- rec['name'] = cons_name
- local_cols, remote_cols = \
- rec['constrained_columns'], rec['referred_columns']
-
- if not rec['referred_table']:
- rec['referred_table'] = remote_table
- if schema is not None:
- rec['referred_schema'] = remote_owner
-
- if local_column not in local_cols:
- local_cols.append(local_column)
- if remote_column not in remote_cols:
- remote_cols.append(remote_column)
-
- return fkeys.values()
-
- @reflection.cache
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
- schema = schema or self.default_schema_name
-
- # Select the column positions from sysindexes for sysconstraints
- data = connection.execute(
- """select t2.*
- from systables as t1, sysindexes as t2, sysconstraints as t3
- where t1.tabid=t2.tabid and t1.tabname=? and t1.owner=?
- and t2.idxname=t3.idxname and t3.constrtype='P'""",
- table_name, schema
- ).fetchall()
-
- colpositions = set()
-
- for row in data:
- colpos = set([getattr(row, 'part%d' % x) for x in range(1,16)])
- colpositions |= colpos
-
- if not len(colpositions):
- return []
-
- # Select the column names using the columnpositions
- # TODO: Maybe cache a bit of those col infos (eg select all colnames for one table)
- place_holder = ','.join('?'*len(colpositions))
- c = connection.execute(
- """select t1.colname
- from syscolumns as t1, systables as t2
- where t2.tabname=? and t1.tabid = t2.tabid and
- t1.colno in (%s)""" % place_holder,
- table_name, *colpositions
- ).fetchall()
-
- return reduce(lambda x,y: list(x)+list(y), c, [])
-
- @reflection.cache
- def get_indexes(self, connection, table_name, schema, **kw):
- # TODO: schema...
- c = connection.execute(
- """select t1.*
- from sysindexes as t1 , systables as t2
- where t1.tabid = t2.tabid and t2.tabname=?""",
- table_name)
-
- indexes = []
- for row in c.fetchall():
- colnames = [getattr(row, 'part%d' % x) for x in range(1,16)]
- colnames = [x for x in colnames if x]
- place_holder = ','.join('?'*len(colnames))
- c = connection.execute(
- """select t1.colname
- from syscolumns as t1, systables as t2
- where t2.tabname=? and t1.tabid = t2.tabid and
- t1.colno in (%s)""" % place_holder,
- table_name, *colnames
- ).fetchall()
- c = reduce(lambda x,y: list(x)+list(y), c, [])
- indexes.append({
- 'name': row.idxname,
- 'unique': row.idxtype.lower() == 'u',
- 'column_names': c
- })
- return indexes
-
- @reflection.cache
- def get_view_definition(self, connection, view_name, schema=None, **kw):
- schema = schema or self.default_schema_name
- c = connection.execute(
- """select t1.viewtext
- from sysviews as t1 , systables as t2
- where t1.tabid=t2.tabid and t2.tabname=?
- and t2.owner=? order by seqno""",
- view_name, schema).fetchall()
-
- return ''.join([row[0] for row in c])
-
- def _get_default_schema_name(self, connection):
- return connection.execute('select CURRENT_ROLE from systables').scalar()
diff --git a/libs/sqlalchemy/dialects/informix/informixdb.py b/libs/sqlalchemy/dialects/informix/informixdb.py
deleted file mode 100644
index 8b543467..00000000
--- a/libs/sqlalchemy/dialects/informix/informixdb.py
+++ /dev/null
@@ -1,73 +0,0 @@
-# informix/informixdb.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"""
-Support for the informixdb DBAPI.
-
-informixdb is available at:
-
- http://informixdb.sourceforge.net/
-
-Connecting
-^^^^^^^^^^
-
-Sample informix connection::
-
- engine = create_engine('informix+informixdb://user:password@host/dbname')
-
-"""
-
-import re
-
-from sqlalchemy.dialects.informix.base import InformixDialect
-from sqlalchemy.engine import default
-
-VERSION_RE = re.compile(r'(\d+)\.(\d+)(.+\d+)')
-
-class InformixExecutionContext_informixdb(default.DefaultExecutionContext):
- def post_exec(self):
- if self.isinsert:
- self._lastrowid = self.cursor.sqlerrd[1]
-
- def get_lastrowid(self):
- return self._lastrowid
-
-
-class InformixDialect_informixdb(InformixDialect):
- driver = 'informixdb'
- execution_ctx_cls = InformixExecutionContext_informixdb
-
- @classmethod
- def dbapi(cls):
- return __import__('informixdb')
-
- def create_connect_args(self, url):
- if url.host:
- dsn = '%s@%s' % (url.database, url.host)
- else:
- dsn = url.database
-
- if url.username:
- opt = {'user': url.username, 'password': url.password}
- else:
- opt = {}
-
- return ([dsn], opt)
-
- def _get_server_version_info(self, connection):
- # http://informixdb.sourceforge.net/manual.html#inspecting-version-numbers
- v = VERSION_RE.split(connection.connection.dbms_version)
- return (int(v[1]), int(v[2]), v[3])
-
- def is_disconnect(self, e, connection, cursor):
- if isinstance(e, self.dbapi.OperationalError):
- return 'closed the connection' in str(e) \
- or 'connection not open' in str(e)
- else:
- return False
-
-
-dialect = InformixDialect_informixdb
diff --git a/libs/sqlalchemy/dialects/maxdb/__init__.py b/libs/sqlalchemy/dialects/maxdb/__init__.py
deleted file mode 100644
index 9d1d6418..00000000
--- a/libs/sqlalchemy/dialects/maxdb/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# maxdb/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-from sqlalchemy.dialects.maxdb import base, sapdb
-
-base.dialect = sapdb.dialect
\ No newline at end of file
diff --git a/libs/sqlalchemy/dialects/maxdb/base.py b/libs/sqlalchemy/dialects/maxdb/base.py
deleted file mode 100644
index 68ae630e..00000000
--- a/libs/sqlalchemy/dialects/maxdb/base.py
+++ /dev/null
@@ -1,1117 +0,0 @@
-# maxdb/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-"""Support for the MaxDB database.
-
-.. note::
-
- The MaxDB dialect is **non-functional as of SQLAlchemy 0.6**,
- pending development efforts to bring it up-to-date.
-
-Overview
---------
-
-The ``maxdb`` dialect is **experimental** and has only been tested on 7.6.03.007
-and 7.6.00.037. Of these, **only 7.6.03.007 will work** with SQLAlchemy's ORM.
-The earlier version has severe ``LEFT JOIN`` limitations and will return
-incorrect results from even very simple ORM queries.
-
-Only the native Python DB-API is currently supported. ODBC driver support
-is a future enhancement.
-
-Connecting
-----------
-
-The username is case-sensitive. If you usually connect to the
-database with sqlcli and other tools in lower case, you likely need to
-use upper case for DB-API.
-
-Implementation Notes
---------------------
-
-With the 7.6.00.37 driver and Python 2.5, it seems that all DB-API
-generated exceptions are broken and can cause Python to crash.
-
-For 'somecol.in_([])' to work, the IN operator's generation must be changed
-to cast 'NULL' to a numeric, i.e. NUM(NULL). The DB-API doesn't accept a
-bind parameter there, so that particular generation must inline the NULL value,
-which depends on [ticket:807].
-
-The DB-API is very picky about where bind params may be used in queries.
-
-Bind params for some functions (e.g. MOD) need type information supplied.
-The dialect does not yet do this automatically.
-
-Max will occasionally throw up 'bad sql, compile again' exceptions for
-perfectly valid SQL. The dialect does not currently handle these, more
-research is needed.
-
-MaxDB 7.5 and Sap DB <= 7.4 reportedly do not support schemas. A very
-slightly different version of this dialect would be required to support
-those versions, and can easily be added if there is demand. Some other
-required components such as an Max-aware 'old oracle style' join compiler
-(thetas with (+) outer indicators) are already done and available for
-integration- email the devel list if you're interested in working on
-this.
-
-Versions tested: 7.6.03.07 and 7.6.00.37, native Python DB-API
-
-* MaxDB has severe limitations on OUTER JOINs, which are essential to ORM
- eager loading. And rather than raise an error if a SELECT can't be serviced,
- the database simply returns incorrect results.
-* Version 7.6.03.07 seems to JOIN properly, however the docs do not show the
- OUTER restrictions being lifted (as of this writing), and no changelog is
- available to confirm either. If you are using a different server version and
- your tasks require the ORM or any semi-advanced SQL through the SQL layer,
- running the SQLAlchemy test suite against your database is HIGHLY
- recommended before you begin.
-* Version 7.6.00.37 is LHS/RHS sensitive in `FROM lhs LEFT OUTER JOIN rhs ON
- lhs.col=rhs.col` vs `rhs.col=lhs.col`!
-* Version 7.6.00.37 is confused by `SELECT DISTINCT col as alias FROM t ORDER
- BY col` - these aliased, DISTINCT, ordered queries need to be re-written to
- order by the alias name.
-* Version 7.6.x supports creating a SAVEPOINT but not its RELEASE.
-* MaxDB supports autoincrement-style columns (DEFAULT SERIAL) and independent
- sequences. When including a DEFAULT SERIAL column in an insert, 0 needs to
- be inserted rather than NULL to generate a value.
-* MaxDB supports ANSI and "old Oracle style" theta joins with (+) outer join
- indicators.
-* The SQLAlchemy dialect is schema-aware and probably won't function correctly
- on server versions (pre-7.6?). Support for schema-less server versions could
- be added if there's call.
-* ORDER BY is not supported in subqueries. LIMIT is not supported in
- subqueries. In 7.6.00.37, TOP does work in subqueries, but without limit not
- so useful. OFFSET does not work in 7.6 despite being in the docs. Row number
- tricks in WHERE via ROWNO may be possible but it only seems to allow
- less-than comparison!
-* Version 7.6.03.07 can't LIMIT if a derived table is in FROM: `SELECT * FROM
- (SELECT * FROM a) LIMIT 2`
-* MaxDB does not support sql's CAST and can only usefullly cast two types.
- There isn't much implicit type conversion, so be precise when creating
- `PassiveDefaults` in DDL generation: `'3'` and `3` aren't the same.
-
-sapdb.dbapi
-^^^^^^^^^^^
-
-* As of 2007-10-22 the Python 2.4 and 2.5 compatible versions of the DB-API
- are no longer available. A forum posting at SAP states that the Python
- driver will be available again "in the future". The last release from MySQL
- AB works if you can find it.
-* sequence.NEXTVAL skips every other value!
-* No rowcount for executemany()
-* If an INSERT into a table with a DEFAULT SERIAL column inserts the results
- of a function `INSERT INTO t VALUES (LENGTH('foo'))`, the cursor won't have
- the serial id. It needs to be manually yanked from tablename.CURRVAL.
-* Super-duper picky about where bind params can be placed. Not smart about
- converting Python types for some functions, such as `MOD(5, ?)`.
-* LONG (text, binary) values in result sets are read-once. The dialect uses a
- caching RowProxy when these types are present.
-* Connection objects seem like they want to be either `close()`d or garbage
- collected, but not both. There's a warning issued but it seems harmless.
-
-
-"""
-import datetime, itertools, re
-
-from sqlalchemy import exc, schema, sql, util, processors
-from sqlalchemy.sql import operators as sql_operators, expression as sql_expr
-from sqlalchemy.sql import compiler, visitors
-from sqlalchemy.engine import base as engine_base, default, reflection
-from sqlalchemy import types as sqltypes
-
-
-class _StringType(sqltypes.String):
- _type = None
-
- def __init__(self, length=None, encoding=None, **kw):
- super(_StringType, self).__init__(length=length, **kw)
- self.encoding = encoding
-
- def bind_processor(self, dialect):
- if self.encoding == 'unicode':
- return None
- else:
- def process(value):
- if isinstance(value, unicode):
- return value.encode(dialect.encoding)
- else:
- return value
- return process
-
- def result_processor(self, dialect, coltype):
- #XXX: this code is probably very slow and one should try (if at all
- # possible) to determine the correct code path on a per-connection
- # basis (ie, here in result_processor, instead of inside the processor
- # function itself) and probably also use a few generic
- # processors, or possibly per query (though there is no mechanism
- # for that yet).
- def process(value):
- while True:
- if value is None:
- return None
- elif isinstance(value, unicode):
- return value
- elif isinstance(value, str):
- if self.convert_unicode or dialect.convert_unicode:
- return value.decode(dialect.encoding)
- else:
- return value
- elif hasattr(value, 'read'):
- # some sort of LONG, snarf and retry
- value = value.read(value.remainingLength())
- continue
- else:
- # unexpected type, return as-is
- return value
- return process
-
-
-class MaxString(_StringType):
- _type = 'VARCHAR'
-
-
-class MaxUnicode(_StringType):
- _type = 'VARCHAR'
-
- def __init__(self, length=None, **kw):
- kw['encoding'] = 'unicode'
- super(MaxUnicode, self).__init__(length=length, **kw)
-
-
-class MaxChar(_StringType):
- _type = 'CHAR'
-
-
-class MaxText(_StringType):
- _type = 'LONG'
-
- def __init__(self, length=None, **kw):
- super(MaxText, self).__init__(length, **kw)
-
- def get_col_spec(self):
- spec = 'LONG'
- if self.encoding is not None:
- spec = ' '.join((spec, self.encoding))
- elif self.convert_unicode:
- spec = ' '.join((spec, 'UNICODE'))
-
- return spec
-
-
-class MaxNumeric(sqltypes.Numeric):
- """The FIXED (also NUMERIC, DECIMAL) data type."""
-
- def __init__(self, precision=None, scale=None, **kw):
- kw.setdefault('asdecimal', True)
- super(MaxNumeric, self).__init__(scale=scale, precision=precision,
- **kw)
-
- def bind_processor(self, dialect):
- return None
-
-
-class MaxTimestamp(sqltypes.DateTime):
- def bind_processor(self, dialect):
- def process(value):
- if value is None:
- return None
- elif isinstance(value, basestring):
- return value
- elif dialect.datetimeformat == 'internal':
- ms = getattr(value, 'microsecond', 0)
- return value.strftime("%Y%m%d%H%M%S" + ("%06u" % ms))
- elif dialect.datetimeformat == 'iso':
- ms = getattr(value, 'microsecond', 0)
- return value.strftime("%Y-%m-%d %H:%M:%S." + ("%06u" % ms))
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." % (
- dialect.datetimeformat,))
- return process
-
- def result_processor(self, dialect, coltype):
- if dialect.datetimeformat == 'internal':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.datetime(
- *[int(v)
- for v in (value[0:4], value[4:6], value[6:8],
- value[8:10], value[10:12], value[12:14],
- value[14:])])
- elif dialect.datetimeformat == 'iso':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.datetime(
- *[int(v)
- for v in (value[0:4], value[5:7], value[8:10],
- value[11:13], value[14:16], value[17:19],
- value[20:])])
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." %
- dialect.datetimeformat)
- return process
-
-
-class MaxDate(sqltypes.Date):
- def bind_processor(self, dialect):
- def process(value):
- if value is None:
- return None
- elif isinstance(value, basestring):
- return value
- elif dialect.datetimeformat == 'internal':
- return value.strftime("%Y%m%d")
- elif dialect.datetimeformat == 'iso':
- return value.strftime("%Y-%m-%d")
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." % (
- dialect.datetimeformat,))
- return process
-
- def result_processor(self, dialect, coltype):
- if dialect.datetimeformat == 'internal':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.date(int(value[0:4]), int(value[4:6]),
- int(value[6:8]))
- elif dialect.datetimeformat == 'iso':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.date(int(value[0:4]), int(value[5:7]),
- int(value[8:10]))
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." %
- dialect.datetimeformat)
- return process
-
-
-class MaxTime(sqltypes.Time):
- def bind_processor(self, dialect):
- def process(value):
- if value is None:
- return None
- elif isinstance(value, basestring):
- return value
- elif dialect.datetimeformat == 'internal':
- return value.strftime("%H%M%S")
- elif dialect.datetimeformat == 'iso':
- return value.strftime("%H-%M-%S")
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." % (
- dialect.datetimeformat,))
- return process
-
- def result_processor(self, dialect, coltype):
- if dialect.datetimeformat == 'internal':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.time(int(value[0:4]), int(value[4:6]),
- int(value[6:8]))
- elif dialect.datetimeformat == 'iso':
- def process(value):
- if value is None:
- return None
- else:
- return datetime.time(int(value[0:4]), int(value[5:7]),
- int(value[8:10]))
- else:
- raise exc.InvalidRequestError(
- "datetimeformat '%s' is not supported." %
- dialect.datetimeformat)
- return process
-
-
-class MaxBlob(sqltypes.LargeBinary):
- def bind_processor(self, dialect):
- return processors.to_str
-
- def result_processor(self, dialect, coltype):
- def process(value):
- if value is None:
- return None
- else:
- return value.read(value.remainingLength())
- return process
-
-class MaxDBTypeCompiler(compiler.GenericTypeCompiler):
- def _string_spec(self, string_spec, type_):
- if type_.length is None:
- spec = 'LONG'
- else:
- spec = '%s(%s)' % (string_spec, type_.length)
-
- if getattr(type_, 'encoding'):
- spec = ' '.join([spec, getattr(type_, 'encoding').upper()])
- return spec
-
- def visit_text(self, type_):
- spec = 'LONG'
- if getattr(type_, 'encoding', None):
- spec = ' '.join((spec, type_.encoding))
- elif type_.convert_unicode:
- spec = ' '.join((spec, 'UNICODE'))
-
- return spec
-
- def visit_char(self, type_):
- return self._string_spec("CHAR", type_)
-
- def visit_string(self, type_):
- return self._string_spec("VARCHAR", type_)
-
- def visit_large_binary(self, type_):
- return "LONG BYTE"
-
- def visit_numeric(self, type_):
- if type_.scale and type_.precision:
- return 'FIXED(%s, %s)' % (type_.precision, type_.scale)
- elif type_.precision:
- return 'FIXED(%s)' % type_.precision
- else:
- return 'INTEGER'
-
- def visit_BOOLEAN(self, type_):
- return "BOOLEAN"
-
-colspecs = {
- sqltypes.Numeric: MaxNumeric,
- sqltypes.DateTime: MaxTimestamp,
- sqltypes.Date: MaxDate,
- sqltypes.Time: MaxTime,
- sqltypes.String: MaxString,
- sqltypes.Unicode:MaxUnicode,
- sqltypes.LargeBinary: MaxBlob,
- sqltypes.Text: MaxText,
- sqltypes.CHAR: MaxChar,
- sqltypes.TIMESTAMP: MaxTimestamp,
- sqltypes.BLOB: MaxBlob,
- sqltypes.Unicode: MaxUnicode,
- }
-
-ischema_names = {
- 'boolean': sqltypes.BOOLEAN,
- 'char': sqltypes.CHAR,
- 'character': sqltypes.CHAR,
- 'date': sqltypes.DATE,
- 'fixed': sqltypes.Numeric,
- 'float': sqltypes.FLOAT,
- 'int': sqltypes.INT,
- 'integer': sqltypes.INT,
- 'long binary': sqltypes.BLOB,
- 'long unicode': sqltypes.Text,
- 'long': sqltypes.Text,
- 'long': sqltypes.Text,
- 'smallint': sqltypes.SmallInteger,
- 'time': sqltypes.Time,
- 'timestamp': sqltypes.TIMESTAMP,
- 'varchar': sqltypes.VARCHAR,
- }
-
-# TODO: migrate this to sapdb.py
-class MaxDBExecutionContext(default.DefaultExecutionContext):
- def post_exec(self):
- # DB-API bug: if there were any functions as values,
- # then do another select and pull CURRVAL from the
- # autoincrement column's implicit sequence... ugh
- if self.compiled.isinsert and not self.executemany:
- table = self.compiled.statement.table
- index, serial_col = _autoserial_column(table)
-
- if serial_col and (not self.compiled._safeserial or
- not(self._last_inserted_ids) or
- self._last_inserted_ids[index] in (None, 0)):
- if table.schema:
- sql = "SELECT %s.CURRVAL FROM DUAL" % (
- self.compiled.preparer.format_table(table))
- else:
- sql = "SELECT CURRENT_SCHEMA.%s.CURRVAL FROM DUAL" % (
- self.compiled.preparer.format_table(table))
-
- rs = self.cursor.execute(sql)
- id = rs.fetchone()[0]
-
- if not self._last_inserted_ids:
- # This shouldn't ever be > 1? Right?
- self._last_inserted_ids = \
- [None] * len(table.primary_key.columns)
- self._last_inserted_ids[index] = id
-
- super(MaxDBExecutionContext, self).post_exec()
-
- def get_result_proxy(self):
- if self.cursor.description is not None:
- for column in self.cursor.description:
- if column[1] in ('Long Binary', 'Long', 'Long Unicode'):
- return MaxDBResultProxy(self)
- return engine_base.ResultProxy(self)
-
- @property
- def rowcount(self):
- if hasattr(self, '_rowcount'):
- return self._rowcount
- else:
- return self.cursor.rowcount
-
- def fire_sequence(self, seq):
- if seq.optional:
- return None
- return self._execute_scalar("SELECT %s.NEXTVAL FROM DUAL" % (
- self.dialect.identifier_preparer.format_sequence(seq)))
-
-class MaxDBCachedColumnRow(engine_base.RowProxy):
- """A RowProxy that only runs result_processors once per column."""
-
- def __init__(self, parent, row):
- super(MaxDBCachedColumnRow, self).__init__(parent, row)
- self.columns = {}
- self._row = row
- self._parent = parent
-
- def _get_col(self, key):
- if key not in self.columns:
- self.columns[key] = self._parent._get_col(self._row, key)
- return self.columns[key]
-
- def __iter__(self):
- for i in xrange(len(self._row)):
- yield self._get_col(i)
-
- def __repr__(self):
- return repr(list(self))
-
- def __eq__(self, other):
- return ((other is self) or
- (other == tuple([self._get_col(key)
- for key in xrange(len(self._row))])))
- def __getitem__(self, key):
- if isinstance(key, slice):
- indices = key.indices(len(self._row))
- return tuple([self._get_col(i) for i in xrange(*indices)])
- else:
- return self._get_col(key)
-
- def __getattr__(self, name):
- try:
- return self._get_col(name)
- except KeyError:
- raise AttributeError(name)
-
-
-class MaxDBResultProxy(engine_base.ResultProxy):
- _process_row = MaxDBCachedColumnRow
-
-class MaxDBCompiler(compiler.SQLCompiler):
-
- function_conversion = {
- 'CURRENT_DATE': 'DATE',
- 'CURRENT_TIME': 'TIME',
- 'CURRENT_TIMESTAMP': 'TIMESTAMP',
- }
-
- # These functions must be written without parens when called with no
- # parameters. e.g. 'SELECT DATE FROM DUAL' not 'SELECT DATE() FROM DUAL'
- bare_functions = set([
- 'CURRENT_SCHEMA', 'DATE', 'FALSE', 'SYSDBA', 'TIME', 'TIMESTAMP',
- 'TIMEZONE', 'TRANSACTION', 'TRUE', 'USER', 'UID', 'USERGROUP',
- 'UTCDATE', 'UTCDIFF'])
-
- def visit_mod(self, binary, **kw):
- return "mod(%s, %s)" % \
- (self.process(binary.left), self.process(binary.right))
-
- def default_from(self):
- return ' FROM DUAL'
-
- def for_update_clause(self, select):
- clause = select.for_update
- if clause is True:
- return " WITH LOCK EXCLUSIVE"
- elif clause is None:
- return ""
- elif clause == "read":
- return " WITH LOCK"
- elif clause == "ignore":
- return " WITH LOCK (IGNORE) EXCLUSIVE"
- elif clause == "nowait":
- return " WITH LOCK (NOWAIT) EXCLUSIVE"
- elif isinstance(clause, basestring):
- return " WITH LOCK %s" % clause.upper()
- elif not clause:
- return ""
- else:
- return " WITH LOCK EXCLUSIVE"
-
- def function_argspec(self, fn, **kw):
- if fn.name.upper() in self.bare_functions:
- return ""
- elif len(fn.clauses) > 0:
- return compiler.SQLCompiler.function_argspec(self, fn, **kw)
- else:
- return ""
-
- def visit_function(self, fn, **kw):
- transform = self.function_conversion.get(fn.name.upper(), None)
- if transform:
- fn = fn._clone()
- fn.name = transform
- return super(MaxDBCompiler, self).visit_function(fn, **kw)
-
- def visit_cast(self, cast, **kwargs):
- # MaxDB only supports casts * to NUMERIC, * to VARCHAR or
- # date/time to VARCHAR. Casts of LONGs will fail.
- if isinstance(cast.type, (sqltypes.Integer, sqltypes.Numeric)):
- return "NUM(%s)" % self.process(cast.clause)
- elif isinstance(cast.type, sqltypes.String):
- return "CHR(%s)" % self.process(cast.clause)
- else:
- return self.process(cast.clause)
-
- def visit_sequence(self, sequence):
- if sequence.optional:
- return None
- else:
- return (
- self.dialect.identifier_preparer.format_sequence(sequence) +
- ".NEXTVAL")
-
- class ColumnSnagger(visitors.ClauseVisitor):
- def __init__(self):
- self.count = 0
- self.column = None
- def visit_column(self, column):
- self.column = column
- self.count += 1
-
- def _find_labeled_columns(self, columns, use_labels=False):
- labels = {}
- for column in columns:
- if isinstance(column, basestring):
- continue
- snagger = self.ColumnSnagger()
- snagger.traverse(column)
- if snagger.count == 1:
- if isinstance(column, sql_expr._Label):
- labels[unicode(snagger.column)] = column.name
- elif use_labels:
- labels[unicode(snagger.column)] = column._label
-
- return labels
-
- def order_by_clause(self, select, **kw):
- order_by = self.process(select._order_by_clause, **kw)
-
- # ORDER BY clauses in DISTINCT queries must reference aliased
- # inner columns by alias name, not true column name.
- if order_by and getattr(select, '_distinct', False):
- labels = self._find_labeled_columns(select.inner_columns,
- select.use_labels)
- if labels:
- for needs_alias in labels.keys():
- r = re.compile(r'(^| )(%s)(,| |$)' %
- re.escape(needs_alias))
- order_by = r.sub((r'\1%s\3' % labels[needs_alias]),
- order_by)
-
- # No ORDER BY in subqueries.
- if order_by:
- if self.is_subquery():
- # It's safe to simply drop the ORDER BY if there is no
- # LIMIT. Right? Other dialects seem to get away with
- # dropping order.
- if select._limit:
- raise exc.CompileError(
- "MaxDB does not support ORDER BY in subqueries")
- else:
- return ""
- return " ORDER BY " + order_by
- else:
- return ""
-
- def get_select_precolumns(self, select):
- # Convert a subquery's LIMIT to TOP
- sql = select._distinct and 'DISTINCT ' or ''
- if self.is_subquery() and select._limit:
- if select._offset:
- raise exc.InvalidRequestError(
- 'MaxDB does not support LIMIT with an offset.')
- sql += 'TOP %s ' % select._limit
- return sql
-
- def limit_clause(self, select):
- # The docs say offsets are supported with LIMIT. But they're not.
- # TODO: maybe emulate by adding a ROWNO/ROWNUM predicate?
- # TODO: does MaxDB support bind params for LIMIT / TOP ?
- if self.is_subquery():
- # sub queries need TOP
- return ''
- elif select._offset:
- raise exc.InvalidRequestError(
- 'MaxDB does not support LIMIT with an offset.')
- else:
- return ' \n LIMIT %s' % (select._limit,)
-
- def visit_insert(self, insert):
- self.isinsert = True
- self._safeserial = True
-
- colparams = self._get_colparams(insert)
- for value in (insert.parameters or {}).itervalues():
- if isinstance(value, sql_expr.Function):
- self._safeserial = False
- break
-
- return ''.join(('INSERT INTO ',
- self.preparer.format_table(insert.table),
- ' (',
- ', '.join([self.preparer.format_column(c[0])
- for c in colparams]),
- ') VALUES (',
- ', '.join([c[1] for c in colparams]),
- ')'))
-
-
-class MaxDBIdentifierPreparer(compiler.IdentifierPreparer):
- reserved_words = set([
- 'abs', 'absolute', 'acos', 'adddate', 'addtime', 'all', 'alpha',
- 'alter', 'any', 'ascii', 'asin', 'atan', 'atan2', 'avg', 'binary',
- 'bit', 'boolean', 'byte', 'case', 'ceil', 'ceiling', 'char',
- 'character', 'check', 'chr', 'column', 'concat', 'constraint', 'cos',
- 'cosh', 'cot', 'count', 'cross', 'curdate', 'current', 'curtime',
- 'database', 'date', 'datediff', 'day', 'dayname', 'dayofmonth',
- 'dayofweek', 'dayofyear', 'dec', 'decimal', 'decode', 'default',
- 'degrees', 'delete', 'digits', 'distinct', 'double', 'except',
- 'exists', 'exp', 'expand', 'first', 'fixed', 'float', 'floor', 'for',
- 'from', 'full', 'get_objectname', 'get_schema', 'graphic', 'greatest',
- 'group', 'having', 'hex', 'hextoraw', 'hour', 'ifnull', 'ignore',
- 'index', 'initcap', 'inner', 'insert', 'int', 'integer', 'internal',
- 'intersect', 'into', 'join', 'key', 'last', 'lcase', 'least', 'left',
- 'length', 'lfill', 'list', 'ln', 'locate', 'log', 'log10', 'long',
- 'longfile', 'lower', 'lpad', 'ltrim', 'makedate', 'maketime',
- 'mapchar', 'max', 'mbcs', 'microsecond', 'min', 'minute', 'mod',
- 'month', 'monthname', 'natural', 'nchar', 'next', 'no', 'noround',
- 'not', 'now', 'null', 'num', 'numeric', 'object', 'of', 'on',
- 'order', 'packed', 'pi', 'power', 'prev', 'primary', 'radians',
- 'real', 'reject', 'relative', 'replace', 'rfill', 'right', 'round',
- 'rowid', 'rowno', 'rpad', 'rtrim', 'second', 'select', 'selupd',
- 'serial', 'set', 'show', 'sign', 'sin', 'sinh', 'smallint', 'some',
- 'soundex', 'space', 'sqrt', 'stamp', 'statistics', 'stddev',
- 'subdate', 'substr', 'substring', 'subtime', 'sum', 'sysdba',
- 'table', 'tan', 'tanh', 'time', 'timediff', 'timestamp', 'timezone',
- 'to', 'toidentifier', 'transaction', 'translate', 'trim', 'trunc',
- 'truncate', 'ucase', 'uid', 'unicode', 'union', 'update', 'upper',
- 'user', 'usergroup', 'using', 'utcdate', 'utcdiff', 'value', 'values',
- 'varchar', 'vargraphic', 'variance', 'week', 'weekofyear', 'when',
- 'where', 'with', 'year', 'zoned' ])
-
- def _normalize_name(self, name):
- if name is None:
- return None
- if name.isupper():
- lc_name = name.lower()
- if not self._requires_quotes(lc_name):
- return lc_name
- return name
-
- def _denormalize_name(self, name):
- if name is None:
- return None
- elif (name.islower() and
- not self._requires_quotes(name)):
- return name.upper()
- else:
- return name
-
- def _maybe_quote_identifier(self, name):
- if self._requires_quotes(name):
- return self.quote_identifier(name)
- else:
- return name
-
-
-class MaxDBDDLCompiler(compiler.DDLCompiler):
- def get_column_specification(self, column, **kw):
- colspec = [self.preparer.format_column(column),
- self.dialect.type_compiler.process(column.type)]
-
- if not column.nullable:
- colspec.append('NOT NULL')
-
- default = column.default
- default_str = self.get_column_default_string(column)
-
- # No DDL default for columns specified with non-optional sequence-
- # this defaulting behavior is entirely client-side. (And as a
- # consequence, non-reflectable.)
- if (default and isinstance(default, schema.Sequence) and
- not default.optional):
- pass
- # Regular default
- elif default_str is not None:
- colspec.append('DEFAULT %s' % default_str)
- # Assign DEFAULT SERIAL heuristically
- elif column.primary_key and column.autoincrement:
- # For SERIAL on a non-primary key member, use
- # DefaultClause(text('SERIAL'))
- try:
- first = [c for c in column.table.primary_key.columns
- if (c.autoincrement and
- (isinstance(c.type, sqltypes.Integer) or
- (isinstance(c.type, MaxNumeric) and
- c.type.precision)) and
- not c.foreign_keys)].pop(0)
- if column is first:
- colspec.append('DEFAULT SERIAL')
- except IndexError:
- pass
- return ' '.join(colspec)
-
- def get_column_default_string(self, column):
- if isinstance(column.server_default, schema.DefaultClause):
- if isinstance(column.default.arg, basestring):
- if isinstance(column.type, sqltypes.Integer):
- return str(column.default.arg)
- else:
- return "'%s'" % column.default.arg
- else:
- return unicode(self._compile(column.default.arg, None))
- else:
- return None
-
- def visit_create_sequence(self, create):
- """Creates a SEQUENCE.
-
- TODO: move to module doc?
-
- start
- With an integer value, set the START WITH option.
-
- increment
- An integer value to increment by. Default is the database default.
-
- maxdb_minvalue
- maxdb_maxvalue
- With an integer value, sets the corresponding sequence option.
-
- maxdb_no_minvalue
- maxdb_no_maxvalue
- Defaults to False. If true, sets the corresponding sequence option.
-
- maxdb_cycle
- Defaults to False. If true, sets the CYCLE option.
-
- maxdb_cache
- With an integer value, sets the CACHE option.
-
- maxdb_no_cache
- Defaults to False. If true, sets NOCACHE.
- """
- sequence = create.element
-
- if (not sequence.optional and
- (not self.checkfirst or
- not self.dialect.has_sequence(self.connection, sequence.name))):
-
- ddl = ['CREATE SEQUENCE',
- self.preparer.format_sequence(sequence)]
-
- sequence.increment = 1
-
- if sequence.increment is not None:
- ddl.extend(('INCREMENT BY', str(sequence.increment)))
-
- if sequence.start is not None:
- ddl.extend(('START WITH', str(sequence.start)))
-
- opts = dict([(pair[0][6:].lower(), pair[1])
- for pair in sequence.kwargs.items()
- if pair[0].startswith('maxdb_')])
-
- if 'maxvalue' in opts:
- ddl.extend(('MAXVALUE', str(opts['maxvalue'])))
- elif opts.get('no_maxvalue', False):
- ddl.append('NOMAXVALUE')
- if 'minvalue' in opts:
- ddl.extend(('MINVALUE', str(opts['minvalue'])))
- elif opts.get('no_minvalue', False):
- ddl.append('NOMINVALUE')
-
- if opts.get('cycle', False):
- ddl.append('CYCLE')
-
- if 'cache' in opts:
- ddl.extend(('CACHE', str(opts['cache'])))
- elif opts.get('no_cache', False):
- ddl.append('NOCACHE')
-
- return ' '.join(ddl)
-
-
-class MaxDBDialect(default.DefaultDialect):
- name = 'maxdb'
- supports_alter = True
- supports_unicode_statements = True
- max_identifier_length = 32
- supports_sane_rowcount = True
- supports_sane_multi_rowcount = False
-
- preparer = MaxDBIdentifierPreparer
- statement_compiler = MaxDBCompiler
- ddl_compiler = MaxDBDDLCompiler
- execution_ctx_cls = MaxDBExecutionContext
-
- ported_sqla_06 = False
-
- colspecs = colspecs
- ischema_names = ischema_names
-
- # MaxDB-specific
- datetimeformat = 'internal'
-
- def __init__(self, _raise_known_sql_errors=False, **kw):
- super(MaxDBDialect, self).__init__(**kw)
- self._raise_known = _raise_known_sql_errors
-
- if self.dbapi is None:
- self.dbapi_type_map = {}
- else:
- self.dbapi_type_map = {
- 'Long Binary': MaxBlob(),
- 'Long byte_t': MaxBlob(),
- 'Long Unicode': MaxText(),
- 'Timestamp': MaxTimestamp(),
- 'Date': MaxDate(),
- 'Time': MaxTime(),
- datetime.datetime: MaxTimestamp(),
- datetime.date: MaxDate(),
- datetime.time: MaxTime(),
- }
-
- def do_execute(self, cursor, statement, parameters, context=None):
- res = cursor.execute(statement, parameters)
- if isinstance(res, int) and context is not None:
- context._rowcount = res
-
- def do_release_savepoint(self, connection, name):
- # Does MaxDB truly support RELEASE SAVEPOINT ? All my attempts
- # produce "SUBTRANS COMMIT/ROLLBACK not allowed without SUBTRANS
- # BEGIN SQLSTATE: I7065"
- # Note that ROLLBACK TO works fine. In theory, a RELEASE should
- # just free up some transactional resources early, before the overall
- # COMMIT/ROLLBACK so omitting it should be relatively ok.
- pass
-
- def _get_default_schema_name(self, connection):
- return self.identifier_preparer._normalize_name(
- connection.execute(
- 'SELECT CURRENT_SCHEMA FROM DUAL').scalar())
-
- def has_table(self, connection, table_name, schema=None):
- denormalize = self.identifier_preparer._denormalize_name
- bind = [denormalize(table_name)]
- if schema is None:
- sql = ("SELECT tablename FROM TABLES "
- "WHERE TABLES.TABLENAME=? AND"
- " TABLES.SCHEMANAME=CURRENT_SCHEMA ")
- else:
- sql = ("SELECT tablename FROM TABLES "
- "WHERE TABLES.TABLENAME = ? AND"
- " TABLES.SCHEMANAME=? ")
- bind.append(denormalize(schema))
-
- rp = connection.execute(sql, bind)
- return bool(rp.first())
-
- @reflection.cache
- def get_table_names(self, connection, schema=None, **kw):
- if schema is None:
- sql = (" SELECT TABLENAME FROM TABLES WHERE "
- " SCHEMANAME=CURRENT_SCHEMA ")
- rs = connection.execute(sql)
- else:
- sql = (" SELECT TABLENAME FROM TABLES WHERE "
- " SCHEMANAME=? ")
- matchname = self.identifier_preparer._denormalize_name(schema)
- rs = connection.execute(sql, matchname)
- normalize = self.identifier_preparer._normalize_name
- return [normalize(row[0]) for row in rs]
-
- def reflecttable(self, connection, table, include_columns):
- denormalize = self.identifier_preparer._denormalize_name
- normalize = self.identifier_preparer._normalize_name
-
- st = ('SELECT COLUMNNAME, MODE, DATATYPE, CODETYPE, LEN, DEC, '
- ' NULLABLE, "DEFAULT", DEFAULTFUNCTION '
- 'FROM COLUMNS '
- 'WHERE TABLENAME=? AND SCHEMANAME=%s '
- 'ORDER BY POS')
-
- fk = ('SELECT COLUMNNAME, FKEYNAME, '
- ' REFSCHEMANAME, REFTABLENAME, REFCOLUMNNAME, RULE, '
- ' (CASE WHEN REFSCHEMANAME = CURRENT_SCHEMA '
- ' THEN 1 ELSE 0 END) AS in_schema '
- 'FROM FOREIGNKEYCOLUMNS '
- 'WHERE TABLENAME=? AND SCHEMANAME=%s '
- 'ORDER BY FKEYNAME ')
-
- params = [denormalize(table.name)]
- if not table.schema:
- st = st % 'CURRENT_SCHEMA'
- fk = fk % 'CURRENT_SCHEMA'
- else:
- st = st % '?'
- fk = fk % '?'
- params.append(denormalize(table.schema))
-
- rows = connection.execute(st, params).fetchall()
- if not rows:
- raise exc.NoSuchTableError(table.fullname)
-
- include_columns = set(include_columns or [])
-
- for row in rows:
- (name, mode, col_type, encoding, length, scale,
- nullable, constant_def, func_def) = row
-
- name = normalize(name)
-
- if include_columns and name not in include_columns:
- continue
-
- type_args, type_kw = [], {}
- if col_type == 'FIXED':
- type_args = length, scale
- # Convert FIXED(10) DEFAULT SERIAL to our Integer
- if (scale == 0 and
- func_def is not None and func_def.startswith('SERIAL')):
- col_type = 'INTEGER'
- type_args = length,
- elif col_type in 'FLOAT':
- type_args = length,
- elif col_type in ('CHAR', 'VARCHAR'):
- type_args = length,
- type_kw['encoding'] = encoding
- elif col_type == 'LONG':
- type_kw['encoding'] = encoding
-
- try:
- type_cls = ischema_names[col_type.lower()]
- type_instance = type_cls(*type_args, **type_kw)
- except KeyError:
- util.warn("Did not recognize type '%s' of column '%s'" %
- (col_type, name))
- type_instance = sqltypes.NullType
-
- col_kw = {'autoincrement': False}
- col_kw['nullable'] = (nullable == 'YES')
- col_kw['primary_key'] = (mode == 'KEY')
-
- if func_def is not None:
- if func_def.startswith('SERIAL'):
- if col_kw['primary_key']:
- # No special default- let the standard autoincrement
- # support handle SERIAL pk columns.
- col_kw['autoincrement'] = True
- else:
- # strip current numbering
- col_kw['server_default'] = schema.DefaultClause(
- sql.text('SERIAL'))
- col_kw['autoincrement'] = True
- else:
- col_kw['server_default'] = schema.DefaultClause(
- sql.text(func_def))
- elif constant_def is not None:
- col_kw['server_default'] = schema.DefaultClause(sql.text(
- "'%s'" % constant_def.replace("'", "''")))
-
- table.append_column(schema.Column(name, type_instance, **col_kw))
-
- fk_sets = itertools.groupby(connection.execute(fk, params),
- lambda row: row.FKEYNAME)
- for fkeyname, fkey in fk_sets:
- fkey = list(fkey)
- if include_columns:
- key_cols = set([r.COLUMNNAME for r in fkey])
- if key_cols != include_columns:
- continue
-
- columns, referants = [], []
- quote = self.identifier_preparer._maybe_quote_identifier
-
- for row in fkey:
- columns.append(normalize(row.COLUMNNAME))
- if table.schema or not row.in_schema:
- referants.append('.'.join(
- [quote(normalize(row[c]))
- for c in ('REFSCHEMANAME', 'REFTABLENAME',
- 'REFCOLUMNNAME')]))
- else:
- referants.append('.'.join(
- [quote(normalize(row[c]))
- for c in ('REFTABLENAME', 'REFCOLUMNNAME')]))
-
- constraint_kw = {'name': fkeyname.lower()}
- if fkey[0].RULE is not None:
- rule = fkey[0].RULE
- if rule.startswith('DELETE '):
- rule = rule[7:]
- constraint_kw['ondelete'] = rule
-
- table_kw = {}
- if table.schema or not row.in_schema:
- table_kw['schema'] = normalize(fkey[0].REFSCHEMANAME)
-
- ref_key = schema._get_table_key(normalize(fkey[0].REFTABLENAME),
- table_kw.get('schema'))
- if ref_key not in table.metadata.tables:
- schema.Table(normalize(fkey[0].REFTABLENAME),
- table.metadata,
- autoload=True, autoload_with=connection,
- **table_kw)
-
- constraint = schema.ForeignKeyConstraint(
- columns, referants, link_to_name=True,
- **constraint_kw)
- table.append_constraint(constraint)
-
- def has_sequence(self, connection, name):
- # [ticket:726] makes this schema-aware.
- denormalize = self.identifier_preparer._denormalize_name
- sql = ("SELECT sequence_name FROM SEQUENCES "
- "WHERE SEQUENCE_NAME=? ")
-
- rp = connection.execute(sql, denormalize(name))
- return bool(rp.first())
-
-
-def _autoserial_column(table):
- """Finds the effective DEFAULT SERIAL column of a Table, if any."""
-
- for index, col in enumerate(table.primary_key.columns):
- if (isinstance(col.type, (sqltypes.Integer, sqltypes.Numeric)) and
- col.autoincrement):
- if isinstance(col.default, schema.Sequence):
- if col.default.optional:
- return index, col
- elif (col.default is None or
- (not isinstance(col.server_default, schema.DefaultClause))):
- return index, col
-
- return None, None
-
diff --git a/libs/sqlalchemy/dialects/maxdb/sapdb.py b/libs/sqlalchemy/dialects/maxdb/sapdb.py
deleted file mode 100644
index 51f272a2..00000000
--- a/libs/sqlalchemy/dialects/maxdb/sapdb.py
+++ /dev/null
@@ -1,23 +0,0 @@
-# maxdb/sapdb.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
-#
-# This module is part of SQLAlchemy and is released under
-# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
-from sqlalchemy.dialects.maxdb.base import MaxDBDialect
-
-class MaxDBDialect_sapdb(MaxDBDialect):
- driver = 'sapdb'
-
- @classmethod
- def dbapi(cls):
- from sapdb import dbapi as _dbapi
- return _dbapi
-
- def create_connect_args(self, url):
- opts = url.translate_connect_args(username='user')
- opts.update(url.query)
- return [], opts
-
-
-dialect = MaxDBDialect_sapdb
\ No newline at end of file
diff --git a/libs/sqlalchemy/dialects/mssql/__init__.py b/libs/sqlalchemy/dialects/mssql/__init__.py
index b3acbf3a..7a2dfa60 100644
--- a/libs/sqlalchemy/dialects/mssql/__init__.py
+++ b/libs/sqlalchemy/dialects/mssql/__init__.py
@@ -1,5 +1,5 @@
# mssql/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -23,4 +23,4 @@ __all__ = (
'DATETIME2', 'DATETIMEOFFSET', 'DATE', 'TIME', 'SMALLDATETIME',
'BINARY', 'VARBINARY', 'BIT', 'REAL', 'IMAGE', 'TIMESTAMP',
'MONEY', 'SMALLMONEY', 'UNIQUEIDENTIFIER', 'SQL_VARIANT', 'dialect'
-)
\ No newline at end of file
+)
diff --git a/libs/sqlalchemy/dialects/mssql/adodbapi.py b/libs/sqlalchemy/dialects/mssql/adodbapi.py
index 05ac6d6f..95cf4242 100644
--- a/libs/sqlalchemy/dialects/mssql/adodbapi.py
+++ b/libs/sqlalchemy/dialects/mssql/adodbapi.py
@@ -1,11 +1,20 @@
# mssql/adodbapi.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-The adodbapi dialect is not implemented for 0.6 at this time.
+.. dialect:: mssql+adodbapi
+ :name: adodbapi
+ :dbapi: adodbapi
+ :connectstring: mssql+adodbapi://:@
+ :url: http://adodbapi.sourceforge.net/
+
+.. note::
+
+ The adodbapi dialect is not implemented SQLAlchemy versions 0.6 and
+ above at this time.
"""
import datetime
@@ -13,6 +22,7 @@ from sqlalchemy import types as sqltypes, util
from sqlalchemy.dialects.mssql.base import MSDateTime, MSDialect
import sys
+
class MSDateTime_adodbapi(MSDateTime):
def result_processor(self, dialect, coltype):
def process(value):
@@ -40,7 +50,7 @@ class MSDialect_adodbapi(MSDialect):
colspecs = util.update_copy(
MSDialect.colspecs,
{
- sqltypes.DateTime:MSDateTime_adodbapi
+ sqltypes.DateTime: MSDateTime_adodbapi
}
)
@@ -49,18 +59,18 @@ class MSDialect_adodbapi(MSDialect):
connectors = ["Provider=SQLOLEDB"]
if 'port' in keys:
- connectors.append ("Data Source=%s, %s" %
+ connectors.append("Data Source=%s, %s" %
(keys.get("host"), keys.get("port")))
else:
- connectors.append ("Data Source=%s" % keys.get("host"))
- connectors.append ("Initial Catalog=%s" % keys.get("database"))
+ connectors.append("Data Source=%s" % keys.get("host"))
+ connectors.append("Initial Catalog=%s" % keys.get("database"))
user = keys.get("user")
if user:
connectors.append("User Id=%s" % user)
connectors.append("Password=%s" % keys.get("password", ""))
else:
connectors.append("Integrated Security=SSPI")
- return [[";".join (connectors)], {}]
+ return [[";".join(connectors)], {}]
def is_disconnect(self, e, connection, cursor):
return isinstance(e, self.dbapi.adodbapi.DatabaseError) and \
diff --git a/libs/sqlalchemy/dialects/mssql/base.py b/libs/sqlalchemy/dialects/mssql/base.py
index b6e0d881..90fd1f38 100644
--- a/libs/sqlalchemy/dialects/mssql/base.py
+++ b/libs/sqlalchemy/dialects/mssql/base.py
@@ -1,15 +1,13 @@
# mssql/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Microsoft SQL Server database.
+"""
+.. dialect:: mssql
+ :name: Microsoft SQL Server
-Connecting
-----------
-
-See the individual driver sections below for details on connecting.
Auto Increment Behavior
-----------------------
@@ -47,13 +45,10 @@ does in other dialects and results in an ``IDENTITY`` column.
Collation Support
-----------------
-MSSQL specific string types support a collation parameter that
-creates a column-level specific collation for the column. The
-collation parameter accepts a Windows Collation Name or a SQL
-Collation Name. Supported types are MSChar, MSNChar, MSString,
-MSNVarchar, MSText, and MSNText. For example::
+Character collations are supported by the base string types,
+specified by the string argument "collation"::
- from sqlalchemy.dialects.mssql import VARCHAR
+ from sqlalchemy import VARCHAR
Column('login', VARCHAR(32, collation='Latin1_General_CI_AS'))
When such a column is associated with a :class:`.Table`, the
@@ -61,6 +56,9 @@ CREATE TABLE statement for this column will yield::
login VARCHAR(32) COLLATE Latin1_General_CI_AS NULL
+.. versionadded:: 0.8 Character collations are now part of the base string
+ types.
+
LIMIT/OFFSET Support
--------------------
@@ -103,6 +101,50 @@ The DATE and TIME types are not available for MSSQL 2005 and
previous - if a server version below 2008 is detected, DDL
for these types will be issued as DATETIME.
+.. _mssql_indexes:
+
+MSSQL-Specific Index Options
+-----------------------------
+
+The MSSQL dialect supports special options for :class:`.Index`.
+
+CLUSTERED
+^^^^^^^^^^
+
+The ``mssql_clustered`` option adds the CLUSTERED keyword to the index::
+
+ Index("my_index", table.c.x, mssql_clustered=True)
+
+would render the index as ``CREATE CLUSTERED INDEX my_index ON table (x)``
+
+.. versionadded:: 0.8
+
+INCLUDE
+^^^^^^^
+
+The ``mssql_include`` option renders INCLUDE(colname) for the given string names::
+
+ Index("my_index", table.c.x, mssql_include=['y'])
+
+would render the index as ``CREATE INDEX my_index ON table (x) INCLUDE (y)``
+
+.. versionadded:: 0.8
+
+Index ordering
+^^^^^^^^^^^^^^
+
+Index ordering is available via functional expressions, such as::
+
+ Index("my_index", table.c.x.desc())
+
+would render the index as ``CREATE INDEX my_index ON table (x DESC)``
+
+.. versionadded:: 0.8
+
+.. seealso::
+
+ :ref:`schema_indexes_functional`
+
Compatibility Levels
--------------------
MSSQL supports the notion of setting compatibility levels at the
@@ -158,30 +200,6 @@ following ALTER DATABASE commands executed at the SQL prompt::
Background on SQL Server snapshot isolation is available at
http://msdn.microsoft.com/en-us/library/ms175095.aspx.
-Scalar Select Comparisons
--------------------------
-
-.. deprecated:: 0.8
- The MSSQL dialect contains a legacy behavior whereby comparing
- a scalar select to a value using the ``=`` or ``!=`` operator
- will resolve to IN or NOT IN, respectively. This behavior
- will be removed in 0.8 - the ``s.in_()``/``~s.in_()`` operators
- should be used when IN/NOT IN are desired.
-
-For the time being, the existing behavior prevents a comparison
-between scalar select and another value that actually wants to use ``=``.
-To remove this behavior in a forwards-compatible way, apply this
-compilation rule by placing the following code at the module import
-level::
-
- from sqlalchemy.ext.compiler import compiles
- from sqlalchemy.sql.expression import _BinaryExpression
- from sqlalchemy.sql.compiler import SQLCompiler
-
- @compiles(_BinaryExpression, 'mssql')
- def override_legacy_binary(element, compiler, **kw):
- return SQLCompiler.visit_binary(compiler, element, **kw)
-
Known Issues
------------
@@ -190,20 +208,23 @@ Known Issues
SQL Server 2005
"""
-import datetime, operator, re
+import datetime
+import operator
+import re
-from sqlalchemy import sql, schema as sa_schema, exc, util
-from sqlalchemy.sql import select, compiler, expression, \
- operators as sql_operators, \
+from ... import sql, schema as sa_schema, exc, util
+from ...sql import compiler, expression, \
util as sql_util, cast
-from sqlalchemy.engine import default, base, reflection
-from sqlalchemy import types as sqltypes
-from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \
+from ... import engine
+from ...engine import reflection, default
+from ... import types as sqltypes
+from ...types import INTEGER, BIGINT, SMALLINT, DECIMAL, NUMERIC, \
FLOAT, TIMESTAMP, DATETIME, DATE, BINARY,\
- VARBINARY, BLOB
+ VARBINARY, TEXT, VARCHAR, NVARCHAR, CHAR, NCHAR
-from sqlalchemy.dialects.mssql import information_schema as ischema
+from ...util import update_wrapper
+from . import information_schema as ischema
MS_2008_VERSION = (10,)
MS_2005_VERSION = (9,)
@@ -240,6 +261,7 @@ RESERVED_WORDS = set(
'writetext',
])
+
class REAL(sqltypes.REAL):
__visit_name__ = 'REAL'
@@ -248,6 +270,7 @@ class REAL(sqltypes.REAL):
kw['precision'] = 24
super(REAL, self).__init__(**kw)
+
class TINYINT(sqltypes.Integer):
__visit_name__ = 'TINYINT'
@@ -267,11 +290,12 @@ class _MSDate(sqltypes.Date):
return process
_reg = re.compile(r"(\d+)-(\d+)-(\d+)")
+
def result_processor(self, dialect, coltype):
def process(value):
if isinstance(value, datetime.datetime):
return value.date()
- elif isinstance(value, basestring):
+ elif isinstance(value, util.string_types):
return datetime.date(*[
int(x or 0)
for x in self._reg.match(value).groups()
@@ -280,6 +304,7 @@ class _MSDate(sqltypes.Date):
return value
return process
+
class TIME(sqltypes.TIME):
def __init__(self, precision=None, **kwargs):
self.precision = precision
@@ -298,17 +323,20 @@ class TIME(sqltypes.TIME):
return process
_reg = re.compile(r"(\d+):(\d+):(\d+)(?:\.(\d{0,6}))?")
+
def result_processor(self, dialect, coltype):
def process(value):
if isinstance(value, datetime.datetime):
return value.time()
- elif isinstance(value, basestring):
+ elif isinstance(value, util.string_types):
return datetime.time(*[
int(x or 0)
for x in self._reg.match(value).groups()])
else:
return value
return process
+_MSTime = TIME
+
class _DateTimeBase(object):
def bind_processor(self, dialect):
@@ -319,12 +347,15 @@ class _DateTimeBase(object):
return value
return process
+
class _MSDateTime(_DateTimeBase, sqltypes.DateTime):
pass
+
class SMALLDATETIME(_DateTimeBase, sqltypes.DateTime):
__visit_name__ = 'SMALLDATETIME'
+
class DATETIME2(_DateTimeBase, sqltypes.DateTime):
__visit_name__ = 'DATETIME2'
@@ -340,135 +371,28 @@ class DATETIMEOFFSET(sqltypes.TypeEngine):
def __init__(self, precision=None, **kwargs):
self.precision = precision
+
class _StringType(object):
"""Base for MSSQL string types."""
def __init__(self, collation=None):
- self.collation = collation
+ super(_StringType, self).__init__(collation=collation)
-class TEXT(_StringType, sqltypes.TEXT):
- """MSSQL TEXT type, for variable-length text up to 2^31 characters."""
- def __init__(self, length=None, collation=None, **kw):
- """Construct a TEXT.
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
- """
- _StringType.__init__(self, collation)
- sqltypes.Text.__init__(self, length, **kw)
-
-class NTEXT(_StringType, sqltypes.UnicodeText):
+class NTEXT(sqltypes.UnicodeText):
"""MSSQL NTEXT type, for variable-length unicode text up to 2^30
characters."""
__visit_name__ = 'NTEXT'
- def __init__(self, length=None, collation=None, **kw):
- """Construct a NTEXT.
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
-
- """
- _StringType.__init__(self, collation)
- sqltypes.UnicodeText.__init__(self, length, **kw)
-
-
-class VARCHAR(_StringType, sqltypes.VARCHAR):
- """MSSQL VARCHAR type, for variable-length non-Unicode data with a maximum
- of 8,000 characters."""
-
- def __init__(self, length=None, collation=None, **kw):
- """Construct a VARCHAR.
-
- :param length: Optinal, maximum data length, in characters.
-
- :param convert_unicode: defaults to False. If True, convert
- ``unicode`` data sent to the database to a ``str``
- bytestring, and convert bytestrings coming back from the
- database into ``unicode``.
-
- Bytestrings are encoded using the dialect's
- :attr:`~sqlalchemy.engine.base.Dialect.encoding`, which
- defaults to `utf-8`.
-
- If False, may be overridden by
- :attr:`sqlalchemy.engine.base.Dialect.convert_unicode`.
-
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
-
- """
- _StringType.__init__(self, collation)
- sqltypes.VARCHAR.__init__(self, length, **kw)
-
-class NVARCHAR(_StringType, sqltypes.NVARCHAR):
- """MSSQL NVARCHAR type.
-
- For variable-length unicode character data up to 4,000 characters."""
-
- def __init__(self, length=None, collation=None, **kw):
- """Construct a NVARCHAR.
-
- :param length: Optional, Maximum data length, in characters.
-
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
-
- """
- _StringType.__init__(self, collation)
- sqltypes.NVARCHAR.__init__(self, length, **kw)
-
-class CHAR(_StringType, sqltypes.CHAR):
- """MSSQL CHAR type, for fixed-length non-Unicode data with a maximum
- of 8,000 characters."""
-
- def __init__(self, length=None, collation=None, **kw):
- """Construct a CHAR.
-
- :param length: Optinal, maximum data length, in characters.
-
- :param convert_unicode: defaults to False. If True, convert
- ``unicode`` data sent to the database to a ``str``
- bytestring, and convert bytestrings coming back from the
- database into ``unicode``.
-
- Bytestrings are encoded using the dialect's
- :attr:`~sqlalchemy.engine.base.Dialect.encoding`, which
- defaults to `utf-8`.
-
- If False, may be overridden by
- :attr:`sqlalchemy.engine.base.Dialect.convert_unicode`.
-
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
-
- """
- _StringType.__init__(self, collation)
- sqltypes.CHAR.__init__(self, length, **kw)
-
-class NCHAR(_StringType, sqltypes.NCHAR):
- """MSSQL NCHAR type.
-
- For fixed-length unicode character data up to 4,000 characters."""
-
- def __init__(self, length=None, collation=None, **kw):
- """Construct an NCHAR.
-
- :param length: Optional, Maximum data length, in characters.
-
- :param collation: Optional, a column-level collation for this string
- value. Accepts a Windows Collation Name or a SQL Collation Name.
-
- """
- _StringType.__init__(self, collation)
- sqltypes.NCHAR.__init__(self, length, **kw)
class IMAGE(sqltypes.LargeBinary):
__visit_name__ = 'IMAGE'
+
class BIT(sqltypes.TypeEngine):
__visit_name__ = 'BIT'
@@ -476,12 +400,15 @@ class BIT(sqltypes.TypeEngine):
class MONEY(sqltypes.TypeEngine):
__visit_name__ = 'MONEY'
+
class SMALLMONEY(sqltypes.TypeEngine):
__visit_name__ = 'SMALLMONEY'
+
class UNIQUEIDENTIFIER(sqltypes.TypeEngine):
__visit_name__ = "UNIQUEIDENTIFIER"
+
class SQL_VARIANT(sqltypes.TypeEngine):
__visit_name__ = 'SQL_VARIANT'
@@ -510,30 +437,30 @@ MSUniqueIdentifier = UNIQUEIDENTIFIER
MSVariant = SQL_VARIANT
ischema_names = {
- 'int' : INTEGER,
+ 'int': INTEGER,
'bigint': BIGINT,
- 'smallint' : SMALLINT,
- 'tinyint' : TINYINT,
- 'varchar' : VARCHAR,
- 'nvarchar' : NVARCHAR,
- 'char' : CHAR,
- 'nchar' : NCHAR,
- 'text' : TEXT,
- 'ntext' : NTEXT,
- 'decimal' : DECIMAL,
- 'numeric' : NUMERIC,
- 'float' : FLOAT,
- 'datetime' : DATETIME,
- 'datetime2' : DATETIME2,
- 'datetimeoffset' : DATETIMEOFFSET,
+ 'smallint': SMALLINT,
+ 'tinyint': TINYINT,
+ 'varchar': VARCHAR,
+ 'nvarchar': NVARCHAR,
+ 'char': CHAR,
+ 'nchar': NCHAR,
+ 'text': TEXT,
+ 'ntext': NTEXT,
+ 'decimal': DECIMAL,
+ 'numeric': NUMERIC,
+ 'float': FLOAT,
+ 'datetime': DATETIME,
+ 'datetime2': DATETIME2,
+ 'datetimeoffset': DATETIMEOFFSET,
'date': DATE,
'time': TIME,
- 'smalldatetime' : SMALLDATETIME,
- 'binary' : BINARY,
- 'varbinary' : VARBINARY,
+ 'smalldatetime': SMALLDATETIME,
+ 'binary': BINARY,
+ 'varbinary': VARBINARY,
'bit': BIT,
- 'real' : REAL,
- 'image' : IMAGE,
+ 'real': REAL,
+ 'image': IMAGE,
'timestamp': TIMESTAMP,
'money': MONEY,
'smallmoney': SMALLMONEY,
@@ -609,8 +536,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
return self._extend("TEXT", type_)
def visit_VARCHAR(self, type_):
- return self._extend("VARCHAR", type_,
- length = type_.length or 'max')
+ return self._extend("VARCHAR", type_, length=type_.length or 'max')
def visit_CHAR(self, type_):
return self._extend("CHAR", type_)
@@ -619,8 +545,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
return self._extend("NCHAR", type_)
def visit_NVARCHAR(self, type_):
- return self._extend("NVARCHAR", type_,
- length = type_.length or 'max')
+ return self._extend("NVARCHAR", type_, length=type_.length or 'max')
def visit_date(self, type_):
if self.dialect.server_version_info < MS_2008_VERSION:
@@ -664,6 +589,7 @@ class MSTypeCompiler(compiler.GenericTypeCompiler):
def visit_SQL_VARIANT(self, type_):
return 'SQL_VARIANT'
+
class MSExecutionContext(default.DefaultExecutionContext):
_enable_identity_insert = False
_select_lastrowid = False
@@ -693,7 +619,7 @@ class MSExecutionContext(default.DefaultExecutionContext):
self.root_connection._cursor_execute(self.cursor,
"SET IDENTITY_INSERT %s ON" %
self.dialect.identifier_preparer.format_table(tbl),
- ())
+ (), self)
def post_exec(self):
"""Disable IDENTITY_INSERT if enabled."""
@@ -702,25 +628,24 @@ class MSExecutionContext(default.DefaultExecutionContext):
if self._select_lastrowid:
if self.dialect.use_scope_identity:
conn._cursor_execute(self.cursor,
- "SELECT scope_identity() AS lastrowid", ())
+ "SELECT scope_identity() AS lastrowid", (), self)
else:
conn._cursor_execute(self.cursor,
- "SELECT @@identity AS lastrowid", ())
+ "SELECT @@identity AS lastrowid", (), self)
# fetchall() ensures the cursor is consumed without closing it
row = self.cursor.fetchall()[0]
self._lastrowid = int(row[0])
if (self.isinsert or self.isupdate or self.isdelete) and \
self.compiled.returning:
- self._result_proxy = base.FullyBufferedResultProxy(self)
+ self._result_proxy = engine.FullyBufferedResultProxy(self)
if self._enable_identity_insert:
conn._cursor_execute(self.cursor,
"SET IDENTITY_INSERT %s OFF" %
self.dialect.identifier_preparer.
format_table(self.compiled.statement.table),
- ()
- )
+ (), self)
def get_lastrowid(self):
return self._lastrowid
@@ -740,7 +665,8 @@ class MSExecutionContext(default.DefaultExecutionContext):
if self._result_proxy:
return self._result_proxy
else:
- return base.ResultProxy(self)
+ return engine.ResultProxy(self)
+
class MSSQLCompiler(compiler.SQLCompiler):
returning_precedes_values = True
@@ -770,12 +696,18 @@ class MSSQLCompiler(compiler.SQLCompiler):
def visit_char_length_func(self, fn, **kw):
return "LEN%s" % self.function_argspec(fn, **kw)
- def visit_concat_op(self, binary, **kw):
+ def visit_concat_op_binary(self, binary, operator, **kw):
return "%s + %s" % \
(self.process(binary.left, **kw),
self.process(binary.right, **kw))
- def visit_match_op(self, binary, **kw):
+ def visit_true(self, expr, **kw):
+ return '1'
+
+ def visit_false(self, expr, **kw):
+ return '0'
+
+ def visit_match_op_binary(self, binary, operator, **kw):
return "CONTAINS (%s, %s)" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw))
@@ -843,8 +775,8 @@ class MSSQLCompiler(compiler.SQLCompiler):
else:
return None
- def visit_table(self, table, mssql_aliased=False, **kwargs):
- if mssql_aliased is table:
+ def visit_table(self, table, mssql_aliased=False, iscrud=False, **kwargs):
+ if mssql_aliased is table or iscrud:
return super(MSSQLCompiler, self).visit_table(table, **kwargs)
# alias schema-qualified tables
@@ -871,7 +803,7 @@ class MSSQLCompiler(compiler.SQLCompiler):
return ("ROLLBACK TRANSACTION %s"
% self.preparer.format_savepoint(savepoint_stmt))
- def visit_column(self, column, result_map=None, **kwargs):
+ def visit_column(self, column, add_to_result_map=None, **kwargs):
if column.table is not None and \
(not self.isupdate and not self.isdelete) or self.is_subquery():
# translate for schema-qualified table aliases
@@ -879,20 +811,19 @@ class MSSQLCompiler(compiler.SQLCompiler):
if t is not None:
converted = expression._corresponding_column_or_error(
t, column)
-
- if result_map is not None:
- result_map[column.name.lower()] = \
- (column.name, (column, column.name,
- column.key),
- column.type)
+ if add_to_result_map is not None:
+ add_to_result_map(
+ column.name,
+ column.name,
+ (column, column.name, column.key),
+ column.type
+ )
return super(MSSQLCompiler, self).\
- visit_column(converted,
- result_map=None, **kwargs)
+ visit_column(converted, **kwargs)
- return super(MSSQLCompiler, self).visit_column(column,
- result_map=result_map,
- **kwargs)
+ return super(MSSQLCompiler, self).visit_column(
+ column, add_to_result_map=add_to_result_map, **kwargs)
def visit_binary(self, binary, **kwargs):
"""Move bind parameters to the right-hand side of an operator, where
@@ -900,40 +831,16 @@ class MSSQLCompiler(compiler.SQLCompiler):
"""
if (
- isinstance(binary.left, expression._BindParamClause)
+ isinstance(binary.left, expression.BindParameter)
and binary.operator == operator.eq
- and not isinstance(binary.right, expression._BindParamClause)
+ and not isinstance(binary.right, expression.BindParameter)
):
return self.process(
- expression._BinaryExpression(binary.right,
+ expression.BinaryExpression(binary.right,
binary.left,
binary.operator),
**kwargs)
- else:
- if (
- (binary.operator is operator.eq or
- binary.operator is operator.ne)
- and (
- (isinstance(binary.left, expression._FromGrouping)
- and isinstance(binary.left.element,
- expression._ScalarSelect))
- or (isinstance(binary.right, expression._FromGrouping)
- and isinstance(binary.right.element,
- expression._ScalarSelect))
- or isinstance(binary.left, expression._ScalarSelect)
- or isinstance(binary.right, expression._ScalarSelect)
- )
- ):
- op = binary.operator == operator.eq and "IN" or "NOT IN"
- util.warn_deprecated("Comparing a scalar select using ``=``/``!=`` will "
- "no longer produce IN/NOT IN in 0.8. To remove this "
- "behavior immediately, use the recipe at "
- "http://www.sqlalchemy.org/docs/07/dialects/mssql.html#scalar-select-comparisons")
- return self.process(
- expression._BinaryExpression(binary.left,
- binary.right, op),
- **kwargs)
- return super(MSSQLCompiler, self).visit_binary(binary, **kwargs)
+ return super(MSSQLCompiler, self).visit_binary(binary, **kwargs)
def returning_clause(self, stmt, returning_cols):
@@ -943,21 +850,13 @@ class MSSQLCompiler(compiler.SQLCompiler):
target = stmt.table.alias("deleted")
adapter = sql_util.ClauseAdapter(target)
- def col_label(col):
- adapted = adapter.traverse(col)
- if isinstance(col, expression._Label):
- return adapted.label(c.key)
- else:
- return self.label_select_column(None, adapted, asfrom=False)
columns = [
- self.process(
- col_label(c),
- within_columns_clause=True,
- result_map=self.result_map
- )
- for c in expression._select_iterables(returning_cols)
- ]
+ self._label_select_column(None, adapter.traverse(c),
+ True, False, {})
+ for c in expression._select_iterables(returning_cols)
+ ]
+
return 'OUTPUT ' + ', '.join(columns)
def get_cte_preamble(self, recursive):
@@ -1004,6 +903,7 @@ class MSSQLCompiler(compiler.SQLCompiler):
fromhints=from_hints, **kw)
for t in [from_table] + extra_froms)
+
class MSSQLStrictCompiler(MSSQLCompiler):
"""A subclass of MSSQLCompiler which disables the usage of bind
parameters where not allowed natively by MS-SQL.
@@ -1014,24 +914,20 @@ class MSSQLStrictCompiler(MSSQLCompiler):
"""
ansi_bind_rules = True
- def visit_in_op(self, binary, **kw):
+ def visit_in_op_binary(self, binary, operator, **kw):
kw['literal_binds'] = True
return "%s IN %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw)
)
- def visit_notin_op(self, binary, **kw):
+ def visit_notin_op_binary(self, binary, operator, **kw):
kw['literal_binds'] = True
return "%s NOT IN %s" % (
self.process(binary.left, **kw),
self.process(binary.right, **kw)
)
- def visit_function(self, func, **kw):
- kw['literal_binds'] = True
- return super(MSSQLStrictCompiler, self).visit_function(func, **kw)
-
def render_literal_value(self, value, type_):
"""
For date and datetime values, convert to a string
@@ -1051,13 +947,15 @@ class MSSQLStrictCompiler(MSSQLCompiler):
return super(MSSQLStrictCompiler, self).\
render_literal_value(value, type_)
+
class MSDDLCompiler(compiler.DDLCompiler):
def get_column_specification(self, column, **kwargs):
colspec = (self.preparer.format_column(column) + " "
+ self.dialect.type_compiler.process(column.type))
if column.nullable is not None:
- if not column.nullable or column.primary_key:
+ if not column.nullable or column.primary_key or \
+ isinstance(column.default, sa_schema.Sequence):
colspec += " NOT NULL"
else:
colspec += " NULL"
@@ -1067,18 +965,16 @@ class MSDDLCompiler(compiler.DDLCompiler):
"mssql requires Table-bound columns "
"in order to generate DDL")
- seq_col = column.table._autoincrement_column
-
- # install a IDENTITY Sequence if we have an implicit IDENTITY column
- if seq_col is column:
- sequence = isinstance(column.default, sa_schema.Sequence) and \
- column.default
- if sequence:
- start, increment = sequence.start or 1, \
- sequence.increment or 1
+ # install an IDENTITY Sequence if we either a sequence or an implicit IDENTITY column
+ if isinstance(column.default, sa_schema.Sequence):
+ if column.default.start == 0:
+ start = 0
else:
- start, increment = 1, 1
- colspec += " IDENTITY(%s,%s)" % (start, increment)
+ start = column.default.start or 1
+
+ colspec += " IDENTITY(%s,%s)" % (start, column.default.increment or 1)
+ elif column is column.table._autoincrement_column:
+ colspec += " IDENTITY(1,1)"
else:
default = self.get_column_default_string(column)
if default is not None:
@@ -1086,14 +982,46 @@ class MSDDLCompiler(compiler.DDLCompiler):
return colspec
- def visit_drop_index(self, drop):
- return "\nDROP INDEX %s.%s" % (
- self.preparer.quote_identifier(drop.element.table.name),
- self.preparer.quote(
- self._index_identifier(drop.element.name),
- drop.element.quote)
- )
+ def visit_create_index(self, create, include_schema=False):
+ index = create.element
+ self._verify_index_table(index)
+ preparer = self.preparer
+ text = "CREATE "
+ if index.unique:
+ text += "UNIQUE "
+ # handle clustering option
+ if index.kwargs.get("mssql_clustered"):
+ text += "CLUSTERED "
+
+ text += "INDEX %s ON %s (%s)" \
+ % (
+ self._prepared_index_name(index,
+ include_schema=include_schema),
+ preparer.format_table(index.table),
+ ', '.join(
+ self.sql_compiler.process(expr,
+ include_table=False, literal_binds=True) for
+ expr in index.expressions)
+ )
+
+ # handle other included columns
+ if index.kwargs.get("mssql_include"):
+ inclusions = [index.table.c[col]
+ if isinstance(col, util.string_types) else col
+ for col in index.kwargs["mssql_include"]]
+
+ text += " INCLUDE (%s)" \
+ % ', '.join([preparer.quote(c.name)
+ for c in inclusions])
+
+ return text
+
+ def visit_drop_index(self, drop):
+ return "\nDROP INDEX %s ON %s" % (
+ self._prepared_index_name(drop.element, include_schema=False),
+ self.preparer.format_table(drop.element.table)
+ )
class MSIdentifierPreparer(compiler.IdentifierPreparer):
reserved_words = RESERVED_WORDS
@@ -1105,11 +1033,48 @@ class MSIdentifierPreparer(compiler.IdentifierPreparer):
def _escape_identifier(self, value):
return value
- def quote_schema(self, schema, force=True):
+ def quote_schema(self, schema, force=None):
"""Prepare a quoted table and schema name."""
result = '.'.join([self.quote(x, force) for x in schema.split('.')])
return result
+
+def _db_plus_owner_listing(fn):
+ def wrap(dialect, connection, schema=None, **kw):
+ dbname, owner = _owner_plus_db(dialect, schema)
+ return _switch_db(dbname, connection, fn, dialect, connection,
+ dbname, owner, schema, **kw)
+ return update_wrapper(wrap, fn)
+
+
+def _db_plus_owner(fn):
+ def wrap(dialect, connection, tablename, schema=None, **kw):
+ dbname, owner = _owner_plus_db(dialect, schema)
+ return _switch_db(dbname, connection, fn, dialect, connection,
+ tablename, dbname, owner, schema, **kw)
+ return update_wrapper(wrap, fn)
+
+
+def _switch_db(dbname, connection, fn, *arg, **kw):
+ if dbname:
+ current_db = connection.scalar("select db_name()")
+ connection.execute("use %s" % dbname)
+ try:
+ return fn(*arg, **kw)
+ finally:
+ if dbname:
+ connection.execute("use %s" % current_db)
+
+
+def _owner_plus_db(dialect, schema):
+ if not schema:
+ return None, dialect.default_schema_name
+ elif "." in schema:
+ return schema.split(".", 1)
+ else:
+ return None, schema
+
+
class MSDialect(default.DefaultDialect):
name = 'mssql'
supports_default_values = True
@@ -1120,9 +1085,9 @@ class MSDialect(default.DefaultDialect):
schema_name = "dbo"
colspecs = {
- sqltypes.DateTime : _MSDateTime,
- sqltypes.Date : _MSDate,
- sqltypes.Time : TIME,
+ sqltypes.DateTime: _MSDateTime,
+ sqltypes.Date: _MSDate,
+ sqltypes.Time: TIME,
}
ischema_names = ischema_names
@@ -1142,7 +1107,7 @@ class MSDialect(default.DefaultDialect):
query_timeout=None,
use_scope_identity=True,
max_identifier_length=None,
- schema_name=u"dbo", **opts):
+ schema_name="dbo", **opts):
self.query_timeout = int(query_timeout or 0)
self.schema_name = schema_name
@@ -1162,7 +1127,7 @@ class MSDialect(default.DefaultDialect):
def initialize(self, connection):
super(MSDialect, self).initialize(connection)
- if self.server_version_info[0] not in range(8, 17):
+ if self.server_version_info[0] not in list(range(8, 17)):
# FreeTDS with version 4.2 seems to report here
# a number like "95.10.255". Don't know what
# that is. So emit warning.
@@ -1171,13 +1136,13 @@ class MSDialect(default.DefaultDialect):
"behaviors may not function properly. If using ODBC "
"with FreeTDS, ensure server version 7.0 or 8.0, not 4.2, "
"is configured in the FreeTDS configuration." %
- ".".join(str(x) for x in self.server_version_info) )
+ ".".join(str(x) for x in self.server_version_info))
if self.server_version_info >= MS_2005_VERSION and \
'implicit_returning' not in self.__dict__:
self.implicit_returning = True
def _get_default_schema_name(self, connection):
- user_name = connection.scalar("SELECT user_name() as user_name;")
+ user_name = connection.scalar("SELECT user_name()")
if user_name is not None:
# now, get the default schema
query = sql.text("""
@@ -1189,25 +1154,20 @@ class MSDialect(default.DefaultDialect):
try:
default_schema_name = connection.scalar(query, name=user_name)
if default_schema_name is not None:
- return unicode(default_schema_name)
+ return util.text_type(default_schema_name)
except:
pass
return self.schema_name
- def _unicode_cast(self, column):
- if self.server_version_info >= MS_2005_VERSION:
- return cast(column, NVARCHAR(_warn_on_bytestring=False))
- else:
- return column
-
- def has_table(self, connection, tablename, schema=None):
- current_schema = schema or self.default_schema_name
+ @_db_plus_owner
+ def has_table(self, connection, tablename, dbname, owner, schema):
columns = ischema.columns
- whereclause = self._unicode_cast(columns.c.table_name)==tablename
- if current_schema:
+ whereclause = columns.c.table_name == tablename
+
+ if owner:
whereclause = sql.and_(whereclause,
- columns.c.table_schema==current_schema)
+ columns.c.table_schema == owner)
s = sql.select([columns], whereclause)
c = connection.execute(s)
return c.first() is not None
@@ -1221,13 +1181,13 @@ class MSDialect(default.DefaultDialect):
return schema_names
@reflection.cache
- def get_table_names(self, connection, schema=None, **kw):
- current_schema = schema or self.default_schema_name
+ @_db_plus_owner_listing
+ def get_table_names(self, connection, dbname, owner, schema, **kw):
tables = ischema.tables
s = sql.select([tables.c.table_name],
sql.and_(
- tables.c.table_schema == current_schema,
- tables.c.table_type == u'BASE TABLE'
+ tables.c.table_schema == owner,
+ tables.c.table_type == 'BASE TABLE'
),
order_by=[tables.c.table_name]
)
@@ -1235,13 +1195,13 @@ class MSDialect(default.DefaultDialect):
return table_names
@reflection.cache
- def get_view_names(self, connection, schema=None, **kw):
- current_schema = schema or self.default_schema_name
+ @_db_plus_owner_listing
+ def get_view_names(self, connection, dbname, owner, schema, **kw):
tables = ischema.tables
s = sql.select([tables.c.table_name],
sql.and_(
- tables.c.table_schema == current_schema,
- tables.c.table_type == u'VIEW'
+ tables.c.table_schema == owner,
+ tables.c.table_type == 'VIEW'
),
order_by=[tables.c.table_name]
)
@@ -1249,15 +1209,13 @@ class MSDialect(default.DefaultDialect):
return view_names
@reflection.cache
- def get_indexes(self, connection, tablename, schema=None, **kw):
+ @_db_plus_owner
+ def get_indexes(self, connection, tablename, dbname, owner, schema, **kw):
# using system catalogs, don't support index reflection
# below MS 2005
if self.server_version_info < MS_2005_VERSION:
return []
- current_schema = schema or self.default_schema_name
- full_tname = "%s.%s" % (current_schema, tablename)
-
rp = connection.execute(
sql.text("select ind.index_id, ind.is_unique, ind.name "
"from sys.indexes as ind join sys.tables as tab on "
@@ -1269,20 +1227,20 @@ class MSDialect(default.DefaultDialect):
bindparams=[
sql.bindparam('tabname', tablename,
sqltypes.String(convert_unicode=True)),
- sql.bindparam('schname', current_schema,
+ sql.bindparam('schname', owner,
sqltypes.String(convert_unicode=True))
],
- typemap = {
- 'name':sqltypes.Unicode()
+ typemap={
+ 'name': sqltypes.Unicode()
}
)
)
indexes = {}
for row in rp:
indexes[row['index_id']] = {
- 'name':row['name'],
- 'unique':row['is_unique'] == 1,
- 'column_names':[]
+ 'name': row['name'],
+ 'unique': row['is_unique'] == 1,
+ 'column_names': []
}
rp = connection.execute(
sql.text(
@@ -1298,24 +1256,21 @@ class MSDialect(default.DefaultDialect):
bindparams=[
sql.bindparam('tabname', tablename,
sqltypes.String(convert_unicode=True)),
- sql.bindparam('schname', current_schema,
+ sql.bindparam('schname', owner,
sqltypes.String(convert_unicode=True))
],
- typemap = {
- 'name':sqltypes.Unicode()
- }
+ typemap={'name': sqltypes.Unicode()}
),
)
for row in rp:
if row['index_id'] in indexes:
indexes[row['index_id']]['column_names'].append(row['name'])
- return indexes.values()
+ return list(indexes.values())
@reflection.cache
- def get_view_definition(self, connection, viewname, schema=None, **kw):
- current_schema = schema or self.default_schema_name
-
+ @_db_plus_owner
+ def get_view_definition(self, connection, viewname, dbname, owner, schema, **kw):
rp = connection.execute(
sql.text(
"select definition from sys.sql_modules as mod, "
@@ -1328,7 +1283,7 @@ class MSDialect(default.DefaultDialect):
bindparams=[
sql.bindparam('viewname', viewname,
sqltypes.String(convert_unicode=True)),
- sql.bindparam('schname', current_schema,
+ sql.bindparam('schname', owner,
sqltypes.String(convert_unicode=True))
]
)
@@ -1339,17 +1294,18 @@ class MSDialect(default.DefaultDialect):
return view_def
@reflection.cache
- def get_columns(self, connection, tablename, schema=None, **kw):
+ @_db_plus_owner
+ def get_columns(self, connection, tablename, dbname, owner, schema, **kw):
# Get base columns
- current_schema = schema or self.default_schema_name
columns = ischema.columns
- if current_schema:
- whereclause = sql.and_(columns.c.table_name==tablename,
- columns.c.table_schema==current_schema)
+ if owner:
+ whereclause = sql.and_(columns.c.table_name == tablename,
+ columns.c.table_schema == owner)
else:
- whereclause = columns.c.table_name==tablename
+ whereclause = columns.c.table_name == tablename
s = sql.select([columns], whereclause,
order_by=[columns.c.ordinal_position])
+
c = connection.execute(s)
cols = []
while True:
@@ -1393,11 +1349,11 @@ class MSDialect(default.DefaultDialect):
coltype = coltype(**kwargs)
cdict = {
- 'name' : name,
- 'type' : coltype,
- 'nullable' : nullable,
- 'default' : default,
- 'autoincrement':False,
+ 'name': name,
+ 'type': coltype,
+ 'nullable': nullable,
+ 'default': default,
+ 'autoincrement': False,
}
cols.append(cdict)
# autoincrement and identity
@@ -1407,7 +1363,7 @@ class MSDialect(default.DefaultDialect):
# We also run an sp_columns to check for identity columns:
cursor = connection.execute("sp_columns @table_name = '%s', "
"@table_owner = '%s'"
- % (tablename, current_schema))
+ % (tablename, owner))
ic = None
while True:
row = cursor.fetchone()
@@ -1423,7 +1379,7 @@ class MSDialect(default.DefaultDialect):
cursor.close()
if ic is not None and self.server_version_info >= MS_2005_VERSION:
- table_fullname = "%s.%s" % (current_schema, tablename)
+ table_fullname = "%s.%s" % (owner, tablename)
cursor = connection.execute(
"select ident_seed('%s'), ident_incr('%s')"
% (table_fullname, table_fullname)
@@ -1432,53 +1388,40 @@ class MSDialect(default.DefaultDialect):
row = cursor.first()
if row is not None and row[0] is not None:
colmap[ic]['sequence'].update({
- 'start' : int(row[0]),
- 'increment' : int(row[1])
+ 'start': int(row[0]),
+ 'increment': int(row[1])
})
return cols
@reflection.cache
- def get_primary_keys(self, connection, tablename, schema=None, **kw):
- current_schema = schema or self.default_schema_name
+ @_db_plus_owner
+ def get_pk_constraint(self, connection, tablename, dbname, owner, schema, **kw):
pkeys = []
- # information_schema.referential_constraints
- RR = ischema.ref_constraints
- # information_schema.table_constraints
TC = ischema.constraints
- # information_schema.constraint_column_usage:
- # the constrained column
- C = ischema.key_constraints.alias('C')
- # information_schema.constraint_column_usage:
- # the referenced column
- R = ischema.key_constraints.alias('R')
+ C = ischema.key_constraints.alias('C')
# Primary key constraints
- s = sql.select([C.c.column_name, TC.c.constraint_type],
+ s = sql.select([C.c.column_name, TC.c.constraint_type, C.c.constraint_name],
sql.and_(TC.c.constraint_name == C.c.constraint_name,
- TC.c.table_schema == C.c.table_schema,
+ TC.c.table_schema == C.c.table_schema,
C.c.table_name == tablename,
- C.c.table_schema == current_schema)
+ C.c.table_schema == owner)
)
c = connection.execute(s)
+ constraint_name = None
for row in c:
if 'PRIMARY' in row[TC.c.constraint_type.name]:
pkeys.append(row[0])
- return pkeys
+ if constraint_name is None:
+ constraint_name = row[C.c.constraint_name.name]
+ return {'constrained_columns': pkeys, 'name': constraint_name}
@reflection.cache
- def get_foreign_keys(self, connection, tablename, schema=None, **kw):
- current_schema = schema or self.default_schema_name
- # Add constraints
- #information_schema.referential_constraints
+ @_db_plus_owner
+ def get_foreign_keys(self, connection, tablename, dbname, owner, schema, **kw):
RR = ischema.ref_constraints
- # information_schema.table_constraints
- TC = ischema.constraints
- # information_schema.constraint_column_usage:
- # the constrained column
- C = ischema.key_constraints.alias('C')
- # information_schema.constraint_column_usage:
- # the referenced column
- R = ischema.key_constraints.alias('R')
+ C = ischema.key_constraints.alias('C')
+ R = ischema.key_constraints.alias('R')
# Foreign key constraints
s = sql.select([C.c.column_name,
@@ -1487,16 +1430,14 @@ class MSDialect(default.DefaultDialect):
RR.c.update_rule,
RR.c.delete_rule],
sql.and_(C.c.table_name == tablename,
- C.c.table_schema == current_schema,
+ C.c.table_schema == owner,
C.c.constraint_name == RR.c.constraint_name,
R.c.constraint_name ==
RR.c.unique_constraint_name,
C.c.ordinal_position == R.c.ordinal_position
),
- order_by = [
- RR.c.constraint_name,
- R.c.ordinal_position])
-
+ order_by=[RR.c.constraint_name, R.c.ordinal_position]
+ )
# group rows by constraint ID, to handle multi-column FKs
fkeys = []
@@ -1504,11 +1445,11 @@ class MSDialect(default.DefaultDialect):
def fkey_rec():
return {
- 'name' : None,
- 'constrained_columns' : [],
- 'referred_schema' : None,
- 'referred_table' : None,
- 'referred_columns' : []
+ 'name': None,
+ 'constrained_columns': [],
+ 'referred_schema': None,
+ 'referred_table': None,
+ 'referred_columns': []
}
fkeys = util.defaultdict(fkey_rec)
@@ -1520,8 +1461,9 @@ class MSDialect(default.DefaultDialect):
rec['name'] = rfknm
if not rec['referred_table']:
rec['referred_table'] = rtbl
-
- if schema is not None or current_schema != rschema:
+ if schema is not None or owner != rschema:
+ if dbname:
+ rschema = dbname + "." + rschema
rec['referred_schema'] = rschema
local_cols, remote_cols = \
@@ -1531,5 +1473,4 @@ class MSDialect(default.DefaultDialect):
local_cols.append(scol)
remote_cols.append(rcol)
- return fkeys.values()
-
+ return list(fkeys.values())
diff --git a/libs/sqlalchemy/dialects/mssql/information_schema.py b/libs/sqlalchemy/dialects/mssql/information_schema.py
index 0dcddae9..26e70f7f 100644
--- a/libs/sqlalchemy/dialects/mssql/information_schema.py
+++ b/libs/sqlalchemy/dialects/mssql/information_schema.py
@@ -1,13 +1,17 @@
# mssql/information_schema.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
# TODO: should be using the sys. catalog with SQL Server, not information schema
-from sqlalchemy import Table, MetaData, Column
-from sqlalchemy.types import String, Unicode, Integer, TypeDecorator
+from ... import Table, MetaData, Column
+from ...types import String, Unicode, UnicodeText, Integer, TypeDecorator
+from ... import cast
+from ... import util
+from ...sql import expression
+from ...ext.compiler import compiles
ischema = MetaData()
@@ -15,12 +19,25 @@ class CoerceUnicode(TypeDecorator):
impl = Unicode
def process_bind_param(self, value, dialect):
- # Py2K
- if isinstance(value, str):
+ if util.py2k and isinstance(value, util.binary_type):
value = value.decode(dialect.encoding)
- # end Py2K
return value
+ def bind_expression(self, bindvalue):
+ return _cast_on_2005(bindvalue)
+
+class _cast_on_2005(expression.ColumnElement):
+ def __init__(self, bindvalue):
+ self.bindvalue = bindvalue
+
+@compiles(_cast_on_2005)
+def _compile(element, compiler, **kw):
+ from . import base
+ if compiler.dialect.server_version_info < base.MS_2005_VERSION:
+ return compiler.process(element.bindvalue, **kw)
+ else:
+ return compiler.process(cast(element.bindvalue, Unicode), **kw)
+
schemata = Table("SCHEMATA", ischema,
Column("CATALOG_NAME", CoerceUnicode, key="catalog_name"),
Column("SCHEMA_NAME", CoerceUnicode, key="schema_name"),
@@ -95,4 +112,3 @@ views = Table("VIEWS", ischema,
Column("CHECK_OPTION", String, key="check_option"),
Column("IS_UPDATABLE", String, key="is_updatable"),
schema="INFORMATION_SCHEMA")
-
diff --git a/libs/sqlalchemy/dialects/mssql/mxodbc.py b/libs/sqlalchemy/dialects/mssql/mxodbc.py
index 56a72f41..5b686c47 100644
--- a/libs/sqlalchemy/dialects/mssql/mxodbc.py
+++ b/libs/sqlalchemy/dialects/mssql/mxodbc.py
@@ -1,28 +1,18 @@
# mssql/mxodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for MS-SQL via mxODBC.
-
-mxODBC is available at:
-
- http://www.egenix.com/
-
-This was tested with mxODBC 3.1.2 and the SQL Server Native
-Client connected to MSSQL 2005 and 2008 Express Editions.
-
-Connecting
-~~~~~~~~~~
-
-Connection is via DSN::
-
- mssql+mxodbc://:@
+.. dialect:: mssql+mxodbc
+ :name: mxODBC
+ :dbapi: mxodbc
+ :connectstring: mssql+mxodbc://:@
+ :url: http://www.egenix.com/
Execution Modes
-~~~~~~~~~~~~~~~
+---------------
mxODBC features two styles of statement execution, using the
``cursor.execute()`` and ``cursor.executedirect()`` methods (the second being
@@ -52,14 +42,38 @@ of ``False`` will unconditionally use string-escaped parameters.
"""
-from sqlalchemy import types as sqltypes
-from sqlalchemy.connectors.mxodbc import MxODBCConnector
-from sqlalchemy.dialects.mssql.pyodbc import MSExecutionContext_pyodbc
-from sqlalchemy.dialects.mssql.base import (MSDialect,
+from ... import types as sqltypes
+from ...connectors.mxodbc import MxODBCConnector
+from .pyodbc import MSExecutionContext_pyodbc, _MSNumeric_pyodbc
+from .base import (MSDialect,
MSSQLStrictCompiler,
- _MSDateTime, _MSDate, TIME)
+ _MSDateTime, _MSDate, _MSTime)
+class _MSNumeric_mxodbc(_MSNumeric_pyodbc):
+ """Include pyodbc's numeric processor.
+ """
+
+
+class _MSDate_mxodbc(_MSDate):
+ def bind_processor(self, dialect):
+ def process(value):
+ if value is not None:
+ return "%s-%s-%s" % (value.year, value.month, value.day)
+ else:
+ return None
+ return process
+
+
+class _MSTime_mxodbc(_MSTime):
+ def bind_processor(self, dialect):
+ def process(value):
+ if value is not None:
+ return "%s:%s:%s" % (value.hour, value.minute, value.second)
+ else:
+ return None
+ return process
+
class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc):
"""
@@ -71,23 +85,27 @@ class MSExecutionContext_mxodbc(MSExecutionContext_pyodbc):
# is really only being used in cases where OUTPUT
# won't work.
+
class MSDialect_mxodbc(MxODBCConnector, MSDialect):
- # TODO: may want to use this only if FreeTDS is not in use,
- # since FreeTDS doesn't seem to use native binds.
- statement_compiler = MSSQLStrictCompiler
+ # this is only needed if "native ODBC" mode is used,
+ # which is now disabled by default.
+ #statement_compiler = MSSQLStrictCompiler
+
execution_ctx_cls = MSExecutionContext_mxodbc
+
+ # flag used by _MSNumeric_mxodbc
+ _need_decimal_fix = True
+
colspecs = {
- #sqltypes.Numeric : _MSNumeric,
- sqltypes.DateTime : _MSDateTime,
- sqltypes.Date : _MSDate,
- sqltypes.Time : TIME,
+ sqltypes.Numeric: _MSNumeric_mxodbc,
+ sqltypes.DateTime: _MSDateTime,
+ sqltypes.Date: _MSDate_mxodbc,
+ sqltypes.Time: _MSTime_mxodbc,
}
-
- def __init__(self, description_encoding='latin-1', **params):
+ def __init__(self, description_encoding=None, **params):
super(MSDialect_mxodbc, self).__init__(**params)
self.description_encoding = description_encoding
dialect = MSDialect_mxodbc
-
diff --git a/libs/sqlalchemy/dialects/mssql/pymssql.py b/libs/sqlalchemy/dialects/mssql/pymssql.py
index 8229d6ce..021219cb 100644
--- a/libs/sqlalchemy/dialects/mssql/pymssql.py
+++ b/libs/sqlalchemy/dialects/mssql/pymssql.py
@@ -1,32 +1,18 @@
# mssql/pymssql.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for the pymssql dialect.
-
-This dialect supports pymssql 1.0 and greater.
-
-pymssql is available at:
-
- http://pymssql.sourceforge.net/
-
-Connecting
-^^^^^^^^^^
-
-Sample connect string::
-
- mssql+pymssql://:@
-
-Adding "?charset=utf8" or similar will cause pymssql to return
-strings as Python unicode objects. This can potentially improve
-performance in some scenarios as decoding of strings is
-handled natively.
+.. dialect:: mssql+pymssql
+ :name: pymssql
+ :dbapi: pymssql
+ :connectstring: mssql+pymssql://:@?charset=utf8
+ :url: http://pymssql.sourceforge.net/
Limitations
-^^^^^^^^^^^
+-----------
pymssql inherits a lot of limitations from FreeTDS, including:
@@ -38,10 +24,11 @@ pymssql inherits a lot of limitations from FreeTDS, including:
Please consult the pymssql documentation for further information.
"""
-from sqlalchemy.dialects.mssql.base import MSDialect
-from sqlalchemy import types as sqltypes, util, processors
+from .base import MSDialect
+from ... import types as sqltypes, util, processors
import re
+
class _MSNumeric_pymssql(sqltypes.Numeric):
def result_processor(self, dialect, type_):
if not self.asdecimal:
@@ -49,6 +36,7 @@ class _MSNumeric_pymssql(sqltypes.Numeric):
else:
return sqltypes.Numeric.result_processor(self, dialect, type_)
+
class MSDialect_pymssql(MSDialect):
supports_sane_rowcount = False
driver = 'pymssql'
@@ -56,16 +44,17 @@ class MSDialect_pymssql(MSDialect):
colspecs = util.update_copy(
MSDialect.colspecs,
{
- sqltypes.Numeric:_MSNumeric_pymssql,
- sqltypes.Float:sqltypes.Float,
+ sqltypes.Numeric: _MSNumeric_pymssql,
+ sqltypes.Float: sqltypes.Float,
}
)
+
@classmethod
def dbapi(cls):
module = __import__('pymssql')
# pymmsql doesn't have a Binary method. we use string
# TODO: monkeypatching here is less than ideal
- module.Binary = str
+ module.Binary = lambda x: x if hasattr(x, 'decode') else str(x)
client_ver = tuple(int(x) for x in module.__version__.split("."))
if client_ver < (1, ):
@@ -96,6 +85,9 @@ class MSDialect_pymssql(MSDialect):
def is_disconnect(self, e, connection, cursor):
for msg in (
+ "Adaptive Server connection timed out",
+ "Net-Lib error during Connection reset by peer",
+ "message 20003", # connection timeout
"Error 10054",
"Not connected to any MS SQL server",
"Connection is closed"
diff --git a/libs/sqlalchemy/dialects/mssql/pyodbc.py b/libs/sqlalchemy/dialects/mssql/pyodbc.py
index 389018c6..8c43eb8a 100644
--- a/libs/sqlalchemy/dialects/mssql/pyodbc.py
+++ b/libs/sqlalchemy/dialects/mssql/pyodbc.py
@@ -1,18 +1,18 @@
# mssql/pyodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for MS-SQL via pyodbc.
+.. dialect:: mssql+pyodbc
+ :name: PyODBC
+ :dbapi: pyodbc
+ :connectstring: mssql+pyodbc://:@
+ :url: http://pypi.python.org/pypi/pyodbc/
-pyodbc is available at:
-
- http://pypi.python.org/pypi/pyodbc/
-
-Connecting
-^^^^^^^^^^
+Additional Connection Examples
+-------------------------------
Examples of pyodbc connection string URLs:
@@ -81,7 +81,7 @@ the python shell. For example::
'dsn%3Dmydsn%3BDatabase%3Ddb'
Unicode Binds
-^^^^^^^^^^^^^
+-------------
The current state of PyODBC on a unix backend with FreeTDS and/or
EasySoft is poor regarding unicode; different OS platforms and versions of UnixODBC
@@ -111,23 +111,23 @@ for unix + PyODBC.
"""
-from sqlalchemy.dialects.mssql.base import MSExecutionContext, MSDialect
-from sqlalchemy.connectors.pyodbc import PyODBCConnector
-from sqlalchemy import types as sqltypes, util
+from .base import MSExecutionContext, MSDialect
+from ...connectors.pyodbc import PyODBCConnector
+from ... import types as sqltypes, util
import decimal
-class _MSNumeric_pyodbc(sqltypes.Numeric):
+class _ms_numeric_pyodbc(object):
+
"""Turns Decimals with adjusted() < 0 or > 7 into strings.
- This is the only method that is proven to work with Pyodbc+MSSQL
- without crashing (floats can be used but seem to cause sporadic
- crashes).
+ The routines here are needed for older pyodbc versions
+ as well as current mxODBC versions.
"""
def bind_processor(self, dialect):
- super_process = super(_MSNumeric_pyodbc, self).\
+ super_process = super(_ms_numeric_pyodbc, self).\
bind_processor(dialect)
if not dialect._need_decimal_fix:
@@ -164,7 +164,7 @@ class _MSNumeric_pyodbc(sqltypes.Numeric):
result = "%s%s%s" % (
(value < 0 and '-' or ''),
"".join([str(s) for s in _int]),
- "0" * (value.adjusted() - (len(_int)-1)))
+ "0" * (value.adjusted() - (len(_int) - 1)))
else:
if (len(_int) - 1) > value.adjusted():
result = "%s%s.%s" % (
@@ -180,6 +180,11 @@ class _MSNumeric_pyodbc(sqltypes.Numeric):
[str(s) for s in _int][0:value.adjusted() + 1]))
return result
+class _MSNumeric_pyodbc(_ms_numeric_pyodbc, sqltypes.Numeric):
+ pass
+
+class _MSFloat_pyodbc(_ms_numeric_pyodbc, sqltypes.Float):
+ pass
class MSExecutionContext_pyodbc(MSExecutionContext):
_embedded_scope_identity = False
@@ -219,7 +224,7 @@ class MSExecutionContext_pyodbc(MSExecutionContext):
# without closing it (FreeTDS particularly)
row = self.cursor.fetchall()[0]
break
- except self.dialect.dbapi.Error, e:
+ except self.dialect.dbapi.Error as e:
# no way around this - nextset() consumes the previous set
# so we need to just keep flipping
self.cursor.nextset()
@@ -238,11 +243,12 @@ class MSDialect_pyodbc(PyODBCConnector, MSDialect):
colspecs = util.update_copy(
MSDialect.colspecs,
{
- sqltypes.Numeric:_MSNumeric_pyodbc
+ sqltypes.Numeric: _MSNumeric_pyodbc,
+ sqltypes.Float: _MSFloat_pyodbc
}
)
- def __init__(self, description_encoding='latin-1', **params):
+ def __init__(self, description_encoding=None, **params):
super(MSDialect_pyodbc, self).__init__(**params)
self.description_encoding = description_encoding
self.use_scope_identity = self.use_scope_identity and \
diff --git a/libs/sqlalchemy/dialects/mssql/zxjdbc.py b/libs/sqlalchemy/dialects/mssql/zxjdbc.py
index 842225da..706eef3a 100644
--- a/libs/sqlalchemy/dialects/mssql/zxjdbc.py
+++ b/libs/sqlalchemy/dialects/mssql/zxjdbc.py
@@ -1,32 +1,22 @@
# mssql/zxjdbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Microsoft SQL Server database via the zxjdbc JDBC
-connector.
+"""
+.. dialect:: mssql+zxjdbc
+ :name: zxJDBC for Jython
+ :dbapi: zxjdbc
+ :connectstring: mssql+zxjdbc://user:pass@host:port/dbname[?key=value&key=value...]
+ :driverurl: http://jtds.sourceforge.net/
-JDBC Driver
------------
-
-Requires the jTDS driver, available from: http://jtds.sourceforge.net/
-
-Connecting
-----------
-
-URLs are of the standard form of
-``mssql+zxjdbc://user:pass@host:port/dbname[?key=value&key=value...]``.
-
-Additional arguments which may be specified either as query string
-arguments on the URL, or as keyword arguments to
-:func:`~sqlalchemy.create_engine()` will be passed as Connection
-properties to the underlying JDBC driver.
"""
-from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
-from sqlalchemy.dialects.mssql.base import MSDialect, MSExecutionContext
-from sqlalchemy.engine import base
+from ...connectors.zxJDBC import ZxJDBCConnector
+from .base import MSDialect, MSExecutionContext
+from ... import engine
+
class MSExecutionContext_zxjdbc(MSExecutionContext):
@@ -46,13 +36,13 @@ class MSExecutionContext_zxjdbc(MSExecutionContext):
try:
row = self.cursor.fetchall()[0]
break
- except self.dialect.dbapi.Error, e:
+ except self.dialect.dbapi.Error:
self.cursor.nextset()
self._lastrowid = int(row[0])
if (self.isinsert or self.isupdate or self.isdelete) and \
self.compiled.returning:
- self._result_proxy = base.FullyBufferedResultProxy(self)
+ self._result_proxy = engine.FullyBufferedResultProxy(self)
if self._enable_identity_insert:
table = self.dialect.identifier_preparer.format_table(
diff --git a/libs/sqlalchemy/dialects/mysql/__init__.py b/libs/sqlalchemy/dialects/mysql/__init__.py
index c41dd0b1..4eb8cc6d 100644
--- a/libs/sqlalchemy/dialects/mysql/__init__.py
+++ b/libs/sqlalchemy/dialects/mysql/__init__.py
@@ -1,17 +1,17 @@
# mysql/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from sqlalchemy.dialects.mysql import base, mysqldb, oursql, \
+from . import base, mysqldb, oursql, \
pyodbc, zxjdbc, mysqlconnector, pymysql,\
- gaerdbms
+ gaerdbms, cymysql
# default dialect
base.dialect = mysqldb.dialect
-from sqlalchemy.dialects.mysql.base import \
+from .base import \
BIGINT, BINARY, BIT, BLOB, BOOLEAN, CHAR, DATE, DATETIME, \
DECIMAL, DOUBLE, ENUM, DECIMAL,\
FLOAT, INTEGER, INTEGER, LONGBLOB, LONGTEXT, MEDIUMBLOB, \
diff --git a/libs/sqlalchemy/dialects/mysql/base.py b/libs/sqlalchemy/dialects/mysql/base.py
index ea180eee..a3942e89 100644
--- a/libs/sqlalchemy/dialects/mysql/base.py
+++ b/libs/sqlalchemy/dialects/mysql/base.py
@@ -1,10 +1,13 @@
# mysql/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database.
+"""
+
+.. dialect:: mysql
+ :name: MySQL
Supported Versions and Features
-------------------------------
@@ -17,10 +20,7 @@ example, they won't work in SQLAlchemy either.
See the official MySQL documentation for detailed information about features
supported in any given server release.
-Connecting
-----------
-
-See the API documentation on individual drivers for details on connecting.
+.. _mysql_connection_timeouts:
Connection Timeouts
-------------------
@@ -55,7 +55,9 @@ creation option can be specified in this syntax::
.. seealso::
- `The InnoDB Storage Engine `_ - on the MySQL website.
+ `The InnoDB Storage Engine
+ `_ -
+ on the MySQL website.
Case Sensitivity and Table Reflection
-------------------------------------
@@ -125,32 +127,20 @@ engines::
Column('id', Integer, primary_key=True)
)
-SQL Mode
---------
+Ansi Quoting Style
+------------------
-MySQL SQL modes are supported. Modes that enable ``ANSI_QUOTES`` (such as
-``ANSI``) require an engine option to modify SQLAlchemy's quoting style.
-When using an ANSI-quoting mode, supply ``use_ansiquotes=True`` when
-creating your ``Engine``::
+MySQL features two varieties of identifier "quoting style", one using
+backticks and the other using quotes, e.g. ```some_identifier``` vs.
+``"some_identifier"``. All MySQL dialects detect which version
+is in use by checking the value of ``sql_mode`` when a connection is first
+established with a particular :class:`.Engine`. This quoting style comes
+into play when rendering table and column names as well as when reflecting
+existing database structures. The detection is entirely automatic and
+no special configuration is needed to use either quoting style.
- create_engine('mysql://localhost/test', use_ansiquotes=True)
-
-This is an engine-wide option and is not toggleable on a per-connection basis.
-SQLAlchemy does not presume to ``SET sql_mode`` for you with this option. For
-the best performance, set the quoting style server-wide in ``my.cnf`` or by
-supplying ``--sql-mode`` to ``mysqld``. You can also use a
-:class:`sqlalchemy.pool.Pool` listener hook to issue a ``SET SESSION
-sql_mode='...'`` on connect to configure each connection.
-
-If you do not specify ``use_ansiquotes``, the regular MySQL quoting style is
-used by default.
-
-If you do issue a ``SET sql_mode`` through SQLAlchemy, the dialect must be
-updated if the quoting style is changed. Again, this change will affect all
-connections::
-
- connection.execute('SET sql_mode="ansi"')
- connection.dialect.use_ansiquotes = True
+.. versionchanged:: 0.6 detection of ANSI quoting style is entirely automatic,
+ there's no longer any end-user ``create_engine()`` options in this regard.
MySQL SQL Extensions
--------------------
@@ -202,13 +192,13 @@ detection, instead rendering the internal expression directly.
CAST may still not be desirable on an early MySQL version post-4.0.2, as it didn't
add all datatype support until 4.1.1. If your application falls into this
-narrow area, the behavior of CAST can be controlled using the :ref:`sqlalchemy.ext.compiler_toplevel`
-system, as per the recipe below::
+narrow area, the behavior of CAST can be controlled using the
+:ref:`sqlalchemy.ext.compiler_toplevel` system, as per the recipe below::
- from sqlalchemy.sql.expression import _Cast
+ from sqlalchemy.sql.expression import Cast
from sqlalchemy.ext.compiler import compiles
- @compiles(_Cast, 'mysql')
+ @compiles(Cast, 'mysql')
def _check_mysql_version(element, compiler, **kw):
if compiler.dialect.server_version_info < (4, 1, 0):
return compiler.process(element.clause, **kw)
@@ -239,11 +229,18 @@ become part of the index. SQLAlchemy provides this feature via the
Index('my_index', my_table.c.data, mysql_length=10)
+ Index('a_b_idx', my_table.c.a, my_table.c.b, mysql_length={'a': 4, 'b': 9})
+
Prefix lengths are given in characters for nonbinary string types and in bytes
-for binary string types. The value passed to the keyword argument will be
-simply passed through to the underlying CREATE INDEX command, so it *must* be
-an integer. MySQL only allows a length for an index if it is for a CHAR,
-VARCHAR, TEXT, BINARY, VARBINARY and BLOB.
+for binary string types. The value passed to the keyword argument *must* be
+either an integer (and, thus, specify the same prefix length value for all
+columns of the index) or a dict in which keys are column names and values are
+prefix length values for corresponding columns. MySQL only allows a length for
+a column of an index if it is for a CHAR, VARCHAR, TEXT, BINARY, VARBINARY and
+BLOB.
+
+.. versionadded:: 0.8.2 ``mysql_length`` may now be specified as a dictionary
+ for use with composite indexes.
Index Types
~~~~~~~~~~~~~
@@ -268,26 +265,62 @@ http://dev.mysql.com/doc/refman/5.0/en/create-index.html
http://dev.mysql.com/doc/refman/5.0/en/create-table.html
+.. _mysql_foreign_keys:
+
+MySQL Foreign Key Options
+-------------------------
+
+MySQL does not support the foreign key arguments "DEFERRABLE", "INITIALLY",
+or "MATCH". Using the ``deferrable`` or ``initially`` keyword argument with
+:class:`.ForeignKeyConstraint` or :class:`.ForeignKey` will have the effect of these keywords being
+rendered in a DDL expression, which will then raise an error on MySQL.
+In order to use these keywords on a foreign key while having them ignored
+on a MySQL backend, use a custom compile rule::
+
+ from sqlalchemy.ext.compiler import compiles
+ from sqlalchemy.schema import ForeignKeyConstraint
+
+ @compiles(ForeignKeyConstraint, "mysql")
+ def process(element, compiler, **kw):
+ element.deferrable = element.initially = None
+ return compiler.visit_foreign_key_constraint(element, **kw)
+
+.. versionchanged:: 0.9.0 - the MySQL backend no longer silently ignores
+ the ``deferrable`` or ``initially`` keyword arguments of :class:`.ForeignKeyConstraint`
+ and :class:`.ForeignKey`.
+
+The "MATCH" keyword is in fact more insidious, and is explicitly disallowed
+by SQLAlchemy in conjunction with the MySQL backend. This argument is silently
+ignored by MySQL, but in addition has the effect of ON UPDATE and ON DELETE options
+also being ignored by the backend. Therefore MATCH should never be used with the
+MySQL backend; as is the case with DEFERRABLE and INITIALLY, custom compilation
+rules can be used to correct a MySQL ForeignKeyConstraint at DDL definition time.
+
+.. versionadded:: 0.9.0 - the MySQL backend will raise a :class:`.CompileError`
+ when the ``match`` keyword is used with :class:`.ForeignKeyConstraint`
+ or :class:`.ForeignKey`.
+
"""
-import datetime, inspect, re, sys
+import datetime
+import inspect
+import re
+import sys
-from sqlalchemy import schema as sa_schema
-from sqlalchemy import exc, log, sql, util
-from sqlalchemy.sql import operators as sql_operators
-from sqlalchemy.sql import functions as sql_functions
-from sqlalchemy.sql import compiler
+from ... import schema as sa_schema
+from ... import exc, log, sql, util
+from ...sql import compiler
from array import array as _array
-from sqlalchemy.engine import reflection
-from sqlalchemy.engine import base as engine_base, default
-from sqlalchemy import types as sqltypes
-from sqlalchemy.util import topological
-from sqlalchemy.types import DATE, DATETIME, BOOLEAN, TIME, \
+from ...engine import reflection
+from ...engine import default
+from ... import types as sqltypes
+from ...util import topological
+from ...types import DATE, DATETIME, BOOLEAN, TIME, \
BLOB, BINARY, VARBINARY
RESERVED_WORDS = set(
- ['accessible', 'add', 'all', 'alter', 'analyze','and', 'as', 'asc',
+ ['accessible', 'add', 'all', 'alter', 'analyze', 'and', 'as', 'asc',
'asensitive', 'before', 'between', 'bigint', 'binary', 'blob', 'both',
'by', 'call', 'cascade', 'case', 'change', 'char', 'character', 'check',
'collate', 'column', 'condition', 'constraint', 'continue', 'convert',
@@ -322,10 +355,20 @@ RESERVED_WORDS = set(
'union', 'unique', 'unlock', 'unsigned', 'update', 'usage', 'use',
'using', 'utc_date', 'utc_time', 'utc_timestamp', 'values', 'varbinary',
'varchar', 'varcharacter', 'varying', 'when', 'where', 'while', 'with',
- 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0
- 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1
+
+ 'write', 'x509', 'xor', 'year_month', 'zerofill', # 5.0
+
+ 'columns', 'fields', 'privileges', 'soname', 'tables', # 4.1
+
'accessible', 'linear', 'master_ssl_verify_server_cert', 'range',
- 'read_only', 'read_write', # 5.1
+ 'read_only', 'read_write', # 5.1
+
+ 'general', 'ignore_server_ids', 'master_heartbeat_period', 'maxvalue',
+ 'resignal', 'signal', 'slow', # 5.5
+
+ 'get', 'io_after_gtids', 'io_before_gtids', 'master_bind', 'one_shot',
+ 'partition', 'sql_after_gtids', 'sql_before_gtids', # 5.6
+
])
AUTOCOMMIT_RE = re.compile(
@@ -337,13 +380,22 @@ SET_RE = re.compile(
class _NumericType(object):
- """Base for MySQL numeric types."""
+ """Base for MySQL numeric types.
+
+ This is the base both for NUMERIC as well as INTEGER, hence
+ it's a mixin.
+
+ """
def __init__(self, unsigned=False, zerofill=False, **kw):
self.unsigned = unsigned
self.zerofill = zerofill
super(_NumericType, self).__init__(**kw)
+ def __repr__(self):
+ return util.generic_repr(self,
+ to_inspect=[_NumericType, sqltypes.Numeric])
+
class _FloatType(_NumericType, sqltypes.Float):
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
if isinstance(self, (REAL, DOUBLE)) and \
@@ -354,48 +406,42 @@ class _FloatType(_NumericType, sqltypes.Float):
raise exc.ArgumentError(
"You must specify both precision and scale or omit "
"both altogether.")
-
super(_FloatType, self).__init__(precision=precision, asdecimal=asdecimal, **kw)
self.scale = scale
+ def __repr__(self):
+ return util.generic_repr(self,
+ to_inspect=[_FloatType, _NumericType, sqltypes.Float])
+
class _IntegerType(_NumericType, sqltypes.Integer):
def __init__(self, display_width=None, **kw):
self.display_width = display_width
super(_IntegerType, self).__init__(**kw)
+ def __repr__(self):
+ return util.generic_repr(self,
+ to_inspect=[_IntegerType, _NumericType, sqltypes.Integer])
+
class _StringType(sqltypes.String):
"""Base for MySQL string types."""
def __init__(self, charset=None, collation=None,
- ascii=False, binary=False,
+ ascii=False, binary=False, unicode=False,
national=False, **kw):
self.charset = charset
+
# allow collate= or collation=
- self.collation = kw.pop('collate', collation)
+ kw.setdefault('collation', kw.pop('collate', collation))
+
self.ascii = ascii
- # We have to munge the 'unicode' param strictly as a dict
- # otherwise 2to3 will turn it into str.
- self.__dict__['unicode'] = kw.get('unicode', False)
- # sqltypes.String does not accept the 'unicode' arg at all.
- if 'unicode' in kw:
- del kw['unicode']
+ self.unicode = unicode
self.binary = binary
self.national = national
super(_StringType, self).__init__(**kw)
def __repr__(self):
- attributes = inspect.getargspec(self.__init__)[0][1:]
- attributes.extend(inspect.getargspec(_StringType.__init__)[0][1:])
-
- params = {}
- for attr in attributes:
- val = getattr(self, attr)
- if val is not None and val is not False:
- params[attr] = val
-
- return "%s(%s)" % (self.__class__.__name__,
- ', '.join(['%s=%r' % (k, params[k]) for k in params]))
-
+ return util.generic_repr(self,
+ to_inspect=[_StringType, sqltypes.String])
class NUMERIC(_NumericType, sqltypes.NUMERIC):
"""MySQL NUMERIC type."""
@@ -418,7 +464,8 @@ class NUMERIC(_NumericType, sqltypes.NUMERIC):
numeric.
"""
- super(NUMERIC, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal, **kw)
+ super(NUMERIC, self).__init__(precision=precision,
+ scale=scale, asdecimal=asdecimal, **kw)
class DECIMAL(_NumericType, sqltypes.DECIMAL):
@@ -454,6 +501,14 @@ class DOUBLE(_FloatType):
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
"""Construct a DOUBLE.
+ .. note::
+
+ The :class:`.DOUBLE` type by default converts from float
+ to Decimal, using a truncation that defaults to 10 digits. Specify
+ either ``scale=n`` or ``decimal_return_scale=n`` in order to change
+ this scale, or ``asdecimal=False`` to return values directly as
+ Python floating points.
+
:param precision: Total digits in this number. If scale and precision
are both None, values are stored to limits allowed by the server.
@@ -470,6 +525,7 @@ class DOUBLE(_FloatType):
super(DOUBLE, self).__init__(precision=precision, scale=scale,
asdecimal=asdecimal, **kw)
+
class REAL(_FloatType, sqltypes.REAL):
"""MySQL REAL type."""
@@ -478,6 +534,14 @@ class REAL(_FloatType, sqltypes.REAL):
def __init__(self, precision=None, scale=None, asdecimal=True, **kw):
"""Construct a REAL.
+ .. note::
+
+ The :class:`.REAL` type by default converts from float
+ to Decimal, using a truncation that defaults to 10 digits. Specify
+ either ``scale=n`` or ``decimal_return_scale=n`` in order to change
+ this scale, or ``asdecimal=False`` to return values directly as
+ Python floating points.
+
:param precision: Total digits in this number. If scale and precision
are both None, values are stored to limits allowed by the server.
@@ -494,6 +558,7 @@ class REAL(_FloatType, sqltypes.REAL):
super(REAL, self).__init__(precision=precision, scale=scale,
asdecimal=asdecimal, **kw)
+
class FLOAT(_FloatType, sqltypes.FLOAT):
"""MySQL FLOAT type."""
@@ -521,6 +586,7 @@ class FLOAT(_FloatType, sqltypes.FLOAT):
def bind_processor(self, dialect):
return None
+
class INTEGER(_IntegerType, sqltypes.INTEGER):
"""MySQL INTEGER type."""
@@ -541,6 +607,7 @@ class INTEGER(_IntegerType, sqltypes.INTEGER):
"""
super(INTEGER, self).__init__(display_width=display_width, **kw)
+
class BIGINT(_IntegerType, sqltypes.BIGINT):
"""MySQL BIGINTEGER type."""
@@ -561,6 +628,7 @@ class BIGINT(_IntegerType, sqltypes.BIGINT):
"""
super(BIGINT, self).__init__(display_width=display_width, **kw)
+
class MEDIUMINT(_IntegerType):
"""MySQL MEDIUMINTEGER type."""
@@ -581,6 +649,7 @@ class MEDIUMINT(_IntegerType):
"""
super(MEDIUMINT, self).__init__(display_width=display_width, **kw)
+
class TINYINT(_IntegerType):
"""MySQL TINYINT type."""
@@ -589,10 +658,6 @@ class TINYINT(_IntegerType):
def __init__(self, display_width=None, **kw):
"""Construct a TINYINT.
- Note: following the usual MySQL conventions, TINYINT(1) columns
- reflected during Table(..., autoload=True) are treated as
- Boolean columns.
-
:param display_width: Optional, maximum display width for this number.
:param unsigned: a boolean, optional.
@@ -605,6 +670,7 @@ class TINYINT(_IntegerType):
"""
super(TINYINT, self).__init__(display_width=display_width, **kw)
+
class SMALLINT(_IntegerType, sqltypes.SMALLINT):
"""MySQL SMALLINTEGER type."""
@@ -625,6 +691,7 @@ class SMALLINT(_IntegerType, sqltypes.SMALLINT):
"""
super(SMALLINT, self).__init__(display_width=display_width, **kw)
+
class BIT(sqltypes.TypeEngine):
"""MySQL BIT type.
@@ -654,34 +721,68 @@ class BIT(sqltypes.TypeEngine):
def process(value):
if value is not None:
- v = 0L
+ v = 0
for i in map(ord, value):
v = v << 8 | i
return v
return value
return process
-class _MSTime(sqltypes.Time):
- """MySQL TIME type."""
+
+class TIME(sqltypes.TIME):
+ """MySQL TIME type.
+
+ Recent versions of MySQL add support for
+ fractional seconds precision. While the
+ :class:`.mysql.TIME` type now supports this,
+ note that many DBAPI drivers may not yet
+ include support.
+
+ """
__visit_name__ = 'TIME'
+ def __init__(self, timezone=False, fsp=None):
+ """Construct a MySQL TIME type.
+
+ :param timezone: not used by the MySQL dialect.
+ :param fsp: fractional seconds precision value.
+ MySQL 5.6 supports storage of fractional seconds;
+ this parameter will be used when emitting DDL
+ for the TIME type. Note that many DBAPI drivers
+ may not yet have support for fractional seconds,
+ however.
+
+ .. versionadded:: 0.8 The MySQL-specific TIME
+ type as well as fractional seconds support.
+
+ """
+ super(TIME, self).__init__(timezone=timezone)
+ self.fsp = fsp
+
def result_processor(self, dialect, coltype):
time = datetime.time
+
def process(value):
# convert from a timedelta value
if value is not None:
+ microseconds = value.microseconds
seconds = value.seconds
- minutes = seconds / 60
- return time(minutes / 60, minutes % 60, seconds - minutes * 60)
+ minutes = seconds // 60
+ return time(minutes // 60,
+ minutes % 60,
+ seconds - minutes * 60,
+ microsecond=microseconds)
else:
return None
return process
+
class TIMESTAMP(sqltypes.TIMESTAMP):
"""MySQL TIMESTAMP type."""
__visit_name__ = 'TIMESTAMP'
+
class YEAR(sqltypes.TypeEngine):
"""MySQL YEAR type, for single byte storage of years 1901-2155."""
@@ -690,6 +791,7 @@ class YEAR(sqltypes.TypeEngine):
def __init__(self, display_width=None):
self.display_width = display_width
+
class TEXT(_StringType, sqltypes.TEXT):
"""MySQL TEXT type, for text up to 2^16 characters."""
@@ -725,6 +827,7 @@ class TEXT(_StringType, sqltypes.TEXT):
"""
super(TEXT, self).__init__(length=length, **kw)
+
class TINYTEXT(_StringType):
"""MySQL TINYTEXT type, for text up to 2^8 characters."""
@@ -756,6 +859,7 @@ class TINYTEXT(_StringType):
"""
super(TINYTEXT, self).__init__(**kwargs)
+
class MEDIUMTEXT(_StringType):
"""MySQL MEDIUMTEXT type, for text up to 2^24 characters."""
@@ -787,6 +891,7 @@ class MEDIUMTEXT(_StringType):
"""
super(MEDIUMTEXT, self).__init__(**kwargs)
+
class LONGTEXT(_StringType):
"""MySQL LONGTEXT type, for text up to 2^32 characters."""
@@ -850,6 +955,7 @@ class VARCHAR(_StringType, sqltypes.VARCHAR):
"""
super(VARCHAR, self).__init__(length=length, **kwargs)
+
class CHAR(_StringType, sqltypes.CHAR):
"""MySQL CHAR type, for fixed-length character data."""
@@ -870,6 +976,7 @@ class CHAR(_StringType, sqltypes.CHAR):
"""
super(CHAR, self).__init__(length=length, **kwargs)
+
class NVARCHAR(_StringType, sqltypes.NVARCHAR):
"""MySQL NVARCHAR type.
@@ -922,24 +1029,66 @@ class NCHAR(_StringType, sqltypes.NCHAR):
super(NCHAR, self).__init__(length=length, **kwargs)
-
-
class TINYBLOB(sqltypes._Binary):
"""MySQL TINYBLOB type, for binary data up to 2^8 bytes."""
__visit_name__ = 'TINYBLOB'
+
class MEDIUMBLOB(sqltypes._Binary):
"""MySQL MEDIUMBLOB type, for binary data up to 2^24 bytes."""
__visit_name__ = 'MEDIUMBLOB'
+
class LONGBLOB(sqltypes._Binary):
"""MySQL LONGBLOB type, for binary data up to 2^32 bytes."""
__visit_name__ = 'LONGBLOB'
-class ENUM(sqltypes.Enum, _StringType):
+class _EnumeratedValues(_StringType):
+ def _init_values(self, values, kw):
+ self.quoting = kw.pop('quoting', 'auto')
+
+ if self.quoting == 'auto' and len(values):
+ # What quoting character are we using?
+ q = None
+ for e in values:
+ if len(e) == 0:
+ self.quoting = 'unquoted'
+ break
+ elif q is None:
+ q = e[0]
+
+ if len(e) == 1 or e[0] != q or e[-1] != q:
+ self.quoting = 'unquoted'
+ break
+ else:
+ self.quoting = 'quoted'
+
+ if self.quoting == 'quoted':
+ util.warn_deprecated(
+ 'Manually quoting %s value literals is deprecated. Supply '
+ 'unquoted values and use the quoting= option in cases of '
+ 'ambiguity.' % self.__class__.__name__)
+
+ values = self._strip_values(values)
+
+ self._enumerated_values = values
+ length = max([len(v) for v in values] + [0])
+ return values, length
+
+ @classmethod
+ def _strip_values(cls, values):
+ strip_values = []
+ for a in values:
+ if a[0:1] == '"' or a[0:1] == "'":
+ # strip enclosing quotes and unquote interior
+ a = a[1:-1].replace(a[0] * 2, a[0])
+ strip_values.append(a)
+ return strip_values
+
+class ENUM(sqltypes.Enum, _EnumeratedValues):
"""MySQL ENUM type."""
__visit_name__ = 'ENUM'
@@ -947,9 +1096,9 @@ class ENUM(sqltypes.Enum, _StringType):
def __init__(self, *enums, **kw):
"""Construct an ENUM.
- Example:
+ E.g.::
- Column('myenum', MSEnum("foo", "bar", "baz"))
+ Column('myenum', ENUM("foo", "bar", "baz"))
:param enums: The range of valid values for this ENUM. Values will be
quoted when generating the schema according to the quoting flag (see
@@ -993,53 +1142,24 @@ class ENUM(sqltypes.Enum, _StringType):
literals for you. This is a transitional option.
"""
- self.quoting = kw.pop('quoting', 'auto')
-
- if self.quoting == 'auto' and len(enums):
- # What quoting character are we using?
- q = None
- for e in enums:
- if len(e) == 0:
- self.quoting = 'unquoted'
- break
- elif q is None:
- q = e[0]
-
- if e[0] != q or e[-1] != q:
- self.quoting = 'unquoted'
- break
- else:
- self.quoting = 'quoted'
-
- if self.quoting == 'quoted':
- util.warn_deprecated(
- 'Manually quoting ENUM value literals is deprecated. Supply '
- 'unquoted values and use the quoting= option in cases of '
- 'ambiguity.')
- enums = self._strip_enums(enums)
-
+ values, length = self._init_values(enums, kw)
self.strict = kw.pop('strict', False)
- length = max([len(v) for v in enums] + [0])
kw.pop('metadata', None)
kw.pop('schema', None)
kw.pop('name', None)
kw.pop('quote', None)
kw.pop('native_enum', None)
+ kw.pop('inherit_schema', None)
_StringType.__init__(self, length=length, **kw)
- sqltypes.Enum.__init__(self, *enums)
+ sqltypes.Enum.__init__(self, *values)
- @classmethod
- def _strip_enums(cls, enums):
- strip_enums = []
- for a in enums:
- if a[0:1] == '"' or a[0:1] == "'":
- # strip enclosing quotes and unquote interior
- a = a[1:-1].replace(a[0] * 2, a[0])
- strip_enums.append(a)
- return strip_enums
+ def __repr__(self):
+ return util.generic_repr(self,
+ to_inspect=[ENUM, _StringType, sqltypes.Enum])
def bind_processor(self, dialect):
super_convert = super(ENUM, self).bind_processor(dialect)
+
def process(value):
if self.strict and value is not None and value not in self.enums:
raise exc.InvalidRequestError('"%s" not a valid value for '
@@ -1054,7 +1174,8 @@ class ENUM(sqltypes.Enum, _StringType):
kw['strict'] = self.strict
return sqltypes.Enum.adapt(self, impltype, **kw)
-class SET(_StringType):
+
+class SET(_EnumeratedValues):
"""MySQL SET type."""
__visit_name__ = 'SET'
@@ -1062,15 +1183,16 @@ class SET(_StringType):
def __init__(self, *values, **kw):
"""Construct a SET.
- Example::
+ E.g.::
- Column('myset', MSSet("'foo'", "'bar'", "'baz'"))
+ Column('myset', SET("foo", "bar", "baz"))
:param values: The range of valid values for this SET. Values will be
- used exactly as they appear when generating schemas. Strings must
- be quoted, as in the example above. Single-quotes are suggested for
- ANSI compatibility and are required for portability to servers with
- ANSI_QUOTES enabled.
+ quoted when generating the schema according to the quoting flag (see
+ below).
+
+ .. versionchanged:: 0.9.0 quoting is applied automatically to
+ :class:`.mysql.SET` in the same way as for :class:`.mysql.ENUM`.
:param charset: Optional, a column-level character set for this string
value. Takes precedence to 'ascii' or 'unicode' short-hand.
@@ -1089,18 +1211,27 @@ class SET(_StringType):
BINARY in schema. This does not affect the type of data stored,
only the collation of character data.
+ :param quoting: Defaults to 'auto': automatically determine enum value
+ quoting. If all enum values are surrounded by the same quoting
+ character, then use 'quoted' mode. Otherwise, use 'unquoted' mode.
+
+ 'quoted': values in enums are already quoted, they will be used
+ directly when generating the schema - this usage is deprecated.
+
+ 'unquoted': values in enums are not quoted, they will be escaped and
+ surrounded by single quotes when generating the schema.
+
+ Previous versions of this type always required manually quoted
+ values to be supplied; future versions will always quote the string
+ literals for you. This is a transitional option.
+
+ .. versionadded:: 0.9.0
+
"""
- self._ddl_values = values
+ values, length = self._init_values(values, kw)
+ self.values = tuple(values)
- strip_values = []
- for a in values:
- if a[0:1] == '"' or a[0:1] == "'":
- # strip enclosing quotes and unquote interior
- a = a[1:-1].replace(a[0] * 2, a[0])
- strip_values.append(a)
-
- self.values = strip_values
- kw.setdefault('length', max([len(v) for v in strip_values] + [0]))
+ kw.setdefault('length', length)
super(SET, self).__init__(**kw)
def result_processor(self, dialect, coltype):
@@ -1109,13 +1240,10 @@ class SET(_StringType):
# No ',' quoting issues- commas aren't allowed in SET values
# The bad news:
# Plenty of driver inconsistencies here.
- if isinstance(value, util.set_types):
+ if isinstance(value, set):
# ..some versions convert '' to an empty set
if not value:
value.add('')
- # ..some return sets.Set, even for pythons that have __builtin__.set
- if not isinstance(value, set):
- value = set(value)
return value
# ...and some versions return strings
if value is not None:
@@ -1126,8 +1254,9 @@ class SET(_StringType):
def bind_processor(self, dialect):
super_convert = super(SET, self).bind_processor(dialect)
+
def process(value):
- if value is None or isinstance(value, (int, long, basestring)):
+ if value is None or isinstance(value, util.int_types + util.string_types):
pass
else:
if None in value:
@@ -1142,7 +1271,7 @@ class SET(_StringType):
return process
# old names
-MSTime = _MSTime
+MSTime = TIME
MSSet = SET
MSEnum = ENUM
MSLongBlob = LONGBLOB
@@ -1174,9 +1303,12 @@ MSFloat = FLOAT
MSInteger = INTEGER
colspecs = {
+ _IntegerType: _IntegerType,
+ _NumericType: _NumericType,
+ _FloatType: _FloatType,
sqltypes.Numeric: NUMERIC,
sqltypes.Float: FLOAT,
- sqltypes.Time: _MSTime,
+ sqltypes.Time: TIME,
sqltypes.Enum: ENUM,
}
@@ -1218,20 +1350,20 @@ ischema_names = {
'year': YEAR,
}
+
class MySQLExecutionContext(default.DefaultExecutionContext):
def should_autocommit_text(self, statement):
return AUTOCOMMIT_RE.match(statement)
+
class MySQLCompiler(compiler.SQLCompiler):
render_table_with_column_in_update_from = True
"""Overridden from base SQLCompiler value"""
extract_map = compiler.SQLCompiler.extract_map.copy()
- extract_map.update ({
- 'milliseconds': 'millisecond',
- })
+ extract_map.update({'milliseconds': 'millisecond'})
def visit_random_func(self, fn, **kw):
return "rand%s" % self.function_argspec(fn)
@@ -1242,11 +1374,13 @@ class MySQLCompiler(compiler.SQLCompiler):
def visit_sysdate_func(self, fn, **kw):
return "SYSDATE()"
- def visit_concat_op(self, binary, **kw):
- return "concat(%s, %s)" % (self.process(binary.left), self.process(binary.right))
+ def visit_concat_op_binary(self, binary, operator, **kw):
+ return "concat(%s, %s)" % (self.process(binary.left),
+ self.process(binary.right))
- def visit_match_op(self, binary, **kw):
- return "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % (self.process(binary.left), self.process(binary.right))
+ def visit_match_op_binary(self, binary, operator, **kw):
+ return "MATCH (%s) AGAINST (%s IN BOOLEAN MODE)" % \
+ (self.process(binary.left), self.process(binary.right))
def get_from_hint_text(self, table, text):
return text
@@ -1260,7 +1394,8 @@ class MySQLCompiler(compiler.SQLCompiler):
return 'SIGNED INTEGER'
elif isinstance(type_, sqltypes.TIMESTAMP):
return 'DATETIME'
- elif isinstance(type_, (sqltypes.DECIMAL, sqltypes.DateTime, sqltypes.Date, sqltypes.Time)):
+ elif isinstance(type_, (sqltypes.DECIMAL, sqltypes.DateTime,
+ sqltypes.Date, sqltypes.Time)):
return self.dialect.type_compiler.process(type_)
elif isinstance(type_, sqltypes.Text):
return 'CHAR'
@@ -1273,7 +1408,8 @@ class MySQLCompiler(compiler.SQLCompiler):
elif isinstance(type_, sqltypes._Binary):
return 'BINARY'
elif isinstance(type_, sqltypes.NUMERIC):
- return self.dialect.type_compiler.process(type_).replace('NUMERIC', 'DECIMAL')
+ return self.dialect.type_compiler.process(
+ type_).replace('NUMERIC', 'DECIMAL')
else:
return None
@@ -1304,7 +1440,7 @@ class MySQLCompiler(compiler.SQLCompiler):
of a SELECT.
"""
- if isinstance(select._distinct, basestring):
+ if isinstance(select._distinct, util.string_types):
return select._distinct.upper() + " "
elif select._distinct:
return "DISTINCT "
@@ -1312,11 +1448,6 @@ class MySQLCompiler(compiler.SQLCompiler):
return ""
def visit_join(self, join, asfrom=False, **kwargs):
- # 'JOIN ... ON ...' for inner joins isn't available until 4.0.
- # Apparently < 3.23.17 requires theta joins for inner joins
- # (but not outer). Not generating these currently, but
- # support can be added, preferably after dialects are
- # refactored to be version-sensitive.
return ''.join(
(self.process(join.left, asfrom=True, **kwargs),
(join.isouter and " LEFT OUTER JOIN " or " INNER JOIN "),
@@ -1325,10 +1456,10 @@ class MySQLCompiler(compiler.SQLCompiler):
self.process(join.onclause, **kwargs)))
def for_update_clause(self, select):
- if select.for_update == 'read':
- return ' LOCK IN SHARE MODE'
+ if select._for_update_arg.read:
+ return " LOCK IN SHARE MODE"
else:
- return super(MySQLCompiler, self).for_update_clause(select)
+ return " FOR UPDATE"
def limit_clause(self, select):
# MySQL supports:
@@ -1389,10 +1520,11 @@ class MySQLCompiler(compiler.SQLCompiler):
class MySQLDDLCompiler(compiler.DDLCompiler):
def create_table_constraints(self, table):
"""Get table constraints."""
- constraint_string = super(MySQLDDLCompiler, self).create_table_constraints(table)
+ constraint_string = super(
+ MySQLDDLCompiler, self).create_table_constraints(table)
engine_key = '%s_engine' % self.dialect.name
- is_innodb = table.kwargs.has_key(engine_key) and \
+ is_innodb = engine_key in table.kwargs and \
table.kwargs[engine_key].lower() == 'innodb'
auto_inc_column = table._autoincrement_column
@@ -1404,14 +1536,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
constraint_string += ", \n\t"
constraint_string += "KEY %s (%s)" % (
self.preparer.quote(
- "idx_autoinc_%s" % auto_inc_column.name, None
+ "idx_autoinc_%s" % auto_inc_column.name
),
self.preparer.format_column(auto_inc_column)
)
return constraint_string
-
def get_column_specification(self, column, **kw):
"""Builds column DDL."""
@@ -1430,7 +1561,8 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
elif column.nullable and is_timestamp and default is None:
colspec.append('NULL')
- if column is column.table._autoincrement_column and column.server_default is None:
+ if column is column.table._autoincrement_column and \
+ column.server_default is None:
colspec.append('AUTO_INCREMENT')
return ' '.join(colspec)
@@ -1442,7 +1574,7 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
opts = dict(
(
- k[len(self.dialect.name)+1:].upper(),
+ k[len(self.dialect.name) + 1:].upper(),
v
)
for k, v in table.kwargs.items()
@@ -1471,31 +1603,47 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
table_opts.append(joiner.join((opt, arg)))
return ' '.join(table_opts)
-
def visit_create_index(self, create):
index = create.element
+ self._verify_index_table(index)
preparer = self.preparer
table = preparer.format_table(index.table)
- columns = [preparer.quote(c.name, c.quote) for c in index.columns]
- name = preparer.quote(
- self._index_identifier(index.name),
- index.quote)
+ columns = [self.sql_compiler.process(expr, include_table=False,
+ literal_binds=True)
+ for expr in index.expressions]
+
+ name = self._prepared_index_name(index)
text = "CREATE "
if index.unique:
text += "UNIQUE "
text += "INDEX %s ON %s " % (name, table)
- columns = ', '.join(columns)
if 'mysql_length' in index.kwargs:
length = index.kwargs['mysql_length']
- text += "(%s(%d))" % (columns, length)
+
+ if isinstance(length, dict):
+ # length value can be a (column_name --> integer value) mapping
+ # specifying the prefix length for each column of the index
+ columns = ', '.join(
+ ('%s(%d)' % (col, length[col])
+ if col in length else '%s' % col)
+ for col in columns
+ )
+ else:
+ # or can be an integer value specifying the same
+ # prefix length for all columns of the index
+ columns = ', '.join(
+ '%s(%d)' % (col, length)
+ for col in columns
+ )
else:
- text += "(%s)" % (columns)
+ columns = ', '.join(columns)
+ text += '(%s)' % columns
if 'mysql_using' in index.kwargs:
using = index.kwargs['mysql_using']
- text += " USING %s" % (preparer.quote(using, index.quote))
+ text += " USING %s" % (preparer.quote(using))
return text
@@ -1504,17 +1652,15 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
visit_primary_key_constraint(constraint)
if "mysql_using" in constraint.kwargs:
using = constraint.kwargs['mysql_using']
- text += " USING %s" % (
- self.preparer.quote(using, constraint.quote))
+ text += " USING %s" % (self.preparer.quote(using))
return text
def visit_drop_index(self, drop):
index = drop.element
- return "\nDROP INDEX %s ON %s" % \
- (self.preparer.quote(
- self._index_identifier(index.name), index.quote
- ),
+ return "\nDROP INDEX %s ON %s" % (
+ self._prepared_index_name(index,
+ include_schema=False),
self.preparer.format_table(index.table))
def visit_drop_constraint(self, drop):
@@ -1535,6 +1681,13 @@ class MySQLDDLCompiler(compiler.DDLCompiler):
(self.preparer.format_table(constraint.table),
qual, const)
+ def define_constraint_match(self, constraint):
+ if constraint.match is not None:
+ raise exc.CompileError(
+ "MySQL ignores the 'MATCH' keyword while at the same time "
+ "causes ON UPDATE/ON DELETE clauses to be ignored.")
+ return ""
+
class MySQLTypeCompiler(compiler.GenericTypeCompiler):
def _extend_numeric(self, type_, spec):
"Extend a numeric-type declaration with MySQL specific extensions."
@@ -1593,7 +1746,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
else:
return self._extend_numeric(type_,
"NUMERIC(%(precision)s, %(scale)s)" %
- {'precision': type_.precision, 'scale' : type_.scale})
+ {'precision': type_.precision,
+ 'scale': type_.scale})
def visit_DECIMAL(self, type_):
if type_.precision is None:
@@ -1605,21 +1759,24 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
else:
return self._extend_numeric(type_,
"DECIMAL(%(precision)s, %(scale)s)" %
- {'precision': type_.precision, 'scale' : type_.scale})
+ {'precision': type_.precision,
+ 'scale': type_.scale})
def visit_DOUBLE(self, type_):
if type_.precision is not None and type_.scale is not None:
- return self._extend_numeric(type_, "DOUBLE(%(precision)s, %(scale)s)" %
+ return self._extend_numeric(type_,
+ "DOUBLE(%(precision)s, %(scale)s)" %
{'precision': type_.precision,
- 'scale' : type_.scale})
+ 'scale': type_.scale})
else:
return self._extend_numeric(type_, 'DOUBLE')
def visit_REAL(self, type_):
if type_.precision is not None and type_.scale is not None:
- return self._extend_numeric(type_, "REAL(%(precision)s, %(scale)s)" %
+ return self._extend_numeric(type_,
+ "REAL(%(precision)s, %(scale)s)" %
{'precision': type_.precision,
- 'scale' : type_.scale})
+ 'scale': type_.scale})
else:
return self._extend_numeric(type_, 'REAL')
@@ -1630,7 +1787,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
return self._extend_numeric(type_,
"FLOAT(%s, %s)" % (type_.precision, type_.scale))
elif type_.precision is not None:
- return self._extend_numeric(type_, "FLOAT(%s)" % (type_.precision,))
+ return self._extend_numeric(type_,
+ "FLOAT(%s)" % (type_.precision,))
else:
return self._extend_numeric(type_, "FLOAT")
@@ -1660,7 +1818,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
def visit_TINYINT(self, type_):
if self._mysql_type(type_) and type_.display_width is not None:
- return self._extend_numeric(type_, "TINYINT(%s)" % type_.display_width)
+ return self._extend_numeric(type_,
+ "TINYINT(%s)" % type_.display_width)
else:
return self._extend_numeric(type_, "TINYINT")
@@ -1686,7 +1845,10 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
return "DATE"
def visit_TIME(self, type_):
- return "TIME"
+ if getattr(type_, 'fsp', None):
+ return "TIME(%d)" % type_.fsp
+ else:
+ return "TIME"
def visit_TIMESTAMP(self, type_):
return 'TIMESTAMP'
@@ -1722,7 +1884,8 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
def visit_CHAR(self, type_):
if type_.length:
- return self._extend_string(type_, {}, "CHAR(%(length)s)" % {'length' : type_.length})
+ return self._extend_string(type_, {}, "CHAR(%(length)s)" %
+ {'length': type_.length})
else:
return self._extend_string(type_, {}, "CHAR")
@@ -1730,18 +1893,21 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
# We'll actually generate the equiv. "NATIONAL VARCHAR" instead
# of "NVARCHAR".
if type_.length:
- return self._extend_string(type_, {'national':True}, "VARCHAR(%(length)s)" % {'length': type_.length})
+ return self._extend_string(type_, {'national': True},
+ "VARCHAR(%(length)s)" % {'length': type_.length})
else:
raise exc.CompileError(
"NVARCHAR requires a length on dialect %s" %
self.dialect.name)
def visit_NCHAR(self, type_):
- # We'll actually generate the equiv. "NATIONAL CHAR" instead of "NCHAR".
+ # We'll actually generate the equiv.
+ # "NATIONAL CHAR" instead of "NCHAR".
if type_.length:
- return self._extend_string(type_, {'national':True}, "CHAR(%(length)s)" % {'length': type_.length})
+ return self._extend_string(type_, {'national': True},
+ "CHAR(%(length)s)" % {'length': type_.length})
else:
- return self._extend_string(type_, {'national':True}, "CHAR")
+ return self._extend_string(type_, {'national': True}, "CHAR")
def visit_VARBINARY(self, type_):
return "VARBINARY(%d)" % type_.length
@@ -1753,7 +1919,7 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
if not type_.native_enum:
return super(MySQLTypeCompiler, self).visit_enum(type_)
else:
- return self.visit_ENUM(type_)
+ return self._visit_enumerated_values("ENUM", type_, type_.enums)
def visit_BLOB(self, type_):
if type_.length:
@@ -1770,14 +1936,21 @@ class MySQLTypeCompiler(compiler.GenericTypeCompiler):
def visit_LONGBLOB(self, type_):
return "LONGBLOB"
- def visit_ENUM(self, type_):
+ def _visit_enumerated_values(self, name, type_, enumerated_values):
quoted_enums = []
- for e in type_.enums:
+ for e in enumerated_values:
quoted_enums.append("'%s'" % e.replace("'", "''"))
- return self._extend_string(type_, {}, "ENUM(%s)" % ",".join(quoted_enums))
+ return self._extend_string(type_, {}, "%s(%s)" % (
+ name, ",".join(quoted_enums))
+ )
+
+ def visit_ENUM(self, type_):
+ return self._visit_enumerated_values("ENUM", type_,
+ type_._enumerated_values)
def visit_SET(self, type_):
- return self._extend_string(type_, {}, "SET(%s)" % ",".join(type_._ddl_values))
+ return self._visit_enumerated_values("SET", type_,
+ type_._enumerated_values)
def visit_BOOLEAN(self, type):
return "BOOL"
@@ -1803,6 +1976,8 @@ class MySQLIdentifierPreparer(compiler.IdentifierPreparer):
return tuple([self.quote_identifier(i) for i in ids if i is not None])
+
+@log.class_logger
class MySQLDialect(default.DefaultDialect):
"""Details of the MySQL dialect. Not used directly in application code."""
@@ -1817,6 +1992,7 @@ class MySQLDialect(default.DefaultDialect):
supports_sane_rowcount = True
supports_sane_multi_rowcount = False
+ supports_multivalues_insert = True
default_paramstyle = 'format'
colspecs = colspecs
@@ -1833,7 +2009,8 @@ class MySQLDialect(default.DefaultDialect):
_backslash_escapes = True
_server_ansiquotes = False
- def __init__(self, use_ansiquotes=None, isolation_level=None, **kwargs):
+ def __init__(self, isolation_level=None, **kwargs):
+ kwargs.pop('use_ansiquotes', None) # legacy
default.DefaultDialect.__init__(self, **kwargs)
self.isolation_level = isolation_level
@@ -1866,9 +2043,11 @@ class MySQLDialect(default.DefaultDialect):
cursor.execute('SELECT @@tx_isolation')
val = cursor.fetchone()[0]
cursor.close()
+ if util.py3k and isinstance(val, bytes):
+ val = val.decode()
return val.upper().replace("-", " ")
- def do_commit(self, connection):
+ def do_commit(self, dbapi_connection):
"""Execute a COMMIT."""
# COMMIT/ROLLBACK were introduced in 3.23.15.
@@ -1877,7 +2056,7 @@ class MySQLDialect(default.DefaultDialect):
# Ignore commit/rollback if support isn't present, otherwise even basic
# operations via autocommit fail.
try:
- connection.commit()
+ dbapi_connection.commit()
except:
if self.server_version_info < (3, 23, 15):
args = sys.exc_info()[1].args
@@ -1885,11 +2064,11 @@ class MySQLDialect(default.DefaultDialect):
return
raise
- def do_rollback(self, connection):
+ def do_rollback(self, dbapi_connection):
"""Execute a ROLLBACK."""
try:
- connection.rollback()
+ dbapi_connection.rollback()
except:
if self.server_version_info < (3, 23, 15):
args = sys.exc_info()[1].args
@@ -1932,17 +2111,20 @@ class MySQLDialect(default.DefaultDialect):
return False
def _compat_fetchall(self, rp, charset=None):
- """Proxy result rows to smooth over MySQL-Python driver inconsistencies."""
+ """Proxy result rows to smooth over MySQL-Python driver
+ inconsistencies."""
return [_DecodingRowProxy(row, charset) for row in rp.fetchall()]
def _compat_fetchone(self, rp, charset=None):
- """Proxy a result row to smooth over MySQL-Python driver inconsistencies."""
+ """Proxy a result row to smooth over MySQL-Python driver
+ inconsistencies."""
return _DecodingRowProxy(rp.fetchone(), charset)
def _compat_first(self, rp, charset=None):
- """Proxy a result row to smooth over MySQL-Python driver inconsistencies."""
+ """Proxy a result row to smooth over MySQL-Python driver
+ inconsistencies."""
return _DecodingRowProxy(rp.first(), charset)
@@ -1952,7 +2134,6 @@ class MySQLDialect(default.DefaultDialect):
def _get_default_schema_name(self, connection):
return connection.execute('SELECT DATABASE()').scalar()
-
def has_table(self, connection, table_name, schema=None):
# SHOW TABLE STATUS LIKE and SHOW TABLES LIKE do not function properly
# on macosx (and maybe win?) with multibyte table names.
@@ -1964,7 +2145,6 @@ class MySQLDialect(default.DefaultDialect):
# full_name = self.identifier_preparer.format_table(table,
# use_schema=True)
-
full_name = '.'.join(self.identifier_preparer._quote_free_identifiers(
schema, table_name))
@@ -1973,10 +2153,10 @@ class MySQLDialect(default.DefaultDialect):
try:
try:
rs = connection.execute(st)
- have = rs.rowcount > 0
+ have = rs.fetchone() is not None
rs.close()
return have
- except exc.DBAPIError, e:
+ except exc.DBAPIError as e:
if self._extract_error_code(e.orig) == 1146:
return False
raise
@@ -1987,14 +2167,12 @@ class MySQLDialect(default.DefaultDialect):
def initialize(self, connection):
default.DefaultDialect.initialize(self, connection)
self._connection_charset = self._detect_charset(connection)
- self._server_casing = self._detect_casing(connection)
- self._server_collations = self._detect_collations(connection)
self._detect_ansiquotes(connection)
if self._server_ansiquotes:
# if ansiquotes == True, build a new IdentifierPreparer
# with the new setting
self.identifier_preparer = self.preparer(self,
- server_ansiquotes=self._server_ansiquotes)
+ server_ansiquotes=self._server_ansiquotes)
@property
def _supports_cast(self):
@@ -2018,13 +2196,15 @@ class MySQLDialect(default.DefaultDialect):
if self.server_version_info < (5, 0, 2):
rp = connection.execute("SHOW TABLES FROM %s" %
self.identifier_preparer.quote_identifier(current_schema))
- return [row[0] for row in self._compat_fetchall(rp, charset=charset)]
+ return [row[0] for
+ row in self._compat_fetchall(rp, charset=charset)]
else:
rp = connection.execute("SHOW FULL TABLES FROM %s" %
self.identifier_preparer.quote_identifier(current_schema))
- return [row[0] for row in self._compat_fetchall(rp, charset=charset)\
- if row[1] == 'BASE TABLE']
+ return [row[0]
+ for row in self._compat_fetchall(rp, charset=charset)
+ if row[1] == 'BASE TABLE']
@reflection.cache
def get_view_names(self, connection, schema=None, **kw):
@@ -2037,34 +2217,39 @@ class MySQLDialect(default.DefaultDialect):
charset = self._connection_charset
rp = connection.execute("SHOW FULL TABLES FROM %s" %
self.identifier_preparer.quote_identifier(schema))
- return [row[0] for row in self._compat_fetchall(rp, charset=charset)\
- if row[1] in ('VIEW', 'SYSTEM VIEW')]
+ return [row[0]
+ for row in self._compat_fetchall(rp, charset=charset)
+ if row[1] in ('VIEW', 'SYSTEM VIEW')]
@reflection.cache
def get_table_options(self, connection, table_name, schema=None, **kw):
- parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw)
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
return parsed_state.table_options
@reflection.cache
def get_columns(self, connection, table_name, schema=None, **kw):
- parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw)
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
return parsed_state.columns
@reflection.cache
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
- parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw)
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
for key in parsed_state.keys:
if key['type'] == 'PRIMARY':
# There can be only one.
- ##raise Exception, str(key)
- return [s[0] for s in key['columns']]
- return []
+ cols = [s[0] for s in key['columns']]
+ return {'constrained_columns': cols, 'name': None}
+ return {'constrained_columns': [], 'name': None}
@reflection.cache
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
- parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw)
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
default_schema = None
fkeys = []
@@ -2085,17 +2270,17 @@ class MySQLDialect(default.DefaultDialect):
ref_names = spec['foreign']
con_kw = {}
- for opt in ('name', 'onupdate', 'ondelete'):
+ for opt in ('onupdate', 'ondelete'):
if spec.get(opt, False):
con_kw[opt] = spec[opt]
fkey_d = {
- 'name' : spec['name'],
- 'constrained_columns' : loc_names,
- 'referred_schema' : ref_schema,
- 'referred_table' : ref_name,
- 'referred_columns' : ref_names,
- 'options' : con_kw
+ 'name': spec['name'],
+ 'constrained_columns': loc_names,
+ 'referred_schema': ref_schema,
+ 'referred_table': ref_name,
+ 'referred_columns': ref_names,
+ 'options': con_kw
}
fkeys.append(fkey_d)
return fkeys
@@ -2103,7 +2288,8 @@ class MySQLDialect(default.DefaultDialect):
@reflection.cache
def get_indexes(self, connection, table_name, schema=None, **kw):
- parsed_state = self._parsed_state_or_create(connection, table_name, schema, **kw)
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
indexes = []
for spec in parsed_state.keys:
@@ -2127,6 +2313,21 @@ class MySQLDialect(default.DefaultDialect):
indexes.append(index_d)
return indexes
+ @reflection.cache
+ def get_unique_constraints(self, connection, table_name,
+ schema=None, **kw):
+ parsed_state = self._parsed_state_or_create(
+ connection, table_name, schema, **kw)
+
+ return [
+ {
+ 'name': key['name'],
+ 'column_names': [col[0] for col in key['columns']]
+ }
+ for key in parsed_state.keys
+ if key['type'] == 'UNIQUE'
+ ]
+
@reflection.cache
def get_view_definition(self, connection, view_name, schema=None, **kw):
@@ -2137,7 +2338,8 @@ class MySQLDialect(default.DefaultDialect):
full_name=full_name)
return sql
- def _parsed_state_or_create(self, connection, table_name, schema=None, **kw):
+ def _parsed_state_or_create(self, connection, table_name,
+ schema=None, **kw):
return self._setup_parser(
connection,
table_name,
@@ -2225,7 +2427,7 @@ class MySQLDialect(default.DefaultDialect):
row = self._compat_first(
connection.execute("SHOW VARIABLES LIKE 'sql_mode'"),
- charset=self._connection_charset)
+ charset=self._connection_charset)
if not row:
mode = ''
@@ -2252,7 +2454,7 @@ class MySQLDialect(default.DefaultDialect):
rp = None
try:
rp = connection.execute(st)
- except exc.DBAPIError, e:
+ except exc.DBAPIError as e:
if self._extract_error_code(e.orig) == 1146:
raise exc.NoSuchTableError(full_name)
else:
@@ -2276,7 +2478,7 @@ class MySQLDialect(default.DefaultDialect):
try:
try:
rp = connection.execute(st)
- except exc.DBAPIError, e:
+ except exc.DBAPIError as e:
if self._extract_error_code(e.orig) == 1146:
raise exc.NoSuchTableError(full_name)
else:
@@ -2287,6 +2489,7 @@ class MySQLDialect(default.DefaultDialect):
rp.close()
return rows
+
class ReflectedState(object):
"""Stores raw information about a SHOW CREATE TABLE statement."""
@@ -2297,6 +2500,8 @@ class ReflectedState(object):
self.keys = []
self.constraints = []
+
+@log.class_logger
class MySQLTableDefinitionParser(object):
"""Parses the results of a SHOW CREATE TABLE statement."""
@@ -2319,7 +2524,8 @@ class MySQLTableDefinitionParser(object):
pass
elif line.startswith('CREATE '):
self._parse_table_name(line, state)
- # Not present in real reflection, but may be if loading from a file.
+ # Not present in real reflection, but may be if
+ # loading from a file.
elif not line:
pass
else:
@@ -2332,7 +2538,6 @@ class MySQLTableDefinitionParser(object):
state.constraints.append(spec)
else:
pass
-
return state
def _parse_constraints(self, line):
@@ -2461,8 +2666,8 @@ class MySQLTableDefinitionParser(object):
if spec.get(kw, False):
type_kw[kw] = spec[kw]
- if type_ == 'enum':
- type_args = ENUM._strip_enums(type_args)
+ if issubclass(col_type, _EnumeratedValues):
+ type_args = _EnumeratedValues._strip_values(type_args)
type_instance = col_type(*type_args, **type_kw)
@@ -2573,7 +2778,6 @@ class MySQLTableDefinitionParser(object):
# 123 or 123,456
self._re_csv_int = _re_compile(r'\d+')
-
# `colname` [type opts]
# (NOT NULL | NULL)
# DEFAULT ('value' | CURRENT_TIMESTAMP...)
@@ -2637,7 +2841,7 @@ class MySQLTableDefinitionParser(object):
#
# unique constraints come back as KEYs
kw = quotes.copy()
- kw['on'] = 'RESTRICT|CASCASDE|SET NULL|NOACTION'
+ kw['on'] = 'RESTRICT|CASCADE|SET NULL|NOACTION'
self._re_constraint = _re_compile(
r' '
r'CONSTRAINT +'
@@ -2658,7 +2862,8 @@ class MySQLTableDefinitionParser(object):
self._re_partition = _re_compile(r'(?:.*)(?:SUB)?PARTITION(?:.*)')
# Table-level options (COLLATE, ENGINE, etc.)
- # Do the string options first, since they have quoted strings we need to get rid of.
+ # Do the string options first, since they have quoted
+ # strings we need to get rid of.
for option in _options_of_type_string:
self._add_option_string(option)
@@ -2681,8 +2886,8 @@ class MySQLTableDefinitionParser(object):
regex = (r'(?P%s)%s'
r"'(?P(?:[^']|'')*?)'(?!')" %
(re.escape(directive), self._optional_equals))
- self._pr_options.append(
- _pr_compile(regex, lambda v: v.replace("\\\\","\\").replace("''", "'")))
+ self._pr_options.append(_pr_compile(regex, lambda v:
+ v.replace("\\\\", "\\").replace("''", "'")))
def _add_option_word(self, directive):
regex = (r'(?P%s)%s'
@@ -2699,8 +2904,6 @@ class MySQLTableDefinitionParser(object):
_options_of_type_string = ('COMMENT', 'DATA DIRECTORY', 'INDEX DIRECTORY',
'PASSWORD', 'CONNECTION')
-log.class_logger(MySQLTableDefinitionParser)
-log.class_logger(MySQLDialect)
class _DecodingRowProxy(object):
@@ -2724,11 +2927,8 @@ class _DecodingRowProxy(object):
item = self.rowproxy[index]
if isinstance(item, _array):
item = item.tostring()
- # Py2K
- if self.charset and isinstance(item, str):
- # end Py2K
- # Py3K
- #if self.charset and isinstance(item, bytes):
+
+ if self.charset and isinstance(item, util.binary_type):
return item.decode(self.charset)
else:
return item
@@ -2737,11 +2937,7 @@ class _DecodingRowProxy(object):
item = getattr(self.rowproxy, attr)
if isinstance(item, _array):
item = item.tostring()
- # Py2K
- if self.charset and isinstance(item, str):
- # end Py2K
- # Py3K
- #if self.charset and isinstance(item, bytes):
+ if self.charset and isinstance(item, util.binary_type):
return item.decode(self.charset)
else:
return item
@@ -2752,8 +2948,8 @@ def _pr_compile(regex, cleanup=None):
return (_re_compile(regex), cleanup)
+
def _re_compile(regex):
"""Compile a string to regex, I and UNICODE."""
return re.compile(regex, re.I | re.UNICODE)
-
diff --git a/libs/sqlalchemy/dialects/mysql/cymysql.py b/libs/sqlalchemy/dialects/mysql/cymysql.py
new file mode 100644
index 00000000..e81a79b8
--- /dev/null
+++ b/libs/sqlalchemy/dialects/mysql/cymysql.py
@@ -0,0 +1,69 @@
+# mysql/cymysql.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+"""
+
+.. dialect:: mysql+cymysql
+ :name: CyMySQL
+ :dbapi: cymysql
+ :connectstring: mysql+cymysql://:@/[?]
+ :url: https://github.com/nakagami/CyMySQL
+
+"""
+
+from .mysqldb import MySQLDialect_mysqldb
+from .base import (BIT, MySQLDialect)
+from ... import util
+
+class _cymysqlBIT(BIT):
+ def result_processor(self, dialect, coltype):
+ """Convert a MySQL's 64 bit, variable length binary string to a long.
+ """
+
+ def process(value):
+ if value is not None:
+ v = 0
+ for i in util.iterbytes(value):
+ v = v << 8 | i
+ return v
+ return value
+ return process
+
+
+class MySQLDialect_cymysql(MySQLDialect_mysqldb):
+ driver = 'cymysql'
+
+ description_encoding = None
+ supports_sane_rowcount = True
+ supports_sane_multi_rowcount = False
+ supports_unicode_statements = True
+
+ colspecs = util.update_copy(
+ MySQLDialect.colspecs,
+ {
+ BIT: _cymysqlBIT,
+ }
+ )
+
+ @classmethod
+ def dbapi(cls):
+ return __import__('cymysql')
+
+ def _extract_error_code(self, exception):
+ return exception.errno
+
+ def is_disconnect(self, e, connection, cursor):
+ if isinstance(e, self.dbapi.OperationalError):
+ return self._extract_error_code(e) in \
+ (2006, 2013, 2014, 2045, 2055)
+ elif isinstance(e, self.dbapi.InterfaceError):
+ # if underlying connection is closed,
+ # this is the error you get
+ return True
+ else:
+ return False
+
+dialect = MySQLDialect_cymysql
diff --git a/libs/sqlalchemy/dialects/mysql/gaerdbms.py b/libs/sqlalchemy/dialects/mysql/gaerdbms.py
index 2203504f..13203fce 100644
--- a/libs/sqlalchemy/dialects/mysql/gaerdbms.py
+++ b/libs/sqlalchemy/dialects/mysql/gaerdbms.py
@@ -1,26 +1,20 @@
# mysql/gaerdbms.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for Google Cloud SQL on Google App Engine.
+"""
+.. dialect:: mysql+gaerdbms
+ :name: Google Cloud SQL
+ :dbapi: rdbms
+ :connectstring: mysql+gaerdbms:///?instance=
+ :url: https://developers.google.com/appengine/docs/python/cloud-sql/developers-guide
-This dialect is based primarily on the :mod:`.mysql.mysqldb` dialect with minimal
-changes.
+ This dialect is based primarily on the :mod:`.mysql.mysqldb` dialect with minimal
+ changes.
-.. versionadded:: 0.7.8
+ .. versionadded:: 0.7.8
-Connecting
-----------
-
-Connect string format::
-
- mysql+gaerdbms:///
-
-E.g.::
-
- create_engine('mysql+gaerdbms:///mydb',
- connect_args={"instance":"instancename"})
Pooling
-------
@@ -32,11 +26,17 @@ default.
"""
-from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb
-from sqlalchemy.pool import NullPool
+import os
+
+from .mysqldb import MySQLDialect_mysqldb
+from ...pool import NullPool
import re
+def _is_dev_environment():
+ return os.environ.get('SERVER_SOFTWARE', '').startswith('Development/')
+
+
class MySQLDialect_gaerdbms(MySQLDialect_mysqldb):
@classmethod
@@ -49,7 +49,10 @@ class MySQLDialect_gaerdbms(MySQLDialect_mysqldb):
# see also http://stackoverflow.com/q/14224679/34549
from google.appengine.api import apiproxy_stub_map
- if apiproxy_stub_map.apiproxy.GetStub('rdbms'):
+ if _is_dev_environment():
+ from google.appengine.api import rdbms_mysqldb
+ return rdbms_mysqldb
+ elif apiproxy_stub_map.apiproxy.GetStub('rdbms'):
from google.storage.speckle.python.api import rdbms_apiproxy
return rdbms_apiproxy
else:
@@ -63,21 +66,18 @@ class MySQLDialect_gaerdbms(MySQLDialect_mysqldb):
def create_connect_args(self, url):
opts = url.translate_connect_args()
- # 'dsn' and 'instance' are because we are skipping
- # the traditional google.api.rdbms wrapper
-
- opts['dsn'] = ''
- opts['instance'] = url.query['instance']
+ if not _is_dev_environment():
+ # 'dsn' and 'instance' are because we are skipping
+ # the traditional google.api.rdbms wrapper
+ opts['dsn'] = ''
+ opts['instance'] = url.query['instance']
return [], opts
def _extract_error_code(self, exception):
- match = re.compile(r"^(\d+):").match(str(exception))
+ match = re.compile(r"^(\d+)L?:|^\((\d+)L?,").match(str(exception))
# The rdbms api will wrap then re-raise some types of errors
# making this regex return no matches.
- if match:
- code = match.group(1)
- else:
- code = None
+ code = match.group(1) or match.group(2) if match else None
if code:
return int(code)
diff --git a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py
index bd8ee013..b6e7c75f 100644
--- a/libs/sqlalchemy/dialects/mysql/mysqlconnector.py
+++ b/libs/sqlalchemy/dialects/mysql/mysqlconnector.py
@@ -1,34 +1,25 @@
# mysql/mysqlconnector.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via the MySQL Connector/Python adapter.
+"""
+.. dialect:: mysql+mysqlconnector
+ :name: MySQL Connector/Python
+ :dbapi: myconnpy
+ :connectstring: mysql+mysqlconnector://:@[:]/
+ :url: https://launchpad.net/myconnpy
-MySQL Connector/Python is available at:
-
- https://launchpad.net/myconnpy
-
-Connecting
------------
-
-Connect string format::
-
- mysql+mysqlconnector://:@[:]/
"""
-import re
-
-from sqlalchemy.dialects.mysql.base import (MySQLDialect,
+from .base import (MySQLDialect,
MySQLExecutionContext, MySQLCompiler, MySQLIdentifierPreparer,
BIT)
-from sqlalchemy.engine import base as engine_base, default
-from sqlalchemy.sql import operators as sql_operators
-from sqlalchemy import exc, log, schema, sql, types as sqltypes, util
-from sqlalchemy import processors
+from ... import util
+
class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
@@ -37,24 +28,28 @@ class MySQLExecutionContext_mysqlconnector(MySQLExecutionContext):
class MySQLCompiler_mysqlconnector(MySQLCompiler):
- def visit_mod(self, binary, **kw):
- return self.process(binary.left) + " %% " + self.process(binary.right)
+ def visit_mod_binary(self, binary, operator, **kw):
+ return self.process(binary.left, **kw) + " %% " + \
+ self.process(binary.right, **kw)
def post_process_text(self, text):
return text.replace('%', '%%')
+
class MySQLIdentifierPreparer_mysqlconnector(MySQLIdentifierPreparer):
def _escape_identifier(self, value):
value = value.replace(self.escape_quote, self.escape_to_quote)
return value.replace("%", "%%")
+
class _myconnpyBIT(BIT):
def result_processor(self, dialect, coltype):
"""MySQL-connector already converts mysql bits, so."""
return None
+
class MySQLDialect_mysqlconnector(MySQLDialect):
driver = 'mysqlconnector'
supports_unicode_statements = True
@@ -84,12 +79,13 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
def create_connect_args(self, url):
opts = url.translate_connect_args(username='user')
+
opts.update(url.query)
util.coerce_kw_type(opts, 'buffered', bool)
util.coerce_kw_type(opts, 'raise_on_warnings', bool)
- opts['buffered'] = True
- opts['raise_on_warnings'] = True
+ opts.setdefault('buffered', True)
+ opts.setdefault('raise_on_warnings', True)
# FOUND_ROWS must be set in ClientFlag to enable
# supports_sane_rowcount.
@@ -116,9 +112,10 @@ class MySQLDialect_mysqlconnector(MySQLDialect):
def is_disconnect(self, e, connection, cursor):
errnos = (2006, 2013, 2014, 2045, 2055, 2048)
- exceptions = (self.dbapi.OperationalError,self.dbapi.InterfaceError)
+ exceptions = (self.dbapi.OperationalError, self.dbapi.InterfaceError)
if isinstance(e, exceptions):
- return e.errno in errnos
+ return e.errno in errnos or \
+ "MySQL Connection not available." in str(e)
else:
return False
diff --git a/libs/sqlalchemy/dialects/mysql/mysqldb.py b/libs/sqlalchemy/dialects/mysql/mysqldb.py
index c6ae5333..c6942ae2 100644
--- a/libs/sqlalchemy/dialects/mysql/mysqldb.py
+++ b/libs/sqlalchemy/dialects/mysql/mysqldb.py
@@ -1,23 +1,17 @@
# mysql/mysqldb.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via the MySQL-python adapter.
+"""
-MySQL-Python is available at:
+.. dialect:: mysql+mysqldb
+ :name: MySQL-Python
+ :dbapi: mysqldb
+ :connectstring: mysql+mysqldb://:@[:]/
+ :url: http://sourceforge.net/projects/mysql-python
- http://sourceforge.net/projects/mysql-python
-
-At least version 1.2.1 or 1.2.2 should be used.
-
-Connecting
------------
-
-Connect string format::
-
- mysql+mysqldb://:@[:]/
Unicode
-------
@@ -54,15 +48,16 @@ It is strongly advised to use the latest version of MySQL-Python.
"""
-from sqlalchemy.dialects.mysql.base import (MySQLDialect, MySQLExecutionContext,
+from .base import (MySQLDialect, MySQLExecutionContext,
MySQLCompiler, MySQLIdentifierPreparer)
-from sqlalchemy.connectors.mysqldb import (
+from ...connectors.mysqldb import (
MySQLDBExecutionContext,
MySQLDBCompiler,
MySQLDBIdentifierPreparer,
MySQLDBConnector
)
+
class MySQLExecutionContext_mysqldb(MySQLDBExecutionContext, MySQLExecutionContext):
pass
@@ -74,6 +69,7 @@ class MySQLCompiler_mysqldb(MySQLDBCompiler, MySQLCompiler):
class MySQLIdentifierPreparer_mysqldb(MySQLDBIdentifierPreparer, MySQLIdentifierPreparer):
pass
+
class MySQLDialect_mysqldb(MySQLDBConnector, MySQLDialect):
execution_ctx_cls = MySQLExecutionContext_mysqldb
statement_compiler = MySQLCompiler_mysqldb
diff --git a/libs/sqlalchemy/dialects/mysql/oursql.py b/libs/sqlalchemy/dialects/mysql/oursql.py
index d6d8e9ff..e6b50f33 100644
--- a/libs/sqlalchemy/dialects/mysql/oursql.py
+++ b/libs/sqlalchemy/dialects/mysql/oursql.py
@@ -1,21 +1,16 @@
# mysql/oursql.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via the oursql adapter.
+"""
-OurSQL is available at:
-
- http://packages.python.org/oursql/
-
-Connecting
------------
-
-Connect string format::
-
- mysql+oursql://:@[:]/
+.. dialect:: mysql+oursql
+ :name: OurSQL
+ :dbapi: oursql
+ :connectstring: mysql+oursql://:@[:]/
+ :url: http://packages.python.org/oursql/
Unicode
-------
@@ -40,13 +35,8 @@ defaults to, there is a separate parameter::
import re
-from sqlalchemy.dialects.mysql.base import (BIT, MySQLDialect, MySQLExecutionContext,
- MySQLCompiler, MySQLIdentifierPreparer)
-from sqlalchemy.engine import base as engine_base, default
-from sqlalchemy.sql import operators as sql_operators
-from sqlalchemy import exc, log, schema, sql, types as sqltypes, util
-from sqlalchemy import processors
-
+from .base import (BIT, MySQLDialect, MySQLExecutionContext)
+from ... import types as sqltypes, util
class _oursqlBIT(BIT):
@@ -62,12 +52,13 @@ class MySQLExecutionContext_oursql(MySQLExecutionContext):
def plain_query(self):
return self.execution_options.get('_oursql_plain_query', False)
+
class MySQLDialect_oursql(MySQLDialect):
driver = 'oursql'
-# Py2K
- supports_unicode_binds = True
- supports_unicode_statements = True
-# end Py2K
+
+ if util.py2k:
+ supports_unicode_binds = True
+ supports_unicode_statements = True
supports_native_decimal = True
@@ -99,12 +90,11 @@ class MySQLDialect_oursql(MySQLDialect):
connection.cursor().execute('BEGIN', plain_query=True)
def _xa_query(self, connection, query, xid):
-# Py2K
- arg = connection.connection._escape_string(xid)
-# end Py2K
-# Py3K
-# charset = self._connection_charset
-# arg = connection.connection._escape_string(xid.encode(charset)).decode(charset)
+ if util.py2k:
+ arg = connection.connection._escape_string(xid)
+ else:
+ charset = self._connection_charset
+ arg = connection.connection._escape_string(xid.encode(charset)).decode(charset)
arg = "'%s'" % arg
connection.execution_options(_oursql_plain_query=True).execute(query % arg)
@@ -135,64 +125,67 @@ class MySQLDialect_oursql(MySQLDialect):
# Q: why didn't we need all these "plain_query" overrides earlier ?
# am i on a newer/older version of OurSQL ?
def has_table(self, connection, table_name, schema=None):
- return MySQLDialect.has_table(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- table_name, schema)
-
- def get_table_options(self, connection, table_name, schema=None, **kw):
- return MySQLDialect.get_table_options(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- table_name,
- schema = schema,
- **kw
+ return MySQLDialect.has_table(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ table_name,
+ schema
)
+ def get_table_options(self, connection, table_name, schema=None, **kw):
+ return MySQLDialect.get_table_options(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ table_name,
+ schema=schema,
+ **kw
+ )
def get_columns(self, connection, table_name, schema=None, **kw):
- return MySQLDialect.get_columns(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- table_name,
- schema=schema,
- **kw
+ return MySQLDialect.get_columns(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ table_name,
+ schema=schema,
+ **kw
)
def get_view_names(self, connection, schema=None, **kw):
- return MySQLDialect.get_view_names(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- schema=schema,
- **kw
+ return MySQLDialect.get_view_names(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ schema=schema,
+ **kw
)
def get_table_names(self, connection, schema=None, **kw):
- return MySQLDialect.get_table_names(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- schema
+ return MySQLDialect.get_table_names(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ schema
)
def get_schema_names(self, connection, **kw):
- return MySQLDialect.get_schema_names(self,
- connection.connect().\
- execution_options(_oursql_plain_query=True),
- **kw
+ return MySQLDialect.get_schema_names(
+ self,
+ connection.connect().execution_options(_oursql_plain_query=True),
+ **kw
)
def initialize(self, connection):
return MySQLDialect.initialize(
- self,
- connection.execution_options(_oursql_plain_query=True)
- )
+ self,
+ connection.execution_options(_oursql_plain_query=True)
+ )
def _show_create_table(self, connection, table, charset=None,
full_name=None):
- return MySQLDialect._show_create_table(self,
- connection.contextual_connect(close_with_result=True).
- execution_options(_oursql_plain_query=True),
- table, charset, full_name)
+ return MySQLDialect._show_create_table(
+ self,
+ connection.contextual_connect(close_with_result=True).
+ execution_options(_oursql_plain_query=True),
+ table, charset, full_name
+ )
def is_disconnect(self, e, connection, cursor):
if isinstance(e, self.dbapi.ProgrammingError):
diff --git a/libs/sqlalchemy/dialects/mysql/pymysql.py b/libs/sqlalchemy/dialects/mysql/pymysql.py
index f5aaa122..74de09c4 100644
--- a/libs/sqlalchemy/dialects/mysql/pymysql.py
+++ b/libs/sqlalchemy/dialects/mysql/pymysql.py
@@ -1,21 +1,16 @@
# mysql/pymysql.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via the pymysql adapter.
+"""
-pymysql is available at:
-
- http://code.google.com/p/pymysql/
-
-Connecting
-----------
-
-Connect string::
-
- mysql+pymysql://:@/[?]
+.. dialect:: mysql+pymysql
+ :name: PyMySQL
+ :dbapi: pymysql
+ :connectstring: mysql+pymysql://:@/[?]
+ :url: http://code.google.com/p/pymysql/
MySQL-Python Compatibility
--------------------------
@@ -26,14 +21,24 @@ the pymysql driver as well.
"""
-from sqlalchemy.dialects.mysql.mysqldb import MySQLDialect_mysqldb
+from .mysqldb import MySQLDialect_mysqldb
+from ...util import py3k
class MySQLDialect_pymysql(MySQLDialect_mysqldb):
driver = 'pymysql'
description_encoding = None
+ if py3k:
+ supports_unicode_statements = True
+
@classmethod
def dbapi(cls):
return __import__('pymysql')
-dialect = MySQLDialect_pymysql
\ No newline at end of file
+ if py3k:
+ def _extract_error_code(self, exception):
+ if isinstance(exception.args[0], Exception):
+ exception = exception.args[0]
+ return exception.args[0]
+
+dialect = MySQLDialect_pymysql
diff --git a/libs/sqlalchemy/dialects/mysql/pyodbc.py b/libs/sqlalchemy/dialects/mysql/pyodbc.py
index 5d631afb..e60e39ce 100644
--- a/libs/sqlalchemy/dialects/mysql/pyodbc.py
+++ b/libs/sqlalchemy/dialects/mysql/pyodbc.py
@@ -1,21 +1,18 @@
# mysql/pyodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via the pyodbc adapter.
+"""
-pyodbc is available at:
- http://pypi.python.org/pypi/pyodbc/
+.. dialect:: mysql+pyodbc
+ :name: PyODBC
+ :dbapi: pyodbc
+ :connectstring: mysql+pyodbc://:@
+ :url: http://pypi.python.org/pypi/pyodbc/
-Connecting
-----------
-
-Connect string::
-
- mysql+pyodbc://:@
Limitations
-----------
@@ -27,12 +24,12 @@ of OurSQL, MySQLdb, or MySQL-connector/Python.
"""
-from sqlalchemy.dialects.mysql.base import MySQLDialect, MySQLExecutionContext
-from sqlalchemy.connectors.pyodbc import PyODBCConnector
-from sqlalchemy.engine import base as engine_base
-from sqlalchemy import util
+from .base import MySQLDialect, MySQLExecutionContext
+from ...connectors.pyodbc import PyODBCConnector
+from ... import util
import re
+
class MySQLExecutionContext_pyodbc(MySQLExecutionContext):
def get_lastrowid(self):
@@ -42,6 +39,7 @@ class MySQLExecutionContext_pyodbc(MySQLExecutionContext):
cursor.close()
return lastrowid
+
class MySQLDialect_pyodbc(PyODBCConnector, MySQLDialect):
supports_unicode_statements = False
execution_ctx_cls = MySQLExecutionContext_pyodbc
diff --git a/libs/sqlalchemy/dialects/mysql/zxjdbc.py b/libs/sqlalchemy/dialects/mysql/zxjdbc.py
index df479043..b5fcfbda 100644
--- a/libs/sqlalchemy/dialects/mysql/zxjdbc.py
+++ b/libs/sqlalchemy/dialects/mysql/zxjdbc.py
@@ -1,23 +1,16 @@
# mysql/zxjdbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the MySQL database via Jython's zxjdbc JDBC connector.
+"""
-JDBC Driver
------------
-
-The official MySQL JDBC driver is at
-http://dev.mysql.com/downloads/connector/j/.
-
-Connecting
-----------
-
-Connect string format:
-
- mysql+zxjdbc://:@[:]/
+.. dialect:: mysql+zxjdbc
+ :name: zxjdbc for Jython
+ :dbapi: zxjdbc
+ :connectstring: mysql+zxjdbc://:@[:]/
+ :driverurl: http://dev.mysql.com/downloads/connector/j/
Character Sets
--------------
@@ -31,9 +24,10 @@ overriden via a ``create_engine`` URL parameter.
"""
import re
-from sqlalchemy import types as sqltypes, util
-from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
-from sqlalchemy.dialects.mysql.base import BIT, MySQLDialect, MySQLExecutionContext
+from ... import types as sqltypes, util
+from ...connectors.zxJDBC import ZxJDBCConnector
+from .base import BIT, MySQLDialect, MySQLExecutionContext
+
class _ZxJDBCBit(BIT):
def result_processor(self, dialect, coltype):
@@ -43,7 +37,7 @@ class _ZxJDBCBit(BIT):
return value
if isinstance(value, bool):
return int(value)
- v = 0L
+ v = 0
for i in value:
v = v << 8 | (i & 0xff)
value = v
@@ -103,7 +97,7 @@ class MySQLDialect_zxjdbc(ZxJDBCConnector, MySQLDialect):
if c:
return int(c)
- def _get_server_version_info(self,connection):
+ def _get_server_version_info(self, connection):
dbapi_con = connection.connection
version = []
r = re.compile('[.\-]')
diff --git a/libs/sqlalchemy/dialects/oracle/__init__.py b/libs/sqlalchemy/dialects/oracle/__init__.py
index a1e2a8dd..070e387d 100644
--- a/libs/sqlalchemy/dialects/oracle/__init__.py
+++ b/libs/sqlalchemy/dialects/oracle/__init__.py
@@ -1,5 +1,5 @@
# oracle/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -12,7 +12,7 @@ from sqlalchemy.dialects.oracle.base import \
VARCHAR, NVARCHAR, CHAR, DATE, DATETIME, NUMBER,\
BLOB, BFILE, CLOB, NCLOB, TIMESTAMP, RAW,\
FLOAT, DOUBLE_PRECISION, LONG, dialect, INTERVAL,\
- VARCHAR2, NVARCHAR2, ROWID
+ VARCHAR2, NVARCHAR2, ROWID, dialect
__all__ = (
diff --git a/libs/sqlalchemy/dialects/oracle/base.py b/libs/sqlalchemy/dialects/oracle/base.py
index f82991bc..e5a16044 100644
--- a/libs/sqlalchemy/dialects/oracle/base.py
+++ b/libs/sqlalchemy/dialects/oracle/base.py
@@ -1,15 +1,14 @@
# oracle/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Oracle database.
+"""
+.. dialect:: oracle
+ :name: Oracle
-Oracle version 8 through current (11g at the time of this writing) are supported.
-
-For information on connecting via specific drivers, see the documentation
-for that driver.
+ Oracle version 8 through current (11g at the time of this writing) are supported.
Connect Arguments
-----------------
@@ -17,12 +16,12 @@ Connect Arguments
The dialect supports several :func:`~sqlalchemy.create_engine()` arguments which
affect the behavior of the dialect regardless of driver in use.
-* *use_ansi* - Use ANSI JOIN constructs (see the section on Oracle 8). Defaults
+* ``use_ansi`` - Use ANSI JOIN constructs (see the section on Oracle 8). Defaults
to ``True``. If ``False``, Oracle-8 compatible constructs are used for joins.
-* *optimize_limits* - defaults to ``False``. see the section on LIMIT/OFFSET.
+* ``optimize_limits`` - defaults to ``False``. see the section on LIMIT/OFFSET.
-* *use_binds_for_limits* - defaults to ``True``. see the section on LIMIT/OFFSET.
+* ``use_binds_for_limits`` - defaults to ``True``. see the section on LIMIT/OFFSET.
Auto Increment Behavior
-----------------------
@@ -100,6 +99,41 @@ http://www.sqlalchemy.org/trac/wiki/UsageRecipes/WindowFunctionsByDefault
which installs a select compiler that overrides the generation of limit/offset with
a window function.
+RETURNING Support
+-----------------
+
+The Oracle database supports a limited form of RETURNING, in order to retrieve result
+sets of matched rows from INSERT, UPDATE and DELETE statements. Oracle's
+RETURNING..INTO syntax only supports one row being returned, as it relies upon
+OUT parameters in order to function. In addition, supported DBAPIs have further
+limitations (see :ref:`cx_oracle_returning`).
+
+SQLAlchemy's "implicit returning" feature, which employs RETURNING within an INSERT
+and sometimes an UPDATE statement in order to fetch newly generated primary key values
+and other SQL defaults and expressions, is normally enabled on the Oracle
+backend. By default, "implicit returning" typically only fetches the value of a
+single ``nextval(some_seq)`` expression embedded into an INSERT in order to increment
+a sequence within an INSERT statement and get the value back at the same time.
+To disable this feature across the board, specify ``implicit_returning=False`` to
+:func:`.create_engine`::
+
+ engine = create_engine("oracle://scott:tiger@dsn", implicit_returning=False)
+
+Implicit returning can also be disabled on a table-by-table basis as a table option::
+
+ # Core Table
+ my_table = Table("my_table", metadata, ..., implicit_returning=False)
+
+
+ # declarative
+ class MyClass(Base):
+ __tablename__ = 'my_table'
+ __table_args__ = {"implicit_returning": False}
+
+.. seealso::
+
+ :ref:`cx_oracle_returning` - additional cx_oracle-specific restrictions on implicit returning.
+
ON UPDATE CASCADE
-----------------
@@ -134,16 +168,16 @@ Synonym/DBLINK Reflection
-------------------------
When using reflection with Table objects, the dialect can optionally search for tables
-indicated by synonyms that reference DBLINK-ed tables by passing the flag
-oracle_resolve_synonyms=True as a keyword argument to the Table construct. If DBLINK
-is not in use this flag should be left off.
+indicated by synonyms, either in local or remote schemas or accessed over DBLINK,
+by passing the flag oracle_resolve_synonyms=True as a
+keyword argument to the Table construct. If synonyms are not in use
+this flag should be left off.
"""
-import random, re
+import re
-from sqlalchemy import schema as sa_schema
-from sqlalchemy import util, sql, log
+from sqlalchemy import util, sql
from sqlalchemy.engine import default, base, reflection
from sqlalchemy.sql import compiler, visitors, expression
from sqlalchemy.sql import operators as sql_operators, functions as sql_functions
@@ -165,18 +199,22 @@ RESERVED_WORDS = \
NO_ARG_FNS = set('UID CURRENT_DATE SYSDATE USER '
'CURRENT_TIME CURRENT_TIMESTAMP'.split())
+
class RAW(sqltypes._Binary):
__visit_name__ = 'RAW'
OracleRaw = RAW
+
class NCLOB(sqltypes.Text):
__visit_name__ = 'NCLOB'
+
class VARCHAR2(VARCHAR):
__visit_name__ = 'VARCHAR2'
NVARCHAR2 = NVARCHAR
+
class NUMBER(sqltypes.Numeric, sqltypes.Integer):
__visit_name__ = 'NUMBER'
@@ -202,18 +240,22 @@ class NUMBER(sqltypes.Numeric, sqltypes.Integer):
class DOUBLE_PRECISION(sqltypes.Numeric):
__visit_name__ = 'DOUBLE_PRECISION'
+
def __init__(self, precision=None, scale=None, asdecimal=None):
if asdecimal is None:
asdecimal = False
super(DOUBLE_PRECISION, self).__init__(precision=precision, scale=scale, asdecimal=asdecimal)
+
class BFILE(sqltypes.LargeBinary):
__visit_name__ = 'BFILE'
+
class LONG(sqltypes.Text):
__visit_name__ = 'LONG'
+
class INTERVAL(sqltypes.TypeEngine):
__visit_name__ = 'INTERVAL'
@@ -244,6 +286,7 @@ class INTERVAL(sqltypes.TypeEngine):
def _type_affinity(self):
return sqltypes.Interval
+
class ROWID(sqltypes.TypeEngine):
"""Oracle ROWID type.
@@ -253,33 +296,32 @@ class ROWID(sqltypes.TypeEngine):
__visit_name__ = 'ROWID'
-
class _OracleBoolean(sqltypes.Boolean):
def get_dbapi_type(self, dbapi):
return dbapi.NUMBER
colspecs = {
- sqltypes.Boolean : _OracleBoolean,
- sqltypes.Interval : INTERVAL,
+ sqltypes.Boolean: _OracleBoolean,
+ sqltypes.Interval: INTERVAL,
}
ischema_names = {
- 'VARCHAR2' : VARCHAR,
- 'NVARCHAR2' : NVARCHAR,
- 'CHAR' : CHAR,
- 'DATE' : DATE,
- 'NUMBER' : NUMBER,
- 'BLOB' : BLOB,
- 'BFILE' : BFILE,
- 'CLOB' : CLOB,
- 'NCLOB' : NCLOB,
- 'TIMESTAMP' : TIMESTAMP,
- 'TIMESTAMP WITH TIME ZONE' : TIMESTAMP,
- 'INTERVAL DAY TO SECOND' : INTERVAL,
- 'RAW' : RAW,
- 'FLOAT' : FLOAT,
- 'DOUBLE PRECISION' : DOUBLE_PRECISION,
- 'LONG' : LONG,
+ 'VARCHAR2': VARCHAR,
+ 'NVARCHAR2': NVARCHAR,
+ 'CHAR': CHAR,
+ 'DATE': DATE,
+ 'NUMBER': NUMBER,
+ 'BLOB': BLOB,
+ 'BFILE': BFILE,
+ 'CLOB': CLOB,
+ 'NCLOB': NCLOB,
+ 'TIMESTAMP': TIMESTAMP,
+ 'TIMESTAMP WITH TIME ZONE': TIMESTAMP,
+ 'INTERVAL DAY TO SECOND': INTERVAL,
+ 'RAW': RAW,
+ 'FLOAT': FLOAT,
+ 'DOUBLE PRECISION': DOUBLE_PRECISION,
+ 'LONG': LONG,
}
@@ -336,9 +378,11 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
if precision is None:
return name
elif scale is None:
- return "%(name)s(%(precision)s)" % {'name':name,'precision': precision}
+ n = "%(name)s(%(precision)s)"
+ return n % {'name': name, 'precision': precision}
else:
- return "%(name)s(%(precision)s, %(scale)s)" % {'name':name,'precision': precision, 'scale' : scale}
+ n = "%(name)s(%(precision)s, %(scale)s)"
+ return n % {'name': name, 'precision': precision, 'scale': scale}
def visit_string(self, type_):
return self.visit_VARCHAR2(type_)
@@ -354,13 +398,14 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
return self._visit_varchar(type_, '', '')
def _visit_varchar(self, type_, n, num):
- if not n and self.dialect._supports_char_length:
- return "VARCHAR%(two)s(%(length)s CHAR)" % {
- 'length' : type_.length,
- 'two':num}
+ if not type_.length:
+ return "%(n)sVARCHAR%(two)s" % {'two': num, 'n': n}
+ elif not n and self.dialect._supports_char_length:
+ varchar = "VARCHAR%(two)s(%(length)s CHAR)"
+ return varchar % {'length': type_.length, 'two': num}
else:
- return "%(n)sVARCHAR%(two)s(%(length)s)" % {'length' : type_.length,
- 'two':num, 'n':n}
+ varchar = "%(n)sVARCHAR%(two)s(%(length)s)"
+ return varchar % {'length': type_.length, 'two': num, 'n': n}
def visit_text(self, type_):
return self.visit_CLOB(type_)
@@ -382,13 +427,14 @@ class OracleTypeCompiler(compiler.GenericTypeCompiler):
def visit_RAW(self, type_):
if type_.length:
- return "RAW(%(length)s)" % {'length' : type_.length}
+ return "RAW(%(length)s)" % {'length': type_.length}
else:
return "RAW"
def visit_ROWID(self, type_):
return "ROWID"
+
class OracleCompiler(compiler.SQLCompiler):
"""Oracle compiler modifies the lexical structure of Select
statements to work under non-ANSI configured Oracle databases, if
@@ -398,7 +444,7 @@ class OracleCompiler(compiler.SQLCompiler):
compound_keywords = util.update_copy(
compiler.SQLCompiler.compound_keywords,
{
- expression.CompoundSelect.EXCEPT : 'MINUS'
+ expression.CompoundSelect.EXCEPT: 'MINUS'
}
)
@@ -407,8 +453,9 @@ class OracleCompiler(compiler.SQLCompiler):
self._quoted_bind_names = {}
super(OracleCompiler, self).__init__(*args, **kwargs)
- def visit_mod(self, binary, **kw):
- return "mod(%s, %s)" % (self.process(binary.left), self.process(binary.right))
+ def visit_mod_binary(self, binary, operator, **kw):
+ return "mod(%s, %s)" % (self.process(binary.left, **kw),
+ self.process(binary.right, **kw))
def visit_now_func(self, fn, **kw):
return "CURRENT_TIMESTAMP"
@@ -416,8 +463,15 @@ class OracleCompiler(compiler.SQLCompiler):
def visit_char_length_func(self, fn, **kw):
return "LENGTH" + self.function_argspec(fn, **kw)
- def visit_match_op(self, binary, **kw):
- return "CONTAINS (%s, %s)" % (self.process(binary.left), self.process(binary.right))
+ def visit_match_op_binary(self, binary, operator, **kw):
+ return "CONTAINS (%s, %s)" % (self.process(binary.left),
+ self.process(binary.right))
+
+ def visit_true(self, expr, **kw):
+ return '1'
+
+ def visit_false(self, expr, **kw):
+ return '0'
def get_select_hint_text(self, byfroms):
return " ".join(
@@ -444,8 +498,13 @@ class OracleCompiler(compiler.SQLCompiler):
return compiler.SQLCompiler.visit_join(self, join, **kwargs)
else:
kwargs['asfrom'] = True
+ if isinstance(join.right, expression.FromGrouping):
+ right = join.right.element
+ else:
+ right = join.right
return self.process(join.left, **kwargs) + \
- ", " + self.process(join.right, **kwargs)
+ ", " + self.process(right, **kwargs)
+
def _get_nonansi_join_whereclause(self, froms):
clauses = []
@@ -454,18 +513,20 @@ class OracleCompiler(compiler.SQLCompiler):
if join.isouter:
def visit_binary(binary):
if binary.operator == sql_operators.eq:
- if binary.left.table is join.right:
+ if join.right.is_derived_from(binary.left.table):
binary.left = _OuterJoinColumn(binary.left)
- elif binary.right.table is join.right:
+ elif join.right.is_derived_from(binary.right.table):
binary.right = _OuterJoinColumn(binary.right)
clauses.append(visitors.cloned_traverse(join.onclause, {},
- {'binary':visit_binary}))
+ {'binary': visit_binary}))
else:
clauses.append(join.onclause)
for j in join.left, join.right:
if isinstance(j, expression.Join):
visit_join(j)
+ elif isinstance(j, expression.FromGrouping):
+ visit_join(j.element)
for f in froms:
if isinstance(f, expression.Join):
@@ -498,20 +559,25 @@ class OracleCompiler(compiler.SQLCompiler):
return self.process(alias.original, **kwargs)
def returning_clause(self, stmt, returning_cols):
+ columns = []
+ binds = []
+ for i, column in enumerate(expression._select_iterables(returning_cols)):
+ if column.type._has_column_expression:
+ col_expr = column.type.column_expression(column)
+ else:
+ col_expr = column
+ outparam = sql.outparam("ret_%d" % i, type_=column.type)
+ self.binds[outparam.key] = outparam
+ binds.append(self.bindparam_string(self._truncate_bindparam(outparam)))
+ columns.append(self.process(col_expr, within_columns_clause=False))
+ self.result_map[outparam.key] = (
+ outparam.key,
+ (column, getattr(column, 'name', None),
+ getattr(column, 'key', None)),
+ column.type
+ )
- def create_out_param(col, i):
- bindparam = sql.outparam("ret_%d" % i, type_=col.type)
- self.binds[bindparam.key] = bindparam
- return self.bindparam_string(self._truncate_bindparam(bindparam))
-
- columnlist = list(expression._select_iterables(returning_cols))
-
- # within_columns_clause =False so that labels (foo AS bar) don't render
- columns = [self.process(c, within_columns_clause=False, result_map=self.result_map) for c in columnlist]
-
- binds = [create_out_param(c, i) for i, c in enumerate(columnlist)]
-
- return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds)
+ return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds)
def _TODO_visit_compound_select(self, select):
"""Need to determine how to get ``LIMIT``/``OFFSET`` into a ``UNION`` for Oracle."""
@@ -524,12 +590,8 @@ class OracleCompiler(compiler.SQLCompiler):
if not getattr(select, '_oracle_visit', None):
if not self.dialect.use_ansi:
- if self.stack and 'from' in self.stack[-1]:
- existingfroms = self.stack[-1]['from']
- else:
- existingfroms = None
-
- froms = select._get_display_froms(existingfroms)
+ froms = self._display_froms_for_select(
+ select, kwargs.get('asfrom', False))
whereclause = self._get_nonansi_join_whereclause(froms)
if whereclause is not None:
select = select.where(whereclause)
@@ -570,7 +632,7 @@ class OracleCompiler(compiler.SQLCompiler):
# If needed, add the ora_rn, and wrap again with offset.
if select._offset is None:
- limitselect.for_update = select.for_update
+ limitselect._for_update_arg = select._for_update_arg
select = limitselect
else:
limitselect = limitselect.column(
@@ -579,7 +641,7 @@ class OracleCompiler(compiler.SQLCompiler):
limitselect._is_wrapper = True
offsetselect = sql.select(
- [c for c in limitselect.c if c.key!='ora_rn'])
+ [c for c in limitselect.c if c.key != 'ora_rn'])
offsetselect._oracle_visit = True
offsetselect._is_wrapper = True
@@ -587,9 +649,9 @@ class OracleCompiler(compiler.SQLCompiler):
if not self.dialect.use_binds_for_limits:
offset_value = sql.literal_column("%d" % offset_value)
offsetselect.append_whereclause(
- sql.literal_column("ora_rn")>offset_value)
+ sql.literal_column("ora_rn") > offset_value)
- offsetselect.for_update = select.for_update
+ offsetselect._for_update_arg = select._for_update_arg
select = offsetselect
kwargs['iswrapper'] = getattr(select, '_is_wrapper', False)
@@ -601,10 +663,20 @@ class OracleCompiler(compiler.SQLCompiler):
def for_update_clause(self, select):
if self.is_subquery():
return ""
- elif select.for_update == "nowait":
- return " FOR UPDATE NOWAIT"
- else:
- return super(OracleCompiler, self).for_update_clause(select)
+
+ tmp = ' FOR UPDATE'
+
+ if select._for_update_arg.of:
+ tmp += ' OF ' + ', '.join(
+ self.process(elem) for elem in
+ select._for_update_arg.of
+ )
+
+ if select._for_update_arg.nowait:
+ tmp += " NOWAIT"
+
+ return tmp
+
class OracleDDLCompiler(compiler.DDLCompiler):
@@ -623,17 +695,22 @@ class OracleDDLCompiler(compiler.DDLCompiler):
return text
+ def visit_create_index(self, create, **kw):
+ return super(OracleDDLCompiler, self).\
+ visit_create_index(create, include_schema=True)
+
+
class OracleIdentifierPreparer(compiler.IdentifierPreparer):
reserved_words = set([x.lower() for x in RESERVED_WORDS])
- illegal_initial_characters = set(xrange(0, 10)).union(["_", "$"])
+ illegal_initial_characters = set(range(0, 10)).union(["_", "$"])
def _bindparam_requires_quotes(self, value):
"""Return True if the given identifier requires quoting."""
lc_value = value.lower()
return (lc_value in self.reserved_words
or value[0] in self.illegal_initial_characters
- or not self.legal_characters.match(unicode(value))
+ or not self.legal_characters.match(util.text_type(value))
)
def format_savepoint(self, savepoint):
@@ -647,6 +724,7 @@ class OracleExecutionContext(default.DefaultExecutionContext):
self.dialect.identifier_preparer.format_sequence(seq) +
".nextval FROM DUAL", type_)
+
class OracleDialect(default.DefaultDialect):
name = 'oracle'
supports_alter = True
@@ -736,10 +814,9 @@ class OracleDialect(default.DefaultDialect):
def normalize_name(self, name):
if name is None:
return None
- # Py2K
- if isinstance(name, str):
- name = name.decode(self.encoding)
- # end Py2K
+ if util.py2k:
+ if isinstance(name, str):
+ name = name.decode(self.encoding)
if name.upper() == name and \
not self.identifier_preparer._requires_quotes(name.lower()):
return name.lower()
@@ -751,16 +828,15 @@ class OracleDialect(default.DefaultDialect):
return None
elif name.lower() == name and not self.identifier_preparer._requires_quotes(name.lower()):
name = name.upper()
- # Py2K
- if not self.supports_unicode_binds:
- name = name.encode(self.encoding)
- else:
- name = unicode(name)
- # end Py2K
+ if util.py2k:
+ if not self.supports_unicode_binds:
+ name = name.encode(self.encoding)
+ else:
+ name = unicode(name)
return name
def _get_default_schema_name(self, connection):
- return self.normalize_name(connection.execute(u'SELECT USER FROM DUAL').scalar())
+ return self.normalize_name(connection.execute('SELECT USER FROM DUAL').scalar())
def _resolve_synonym(self, connection, desired_owner=None, desired_synonym=None, desired_table=None):
"""search for a local synonym matching the given desired owner/name.
@@ -770,14 +846,15 @@ class OracleDialect(default.DefaultDialect):
returns the actual name, owner, dblink name, and synonym name if found.
"""
- q = "SELECT owner, table_owner, table_name, db_link, synonym_name FROM all_synonyms WHERE "
+ q = "SELECT owner, table_owner, table_name, db_link, "\
+ "synonym_name FROM all_synonyms WHERE "
clauses = []
params = {}
if desired_synonym:
clauses.append("synonym_name = :synonym_name")
params['synonym_name'] = desired_synonym
if desired_owner:
- clauses.append("table_owner = :desired_owner")
+ clauses.append("owner = :desired_owner")
params['desired_owner'] = desired_owner
if desired_table:
clauses.append("table_name = :tname")
@@ -808,19 +885,29 @@ class OracleDialect(default.DefaultDialect):
if resolve_synonyms:
actual_name, owner, dblink, synonym = self._resolve_synonym(
- connection,
- desired_owner=self.denormalize_name(schema),
- desired_synonym=self.denormalize_name(table_name)
- )
+ connection,
+ desired_owner=self.denormalize_name(schema),
+ desired_synonym=self.denormalize_name(table_name)
+ )
else:
actual_name, owner, dblink, synonym = None, None, None, None
if not actual_name:
actual_name = self.denormalize_name(table_name)
- if not dblink:
- dblink = ''
- if not owner:
+
+ if dblink:
+ # using user_db_links here since all_db_links appears
+ # to have more restricted permissions.
+ # http://docs.oracle.com/cd/B28359_01/server.111/b28310/ds_admin005.htm
+ # will need to hear from more users if we are doing
+ # the right thing here. See [ticket:2619]
+ owner = connection.scalar(
+ sql.text("SELECT username FROM user_db_links "
+ "WHERE db_link=:link"), link=dblink)
+ dblink = "@" + dblink
+ elif not owner:
owner = self.denormalize_name(schema or self.default_schema_name)
- return (actual_name, owner, dblink, synonym)
+
+ return (actual_name, owner, dblink or '', synonym)
@reflection.cache
def get_schema_names(self, connection, **kw):
@@ -843,7 +930,6 @@ class OracleDialect(default.DefaultDialect):
cursor = connection.execute(s, owner=schema)
return [self.normalize_name(row[0]) for row in cursor]
-
@reflection.cache
def get_view_names(self, connection, schema=None, **kw):
schema = self.denormalize_name(schema or self.default_schema_name)
@@ -877,18 +963,24 @@ class OracleDialect(default.DefaultDialect):
else:
char_length_col = 'data_length'
- c = connection.execute(sql.text(
- "SELECT column_name, data_type, %(char_length_col)s, data_precision, data_scale, "
- "nullable, data_default FROM ALL_TAB_COLUMNS%(dblink)s "
- "WHERE table_name = :table_name AND owner = :owner "
- "ORDER BY column_id" % {'dblink': dblink, 'char_length_col':char_length_col}),
- table_name=table_name, owner=schema)
+ params = {"table_name": table_name}
+ text = "SELECT column_name, data_type, %(char_length_col)s, "\
+ "data_precision, data_scale, "\
+ "nullable, data_default FROM ALL_TAB_COLUMNS%(dblink)s "\
+ "WHERE table_name = :table_name"
+ if schema is not None:
+ params['owner'] = schema
+ text += " AND owner = :owner "
+ text += " ORDER BY column_id"
+ text = text % {'dblink': dblink, 'char_length_col': char_length_col}
+
+ c = connection.execute(sql.text(text), **params)
for row in c:
(colname, orig_colname, coltype, length, precision, scale, nullable, default) = \
- (self.normalize_name(row[0]), row[0], row[1], row[2], row[3], row[4], row[5]=='Y', row[6])
+ (self.normalize_name(row[0]), row[0], row[1], row[2], row[3], row[4], row[5] == 'Y', row[6])
- if coltype == 'NUMBER' :
+ if coltype == 'NUMBER':
coltype = NUMBER(precision, scale)
elif coltype in ('VARCHAR2', 'NVARCHAR2', 'CHAR'):
coltype = self.ischema_names.get(coltype)(length)
@@ -908,7 +1000,7 @@ class OracleDialect(default.DefaultDialect):
'type': coltype,
'nullable': nullable,
'default': default,
- 'autoincrement':default is None
+ 'autoincrement': default is None
}
if orig_colname.lower() == orig_colname:
cdict['quote'] = True
@@ -920,33 +1012,40 @@ class OracleDialect(default.DefaultDialect):
def get_indexes(self, connection, table_name, schema=None,
resolve_synonyms=False, dblink='', **kw):
-
info_cache = kw.get('info_cache')
(table_name, schema, dblink, synonym) = \
self._prepare_reflection_args(connection, table_name, schema,
resolve_synonyms, dblink,
info_cache=info_cache)
indexes = []
- q = sql.text("""
- SELECT a.index_name, a.column_name, b.uniqueness
- FROM ALL_IND_COLUMNS%(dblink)s a,
- ALL_INDEXES%(dblink)s b
- WHERE
- a.index_name = b.index_name
- AND a.table_owner = b.table_owner
- AND a.table_name = b.table_name
- AND a.table_name = :table_name
- AND a.table_owner = :schema
- ORDER BY a.index_name, a.column_position""" % {'dblink': dblink})
- rp = connection.execute(q, table_name=self.denormalize_name(table_name),
- schema=self.denormalize_name(schema))
+ params = {'table_name': table_name}
+ text = \
+ "SELECT a.index_name, a.column_name, b.uniqueness "\
+ "\nFROM ALL_IND_COLUMNS%(dblink)s a, "\
+ "\nALL_INDEXES%(dblink)s b "\
+ "\nWHERE "\
+ "\na.index_name = b.index_name "\
+ "\nAND a.table_owner = b.table_owner "\
+ "\nAND a.table_name = b.table_name "\
+ "\nAND a.table_name = :table_name "
+
+ if schema is not None:
+ params['schema'] = schema
+ text += "AND a.table_owner = :schema "
+
+ text += "ORDER BY a.index_name, a.column_position"
+
+ text = text % {'dblink': dblink}
+
+ q = sql.text(text)
+ rp = connection.execute(q, **params)
indexes = []
last_index_name = None
- pkeys = self.get_primary_keys(connection, table_name, schema,
- resolve_synonyms=resolve_synonyms,
- dblink=dblink,
- info_cache=kw.get('info_cache'))
+ pk_constraint = self.get_pk_constraint(
+ connection, table_name, schema, resolve_synonyms=resolve_synonyms,
+ dblink=dblink, info_cache=kw.get('info_cache'))
+ pkeys = pk_constraint['constrained_columns']
uniqueness = dict(NONUNIQUE=False, UNIQUE=True)
oracle_sys_col = re.compile(r'SYS_NC\d+\$', re.IGNORECASE)
@@ -982,46 +1081,43 @@ class OracleDialect(default.DefaultDialect):
def _get_constraint_data(self, connection, table_name, schema=None,
dblink='', **kw):
- rp = connection.execute(
- sql.text("""SELECT
- ac.constraint_name,
- ac.constraint_type,
- loc.column_name AS local_column,
- rem.table_name AS remote_table,
- rem.column_name AS remote_column,
- rem.owner AS remote_owner,
- loc.position as loc_pos,
- rem.position as rem_pos
- FROM all_constraints%(dblink)s ac,
- all_cons_columns%(dblink)s loc,
- all_cons_columns%(dblink)s rem
- WHERE ac.table_name = :table_name
- AND ac.constraint_type IN ('R','P')
- AND ac.owner = :owner
- AND ac.owner = loc.owner
- AND ac.constraint_name = loc.constraint_name
- AND ac.r_owner = rem.owner(+)
- AND ac.r_constraint_name = rem.constraint_name(+)
- AND (rem.position IS NULL or loc.position=rem.position)
- ORDER BY ac.constraint_name, loc.position""" % {'dblink': dblink}),
- table_name=table_name, owner=schema)
+ params = {'table_name': table_name}
+
+ text = \
+ "SELECT"\
+ "\nac.constraint_name,"\
+ "\nac.constraint_type,"\
+ "\nloc.column_name AS local_column,"\
+ "\nrem.table_name AS remote_table,"\
+ "\nrem.column_name AS remote_column,"\
+ "\nrem.owner AS remote_owner,"\
+ "\nloc.position as loc_pos,"\
+ "\nrem.position as rem_pos"\
+ "\nFROM all_constraints%(dblink)s ac,"\
+ "\nall_cons_columns%(dblink)s loc,"\
+ "\nall_cons_columns%(dblink)s rem"\
+ "\nWHERE ac.table_name = :table_name"\
+ "\nAND ac.constraint_type IN ('R','P')"
+
+ if schema is not None:
+ params['owner'] = schema
+ text += "\nAND ac.owner = :owner"
+
+ text += \
+ "\nAND ac.owner = loc.owner"\
+ "\nAND ac.constraint_name = loc.constraint_name"\
+ "\nAND ac.r_owner = rem.owner(+)"\
+ "\nAND ac.r_constraint_name = rem.constraint_name(+)"\
+ "\nAND (rem.position IS NULL or loc.position=rem.position)"\
+ "\nORDER BY ac.constraint_name, loc.position"
+
+ text = text % {'dblink': dblink}
+ rp = connection.execute(sql.text(text), **params)
constraint_data = rp.fetchall()
return constraint_data
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
- """
-
- kw arguments can be:
-
- oracle_resolve_synonyms
-
- dblink
-
- """
- return self._get_primary_keys(connection, table_name, schema, **kw)[0]
-
@reflection.cache
- def _get_primary_keys(self, connection, table_name, schema=None, **kw):
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
resolve_synonyms = kw.get('oracle_resolve_synonyms', False)
dblink = kw.get('dblink', '')
info_cache = kw.get('info_cache')
@@ -1037,22 +1133,13 @@ class OracleDialect(default.DefaultDialect):
info_cache=kw.get('info_cache'))
for row in constraint_data:
- #print "ROW:" , row
(cons_name, cons_type, local_column, remote_table, remote_column, remote_owner) = \
row[0:2] + tuple([self.normalize_name(x) for x in row[2:6]])
if cons_type == 'P':
if constraint_name is None:
constraint_name = self.normalize_name(cons_name)
pkeys.append(local_column)
- return pkeys, constraint_name
-
- def get_pk_constraint(self, connection, table_name, schema=None, **kw):
- cols, name = self._get_primary_keys(connection, table_name, schema=schema, **kw)
-
- return {
- 'constrained_columns':cols,
- 'name':name
- }
+ return {'constrained_columns': pkeys, 'name': constraint_name}
@reflection.cache
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
@@ -1066,7 +1153,7 @@ class OracleDialect(default.DefaultDialect):
"""
- requested_schema = schema # to check later on
+ requested_schema = schema # to check later on
resolve_synonyms = kw.get('oracle_resolve_synonyms', False)
dblink = kw.get('dblink', '')
info_cache = kw.get('info_cache')
@@ -1082,11 +1169,11 @@ class OracleDialect(default.DefaultDialect):
def fkey_rec():
return {
- 'name' : None,
- 'constrained_columns' : [],
- 'referred_schema' : None,
- 'referred_table' : None,
- 'referred_columns' : []
+ 'name': None,
+ 'constrained_columns': [],
+ 'referred_schema': None,
+ 'referred_table': None,
+ 'referred_columns': []
}
fkeys = util.defaultdict(fkey_rec)
@@ -1101,7 +1188,7 @@ class OracleDialect(default.DefaultDialect):
util.warn(
("Got 'None' querying 'table_name' from "
"all_cons_columns%(dblink)s - does the user have "
- "proper rights to the table?") % {'dblink':dblink})
+ "proper rights to the table?") % {'dblink': dblink})
continue
rec = fkeys[cons_name]
@@ -1128,7 +1215,7 @@ class OracleDialect(default.DefaultDialect):
local_cols.append(local_column)
remote_cols.append(remote_column)
- return fkeys.values()
+ return list(fkeys.values())
@reflection.cache
def get_view_definition(self, connection, view_name, schema=None,
@@ -1138,25 +1225,25 @@ class OracleDialect(default.DefaultDialect):
self._prepare_reflection_args(connection, view_name, schema,
resolve_synonyms, dblink,
info_cache=info_cache)
- s = sql.text("""
- SELECT text FROM all_views
- WHERE owner = :schema
- AND view_name = :view_name
- """)
- rp = connection.execute(s,
- view_name=view_name, schema=schema).scalar()
+
+ params = {'view_name': view_name}
+ text = "SELECT text FROM all_views WHERE view_name=:view_name"
+
+ if schema is not None:
+ text += " AND owner = :schema"
+ params['schema'] = schema
+
+ rp = connection.execute(sql.text(text), **params).scalar()
if rp:
- return rp.decode(self.encoding)
+ if util.py2k:
+ rp = rp.decode(self.encoding)
+ return rp
else:
return None
-
class _OuterJoinColumn(sql.ClauseElement):
__visit_name__ = 'outer_join_column'
def __init__(self, column):
self.column = column
-
-
-
diff --git a/libs/sqlalchemy/dialects/oracle/cx_oracle.py b/libs/sqlalchemy/dialects/oracle/cx_oracle.py
index 0154180d..c427e4bc 100644
--- a/libs/sqlalchemy/dialects/oracle/cx_oracle.py
+++ b/libs/sqlalchemy/dialects/oracle/cx_oracle.py
@@ -1,48 +1,57 @@
# oracle/cx_oracle.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Oracle database via the cx_oracle driver.
+"""
-Driver
-------
+.. dialect:: oracle+cx_oracle
+ :name: cx-Oracle
+ :dbapi: cx_oracle
+ :connectstring: oracle+cx_oracle://user:pass@host:port/dbname[?key=value&key=value...]
+ :url: http://cx-oracle.sourceforge.net/
-The Oracle dialect uses the cx_oracle driver, available at
-http://cx-oracle.sourceforge.net/ . The dialect has several behaviors
-which are specifically tailored towards compatibility with this module.
-Version 5.0 or greater is **strongly** recommended, as SQLAlchemy makes
-extensive use of the cx_oracle output converters for numeric and
-string conversions.
+Additional Connect Arguments
+----------------------------
-Connecting
-----------
+When connecting with ``dbname`` present, the host, port, and dbname tokens are
+converted to a TNS name using
+the cx_oracle ``makedsn()`` function. Otherwise, the host token is taken
+directly as a TNS name.
-Connecting with create_engine() uses the standard URL approach of
-``oracle://user:pass@host:port/dbname[?key=value&key=value...]``. If dbname is present, the
-host, port, and dbname tokens are converted to a TNS name using the cx_oracle
-:func:`makedsn()` function. Otherwise, the host token is taken directly as a TNS name.
+Additional arguments which may be specified either as query string arguments
+on the URL, or as keyword arguments to :func:`.create_engine()` are:
-Additional arguments which may be specified either as query string arguments on the
-URL, or as keyword arguments to :func:`~sqlalchemy.create_engine()` are:
+* allow_twophase - enable two-phase transactions. Defaults to ``True``.
-* *allow_twophase* - enable two-phase transactions. Defaults to ``True``.
-
-* *arraysize* - set the cx_oracle.arraysize value on cursors, in SQLAlchemy
+* arraysize - set the cx_oracle.arraysize value on cursors, in SQLAlchemy
it defaults to 50. See the section on "LOB Objects" below.
-* *auto_convert_lobs* - defaults to True, see the section on LOB objects.
+* auto_convert_lobs - defaults to True, see the section on LOB objects.
-* *auto_setinputsizes* - the cx_oracle.setinputsizes() call is issued for all bind parameters.
- This is required for LOB datatypes but can be disabled to reduce overhead. Defaults
- to ``True``.
+* auto_setinputsizes - the cx_oracle.setinputsizes() call is issued for
+ all bind parameters. This is required for LOB datatypes but can be
+ disabled to reduce overhead. Defaults to ``True``. Specific types
+ can be excluded from this process using the ``exclude_setinputsizes``
+ parameter.
-* *mode* - This is given the string value of SYSDBA or SYSOPER, or alternatively an
- integer value. This value is only available as a URL query string argument.
+* exclude_setinputsizes - a tuple or list of string DBAPI type names to
+ be excluded from the "auto setinputsizes" feature. The type names here
+ must match DBAPI types that are found in the "cx_Oracle" module namespace,
+ such as cx_Oracle.UNICODE, cx_Oracle.NCLOB, etc. Defaults to
+ ``(STRING, UNICODE)``.
-* *threaded* - enable multithreaded access to cx_oracle connections. Defaults
- to ``True``. Note that this is the opposite default of cx_oracle itself.
+ .. versionadded:: 0.8 specific DBAPI types can be excluded from the
+ auto_setinputsizes feature via the exclude_setinputsizes attribute.
+
+* mode - This is given the string value of SYSDBA or SYSOPER, or alternatively
+ an integer value. This value is only available as a URL query string
+ argument.
+
+* threaded - enable multithreaded access to cx_oracle connections. Defaults
+ to ``True``. Note that this is the opposite default of the cx_Oracle DBAPI
+ itself.
Unicode
-------
@@ -56,6 +65,27 @@ of the encoding to be used.
Note that this behavior is disabled when Oracle 8 is detected, as it has been
observed that issues remain when passing Python unicodes to cx_oracle with Oracle 8.
+.. _cx_oracle_returning:
+
+RETURNING Support
+-----------------
+
+cx_oracle supports a limited subset of Oracle's already limited RETURNING support.
+Typically, results can only be guaranteed for at most one column being returned;
+this is the typical case when SQLAlchemy uses RETURNING to get just the value of a
+primary-key-associated sequence value. Additional column expressions will
+cause problems in a non-determinative way, due to cx_oracle's lack of support for
+the OCI_DATA_AT_EXEC API which is required for more complex RETURNING scenarios.
+
+.. seealso::
+
+ http://docs.oracle.com/cd/B10501_01/appdev.920/a96584/oci05bnd.htm#420693 - OCI documentation for RETURNING
+
+ http://sourceforge.net/mailarchive/message.php?msg_id=31338136 - cx_oracle developer commentary
+
+
+
+
LOB Objects
-----------
@@ -66,7 +96,7 @@ like result.fetchmany() and result.fetchall(). This means that by default, LOB
objects are fully fetched unconditionally by SQLAlchemy, and the linkage to a live
cursor is broken.
-To disable this processing, pass ``auto_convert_lobs=False`` to :func:`create_engine()`.
+To disable this processing, pass ``auto_convert_lobs=False`` to :func:`.create_engine()`.
Two Phase Transaction Support
-----------------------------
@@ -99,7 +129,7 @@ the application can make one of several choices:
* For ad-hoc two-phase operations without disabling pooling, the DBAPI
connection in use can be evicted from the connection pool using the
- :class:`.Connection.detach` method.
+ :meth:`.Connection.detach` method.
.. versionchanged:: 0.8.0b2,0.7.10
Support for cx_oracle prepared transactions has been implemented
@@ -173,16 +203,18 @@ a period "." as the decimal character.
"""
-from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, \
- OracleExecutionContext
-from sqlalchemy.dialects.oracle import base as oracle
-from sqlalchemy.engine import base
+from __future__ import absolute_import
+
+from .base import OracleCompiler, OracleDialect, OracleExecutionContext
+from . import base as oracle
+from ...engine import result as _result
from sqlalchemy import types as sqltypes, util, exc, processors
import random
import collections
-from sqlalchemy.util.compat import decimal
+import decimal
import re
+
class _OracleNumeric(sqltypes.Numeric):
def bind_processor(self, dialect):
# cx_oracle accepts Decimal objects and floats
@@ -200,10 +232,8 @@ class _OracleNumeric(sqltypes.Numeric):
if dialect.supports_native_decimal:
if self.asdecimal:
- if self.scale is None:
- fstring = "%.10f"
- else:
- fstring = "%%.%df" % self.scale
+ fstring = "%%.%df" % self._effective_decimal_return_scale
+
def to_decimal(value):
if value is None:
return None
@@ -211,6 +241,7 @@ class _OracleNumeric(sqltypes.Numeric):
return value
else:
return decimal.Decimal(fstring % value)
+
return to_decimal
else:
if self.precision is None and self.scale is None:
@@ -226,6 +257,7 @@ class _OracleNumeric(sqltypes.Numeric):
return super(_OracleNumeric, self).\
result_processor(dialect, coltype)
+
class _OracleDate(sqltypes.Date):
def bind_processor(self, dialect):
return None
@@ -238,6 +270,7 @@ class _OracleDate(sqltypes.Date):
return value
return process
+
class _LOBMixin(object):
def result_processor(self, dialect, coltype):
if not dialect.auto_convert_lobs:
@@ -251,38 +284,40 @@ class _LOBMixin(object):
return value
return process
+
class _NativeUnicodeMixin(object):
- # Py3K
- #pass
- # Py2K
- def bind_processor(self, dialect):
- if dialect._cx_oracle_with_unicode:
- def process(value):
- if value is None:
- return value
- else:
- return unicode(value)
- return process
- else:
- return super(_NativeUnicodeMixin, self).bind_processor(dialect)
- # end Py2K
+ if util.py2k:
+ def bind_processor(self, dialect):
+ if dialect._cx_oracle_with_unicode:
+ def process(value):
+ if value is None:
+ return value
+ else:
+ return unicode(value)
+ return process
+ else:
+ return super(_NativeUnicodeMixin, self).bind_processor(dialect)
# we apply a connection output handler that returns
# unicode in all cases, so the "native_unicode" flag
# will be set for the default String.result_processor.
+
class _OracleChar(_NativeUnicodeMixin, sqltypes.CHAR):
def get_dbapi_type(self, dbapi):
return dbapi.FIXED_CHAR
+
class _OracleNVarChar(_NativeUnicodeMixin, sqltypes.NVARCHAR):
def get_dbapi_type(self, dbapi):
return getattr(dbapi, 'UNICODE', dbapi.STRING)
+
class _OracleText(_LOBMixin, sqltypes.Text):
def get_dbapi_type(self, dbapi):
return dbapi.CLOB
+
class _OracleLong(oracle.LONG):
# a raw LONG is a text type, but does *not*
# get the LobMixin with cx_oracle.
@@ -293,6 +328,7 @@ class _OracleLong(oracle.LONG):
class _OracleString(_NativeUnicodeMixin, sqltypes.String):
pass
+
class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText):
def get_dbapi_type(self, dbapi):
return dbapi.NCLOB
@@ -311,6 +347,7 @@ class _OracleUnicodeText(_LOBMixin, _NativeUnicodeMixin, sqltypes.UnicodeText):
return string_processor(lob_processor(value))
return process
+
class _OracleInteger(sqltypes.Integer):
def result_processor(self, dialect, coltype):
def to_int(val):
@@ -319,6 +356,7 @@ class _OracleInteger(sqltypes.Integer):
return val
return to_int
+
class _OracleBinary(_LOBMixin, sqltypes.LargeBinary):
def get_dbapi_type(self, dbapi):
return dbapi.BLOB
@@ -326,20 +364,26 @@ class _OracleBinary(_LOBMixin, sqltypes.LargeBinary):
def bind_processor(self, dialect):
return None
+
class _OracleInterval(oracle.INTERVAL):
def get_dbapi_type(self, dbapi):
return dbapi.INTERVAL
+
class _OracleRaw(oracle.RAW):
pass
+
class _OracleRowid(oracle.ROWID):
def get_dbapi_type(self, dbapi):
return dbapi.ROWID
+
class OracleCompiler_cx_oracle(OracleCompiler):
def bindparam_string(self, name, **kw):
- if self.preparer._bindparam_requires_quotes(name):
+ quote = getattr(name, 'quote', None)
+ if quote is True or quote is not False and \
+ self.preparer._bindparam_requires_quotes(name):
quoted_name = '"%s"' % name
self._quoted_bind_names[name] = quoted_name
return OracleCompiler.bindparam_string(self, quoted_name, **kw)
@@ -375,7 +419,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
# on String, including that outparams/RETURNING
# breaks for varchars
self.set_input_sizes(quoted_bind_names,
- exclude_types=self.dialect._cx_oracle_exclude_setinputsizes
+ exclude_types=self.dialect.exclude_setinputsizes
)
# if a single execute, check for outparams
@@ -387,11 +431,12 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
if not hasattr(self, 'out_parameters'):
self.out_parameters = {}
if dbtype is None:
- raise exc.InvalidRequestError("Cannot create out parameter for parameter "
- "%r - it's type %r is not supported by"
- " cx_oracle" %
- (name, bindparam.type)
- )
+ raise exc.InvalidRequestError(
+ "Cannot create out parameter for parameter "
+ "%r - it's type %r is not supported by"
+ " cx_oracle" %
+ (bindparam.key, bindparam.type)
+ )
name = self.compiled.bind_names[bindparam]
self.out_parameters[name] = self.cursor.var(dbtype)
self.parameters[0][quoted_bind_names.get(name, name)] = \
@@ -417,10 +462,10 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
for column in self.cursor.description:
type_code = column[1]
if type_code in self.dialect._cx_oracle_binary_types:
- result = base.BufferedColumnResultProxy(self)
+ result = _result.BufferedColumnResultProxy(self)
if result is None:
- result = base.ResultProxy(self)
+ result = _result.ResultProxy(self)
if hasattr(self, 'out_parameters'):
if self.compiled_parameters is not None and \
@@ -448,6 +493,7 @@ class OracleExecutionContext_cx_oracle(OracleExecutionContext):
return result
+
class OracleExecutionContext_cx_oracle_with_unicode(OracleExecutionContext_cx_oracle):
"""Support WITH_UNICODE in Python 2.xx.
@@ -463,13 +509,14 @@ class OracleExecutionContext_cx_oracle_with_unicode(OracleExecutionContext_cx_or
"""
def __init__(self, *arg, **kw):
OracleExecutionContext_cx_oracle.__init__(self, *arg, **kw)
- self.statement = unicode(self.statement)
+ self.statement = util.text_type(self.statement)
def _execute_scalar(self, stmt):
return super(OracleExecutionContext_cx_oracle_with_unicode, self).\
- _execute_scalar(unicode(stmt))
+ _execute_scalar(util.text_type(stmt))
-class ReturningResultProxy(base.FullyBufferedResultProxy):
+
+class ReturningResultProxy(_result.FullyBufferedResultProxy):
"""Result proxy which stuffs the _returning clause + outparams into the fetch."""
def __init__(self, context, returning_params):
@@ -478,19 +525,16 @@ class ReturningResultProxy(base.FullyBufferedResultProxy):
def _cursor_description(self):
returning = self.context.compiled.returning
-
- ret = []
- for c in returning:
- if hasattr(c, 'name'):
- ret.append((c.name, c.type))
- else:
- ret.append((c.anon_label, c.type))
- return ret
+ return [
+ ("ret_%d" % i, None)
+ for i, col in enumerate(returning)
+ ]
def _buffer_rows(self):
return collections.deque([tuple(self._returning_params["ret_%d" % i]
for i, c in enumerate(self._returning_params))])
+
class OracleDialect_cx_oracle(OracleDialect):
execution_ctx_cls = OracleExecutionContext_cx_oracle
statement_compiler = OracleCompiler_cx_oracle
@@ -499,34 +543,36 @@ class OracleDialect_cx_oracle(OracleDialect):
colspecs = colspecs = {
sqltypes.Numeric: _OracleNumeric,
- sqltypes.Date : _OracleDate, # generic type, assume datetime.date is desired
+ sqltypes.Date: _OracleDate, # generic type, assume datetime.date is desired
oracle.DATE: oracle.DATE, # non generic type - passthru
- sqltypes.LargeBinary : _OracleBinary,
- sqltypes.Boolean : oracle._OracleBoolean,
- sqltypes.Interval : _OracleInterval,
- oracle.INTERVAL : _OracleInterval,
- sqltypes.Text : _OracleText,
- sqltypes.String : _OracleString,
- sqltypes.UnicodeText : _OracleUnicodeText,
- sqltypes.CHAR : _OracleChar,
+ sqltypes.LargeBinary: _OracleBinary,
+ sqltypes.Boolean: oracle._OracleBoolean,
+ sqltypes.Interval: _OracleInterval,
+ oracle.INTERVAL: _OracleInterval,
+ sqltypes.Text: _OracleText,
+ sqltypes.String: _OracleString,
+ sqltypes.UnicodeText: _OracleUnicodeText,
+ sqltypes.CHAR: _OracleChar,
# a raw LONG is a text type, but does *not*
# get the LobMixin with cx_oracle.
oracle.LONG: _OracleLong,
- sqltypes.Integer : _OracleInteger, # this is only needed for OUT parameters.
- # it would be nice if we could not use it otherwise.
+ # this is only needed for OUT parameters.
+ # it would be nice if we could not use it otherwise.
+ sqltypes.Integer: _OracleInteger,
+
oracle.RAW: _OracleRaw,
sqltypes.Unicode: _OracleNVarChar,
- sqltypes.NVARCHAR : _OracleNVarChar,
+ sqltypes.NVARCHAR: _OracleNVarChar,
oracle.ROWID: _OracleRowid,
}
-
execute_sequence_format = list
def __init__(self,
auto_setinputsizes=True,
+ exclude_setinputsizes=("STRING", "UNICODE"),
auto_convert_lobs=True,
threaded=True,
allow_twophase=True,
@@ -536,22 +582,25 @@ class OracleDialect_cx_oracle(OracleDialect):
self.threaded = threaded
self.arraysize = arraysize
self.allow_twophase = allow_twophase
- self.supports_timestamp = self.dbapi is None or hasattr(self.dbapi, 'TIMESTAMP' )
+ self.supports_timestamp = self.dbapi is None or \
+ hasattr(self.dbapi, 'TIMESTAMP')
self.auto_setinputsizes = auto_setinputsizes
self.auto_convert_lobs = auto_convert_lobs
if hasattr(self.dbapi, 'version'):
- self.cx_oracle_ver = tuple([int(x) for x in self.dbapi.version.split('.')])
+ self.cx_oracle_ver = tuple([int(x) for x in
+ self.dbapi.version.split('.')])
else:
self.cx_oracle_ver = (0, 0, 0)
def types(*names):
- return set([
- getattr(self.dbapi, name, None) for name in names
- ]).difference([None])
+ return set(
+ getattr(self.dbapi, name, None) for name in names
+ ).difference([None])
- self._cx_oracle_exclude_setinputsizes = types("STRING", "UNICODE")
- self._cx_oracle_string_types = types("STRING", "UNICODE", "NCLOB", "CLOB")
+ self.exclude_setinputsizes = types(*(exclude_setinputsizes or ()))
+ self._cx_oracle_string_types = types("STRING", "UNICODE",
+ "NCLOB", "CLOB")
self._cx_oracle_unicode_types = types("UNICODE", "NCLOB")
self._cx_oracle_binary_types = types("BFILE", "CLOB", "NCLOB", "BLOB")
self.supports_unicode_binds = self.cx_oracle_ver >= (5, 0)
@@ -573,19 +622,23 @@ class OracleDialect_cx_oracle(OracleDialect):
self.supports_unicode_statements = True
self.supports_unicode_binds = True
self._cx_oracle_with_unicode = True
- # Py2K
- # There's really no reason to run with WITH_UNICODE under Python 2.x.
- # Give the user a hint.
- util.warn("cx_Oracle is compiled under Python 2.xx using the "
- "WITH_UNICODE flag. Consider recompiling cx_Oracle without "
- "this flag, which is in no way necessary for full support of Unicode. "
- "Otherwise, all string-holding bind parameters must "
- "be explicitly typed using SQLAlchemy's String type or one of its subtypes,"
- "or otherwise be passed as Python unicode. Plain Python strings "
- "passed as bind parameters will be silently corrupted by cx_Oracle."
- )
- self.execution_ctx_cls = OracleExecutionContext_cx_oracle_with_unicode
- # end Py2K
+
+ if util.py2k:
+ # There's really no reason to run with WITH_UNICODE under Python 2.x.
+ # Give the user a hint.
+ util.warn(
+ "cx_Oracle is compiled under Python 2.xx using the "
+ "WITH_UNICODE flag. Consider recompiling cx_Oracle "
+ "without this flag, which is in no way necessary for full "
+ "support of Unicode. Otherwise, all string-holding bind "
+ "parameters must be explicitly typed using SQLAlchemy's "
+ "String type or one of its subtypes,"
+ "or otherwise be passed as Python unicode. "
+ "Plain Python strings passed as bind parameters will be "
+ "silently corrupted by cx_Oracle."
+ )
+ self.execution_ctx_cls = \
+ OracleExecutionContext_cx_oracle_with_unicode
else:
self._cx_oracle_with_unicode = False
@@ -603,9 +656,10 @@ class OracleDialect_cx_oracle(OracleDialect):
self.dbapi.BLOB: oracle.BLOB(),
self.dbapi.BINARY: oracle.RAW(),
}
+
@classmethod
def dbapi(cls):
- cx_Oracle = __import__('cx_Oracle')
+ import cx_Oracle
return cx_Oracle
def initialize(self, connection):
@@ -668,6 +722,7 @@ class OracleDialect_cx_oracle(OracleDialect):
return
cx_Oracle = self.dbapi
+
def output_type_handler(cursor, name, defaultType,
size, precision, scale):
# convert all NUMBER with precision + positive scale to Decimal
@@ -695,7 +750,7 @@ class OracleDialect_cx_oracle(OracleDialect):
arraysize=cursor.arraysize)
# allow all strings to come back natively as Unicode
elif defaultType in (cx_Oracle.STRING, cx_Oracle.FIXED_CHAR):
- return cursor.var(unicode, size, cursor.arraysize)
+ return cursor.var(util.text_type, size, cursor.arraysize)
def on_connect(conn):
conn.outputtypehandler = output_type_handler
@@ -730,20 +785,19 @@ class OracleDialect_cx_oracle(OracleDialect):
twophase=self.allow_twophase,
)
- # Py2K
- if self._cx_oracle_with_unicode:
- for k, v in opts.items():
- if isinstance(v, str):
- opts[k] = unicode(v)
- else:
- for k, v in opts.items():
- if isinstance(v, unicode):
- opts[k] = str(v)
- # end Py2K
+ if util.py2k:
+ if self._cx_oracle_with_unicode:
+ for k, v in opts.items():
+ if isinstance(v, str):
+ opts[k] = unicode(v)
+ else:
+ for k, v in opts.items():
+ if isinstance(v, unicode):
+ opts[k] = str(v)
if 'mode' in url.query:
opts['mode'] = url.query['mode']
- if isinstance(opts['mode'], basestring):
+ if isinstance(opts['mode'], util.string_types):
mode = opts['mode'].upper()
if mode == 'SYSDBA':
opts['mode'] = self.dbapi.SYSDBA
@@ -769,8 +823,9 @@ class OracleDialect_cx_oracle(OracleDialect):
# ORA-03113: end-of-file on communication channel
# ORA-03135: connection lost contact
# ORA-01033: ORACLE initialization or shutdown in progress
+ # ORA-02396: exceeded maximum idle time, please connect again
# TODO: Others ?
- return error.code in (28, 3114, 3113, 3135, 1033)
+ return error.code in (28, 3114, 3113, 3135, 1033, 2396)
else:
return False
@@ -783,6 +838,11 @@ class OracleDialect_cx_oracle(OracleDialect):
id = random.randint(0, 2 ** 128)
return (0x1234, "%032x" % id, "%032x" % 9)
+ def do_executemany(self, cursor, statement, parameters, context=None):
+ if isinstance(parameters, tuple):
+ parameters = list(parameters)
+ cursor.executemany(statement, parameters)
+
def do_begin_twophase(self, connection, xid):
connection.connection.begin(*xid)
diff --git a/libs/sqlalchemy/dialects/oracle/zxjdbc.py b/libs/sqlalchemy/dialects/oracle/zxjdbc.py
index e4a12ce0..710645b2 100644
--- a/libs/sqlalchemy/dialects/oracle/zxjdbc.py
+++ b/libs/sqlalchemy/dialects/oracle/zxjdbc.py
@@ -1,16 +1,15 @@
# oracle/zxjdbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the Oracle database via the zxjdbc JDBC connector.
-
-JDBC Driver
------------
-
-The official Oracle JDBC driver is at
-http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html.
+"""
+.. dialect:: oracle+zxjdbc
+ :name: zxJDBC for Jython
+ :dbapi: zxjdbc
+ :connectstring: oracle+zxjdbc://user:pass@host/dbname
+ :driverurl: http://www.oracle.com/technology/software/tech/java/sqlj_jdbc/index.html.
"""
import decimal
@@ -19,12 +18,13 @@ import re
from sqlalchemy import sql, types as sqltypes, util
from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
from sqlalchemy.dialects.oracle.base import OracleCompiler, OracleDialect, OracleExecutionContext
-from sqlalchemy.engine import base, default
+from sqlalchemy.engine import result as _result
from sqlalchemy.sql import expression
import collections
SQLException = zxJDBC = None
+
class _ZxJDBCDate(sqltypes.Date):
def result_processor(self, dialect, coltype):
@@ -78,7 +78,7 @@ class OracleCompiler_zxjdbc(OracleCompiler):
self.binds[bindparam.key] = bindparam
binds.append(self.bindparam_string(self._truncate_bindparam(bindparam)))
- return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds)
+ return 'RETURNING ' + ', '.join(columns) + " INTO " + ", ".join(binds)
class OracleExecutionContext_zxjdbc(OracleExecutionContext):
@@ -95,8 +95,8 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext):
try:
try:
rrs = self.statement.__statement__.getReturnResultSet()
- rrs.next()
- except SQLException, sqle:
+ next(rrs)
+ except SQLException as sqle:
msg = '%s [SQLCode: %d]' % (sqle.getMessage(), sqle.getErrorCode())
if sqle.getSQLState() is not None:
msg += ' [SQLState: %s]' % sqle.getSQLState()
@@ -113,7 +113,7 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext):
pass
self.statement.close()
- return base.ResultProxy(self)
+ return _result.ResultProxy(self)
def create_cursor(self):
cursor = self._dbapi_connection.cursor()
@@ -121,7 +121,7 @@ class OracleExecutionContext_zxjdbc(OracleExecutionContext):
return cursor
-class ReturningResultProxy(base.FullyBufferedResultProxy):
+class ReturningResultProxy(_result.FullyBufferedResultProxy):
"""ResultProxy backed by the RETURNING ResultSet results."""
@@ -178,7 +178,7 @@ class OracleDialect_zxjdbc(ZxJDBCConnector, OracleDialect):
colspecs = util.update_copy(
OracleDialect.colspecs,
{
- sqltypes.Date : _ZxJDBCDate,
+ sqltypes.Date: _ZxJDBCDate,
sqltypes.Numeric: _ZxJDBCNumeric
}
)
@@ -189,17 +189,19 @@ class OracleDialect_zxjdbc(ZxJDBCConnector, OracleDialect):
from java.sql import SQLException
from com.ziclix.python.sql import zxJDBC
from com.ziclix.python.sql.handler import OracleDataHandler
- class OracleReturningDataHandler(OracleDataHandler):
+ class OracleReturningDataHandler(OracleDataHandler):
"""zxJDBC DataHandler that specially handles ReturningParam."""
def setJDBCObject(self, statement, index, object, dbtype=None):
if type(object) is ReturningParam:
statement.registerReturnParameter(index, object.type)
elif dbtype is None:
- OracleDataHandler.setJDBCObject(self, statement, index, object)
+ OracleDataHandler.setJDBCObject(
+ self, statement, index, object)
else:
- OracleDataHandler.setJDBCObject(self, statement, index, object, dbtype)
+ OracleDataHandler.setJDBCObject(
+ self, statement, index, object, dbtype)
self.DataHandler = OracleReturningDataHandler
def initialize(self, connection):
diff --git a/libs/sqlalchemy/dialects/postgres.py b/libs/sqlalchemy/dialects/postgres.py
index 82d1a39c..6ed7e18b 100644
--- a/libs/sqlalchemy/dialects/postgres.py
+++ b/libs/sqlalchemy/dialects/postgres.py
@@ -1,5 +1,5 @@
# dialects/postgres.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
diff --git a/libs/sqlalchemy/dialects/postgresql/__init__.py b/libs/sqlalchemy/dialects/postgresql/__init__.py
index 04ae413c..180e9fc7 100644
--- a/libs/sqlalchemy/dialects/postgresql/__init__.py
+++ b/libs/sqlalchemy/dialects/postgresql/__init__.py
@@ -1,20 +1,29 @@
# postgresql/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-from sqlalchemy.dialects.postgresql import base, psycopg2, pg8000, pypostgresql, zxjdbc
+from . import base, psycopg2, pg8000, pypostgresql, zxjdbc
base.dialect = psycopg2.dialect
-from sqlalchemy.dialects.postgresql.base import \
- INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, INET, \
- CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME,\
- DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect
+from .base import \
+ INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, TEXT, NUMERIC, FLOAT, REAL, \
+ INET, CIDR, UUID, BIT, MACADDR, DOUBLE_PRECISION, TIMESTAMP, TIME, \
+ DATE, BYTEA, BOOLEAN, INTERVAL, ARRAY, ENUM, dialect, array, Any, All, \
+ TSVECTOR
+from .constraints import ExcludeConstraint
+from .hstore import HSTORE, hstore
+from .json import JSON, JSONElement
+from .ranges import INT4RANGE, INT8RANGE, NUMRANGE, DATERANGE, TSRANGE, \
+ TSTZRANGE
__all__ = (
-'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC', 'FLOAT', 'REAL', 'INET',
-'CIDR', 'UUID', 'BIT', 'MACADDR', 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME',
-'DATE', 'BYTEA', 'BOOLEAN', 'INTERVAL', 'ARRAY', 'ENUM', 'dialect'
+ 'INTEGER', 'BIGINT', 'SMALLINT', 'VARCHAR', 'CHAR', 'TEXT', 'NUMERIC',
+ 'FLOAT', 'REAL', 'INET', 'CIDR', 'UUID', 'BIT', 'MACADDR',
+ 'DOUBLE_PRECISION', 'TIMESTAMP', 'TIME', 'DATE', 'BYTEA', 'BOOLEAN',
+ 'INTERVAL', 'ARRAY', 'ENUM', 'dialect', 'Any', 'All', 'array', 'HSTORE',
+ 'hstore', 'INT4RANGE', 'INT8RANGE', 'NUMRANGE', 'DATERANGE',
+ 'TSRANGE', 'TSTZRANGE', 'json', 'JSON', 'JSONElement'
)
diff --git a/libs/sqlalchemy/dialects/postgresql/base.py b/libs/sqlalchemy/dialects/postgresql/base.py
index 384b7616..b7979a3e 100644
--- a/libs/sqlalchemy/dialects/postgresql/base.py
+++ b/libs/sqlalchemy/dialects/postgresql/base.py
@@ -1,13 +1,13 @@
# postgresql/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the PostgreSQL database.
+"""
+.. dialect:: postgresql
+ :name: PostgreSQL
-For information on connecting using specific drivers, see the documentation
-section regarding that driver.
Sequences/SERIAL
----------------
@@ -41,23 +41,40 @@ case.
To force the usage of RETURNING by default off, specify the flag
``implicit_returning=False`` to :func:`.create_engine`.
+.. _postgresql_isolation_level:
+
Transaction Isolation Level
---------------------------
-:func:`.create_engine` accepts an ``isolation_level`` parameter which results
-in the command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL
-`` being invoked for every new connection. Valid values for this
-parameter are ``READ COMMITTED``, ``READ UNCOMMITTED``, ``REPEATABLE READ``,
-and ``SERIALIZABLE``::
+All Postgresql dialects support setting of transaction isolation level
+both via a dialect-specific parameter ``isolation_level``
+accepted by :func:`.create_engine`,
+as well as the ``isolation_level`` argument as passed to :meth:`.Connection.execution_options`.
+When using a non-psycopg2 dialect, this feature works by issuing the
+command ``SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL
+`` for each new connection.
+
+To set isolation level using :func:`.create_engine`::
engine = create_engine(
"postgresql+pg8000://scott:tiger@localhost/test",
isolation_level="READ UNCOMMITTED"
)
-When using the psycopg2 dialect, a psycopg2-specific method of setting
-transaction isolation level is used, but the API of ``isolation_level``
-remains the same - see :ref:`psycopg2_isolation`.
+To set using per-connection execution options::
+
+ connection = engine.connect()
+ connection = connection.execution_options(isolation_level="READ COMMITTED")
+
+Valid values for ``isolation_level`` include:
+
+* ``READ COMMITTED``
+* ``READ UNCOMMITTED``
+* ``REPEATABLE READ``
+* ``SERIALIZABLE``
+
+The :mod:`~sqlalchemy.dialects.postgresql.psycopg2` dialect also offers the special level ``AUTOCOMMIT``. See
+:ref:`psycopg2_isolation_level` for details.
Remote / Cross-Schema Table Introspection
@@ -114,6 +131,62 @@ use the :meth:`._UpdateBase.returning` method on a per-statement basis::
where(table.c.name=='foo')
print result.fetchall()
+.. _postgresql_match:
+
+Full Text Search
+----------------
+
+SQLAlchemy makes available the Postgresql ``@@`` operator via the
+:meth:`.ColumnElement.match` method on any textual column expression.
+On a Postgresql dialect, an expression like the following::
+
+ select([sometable.c.text.match("search string")])
+
+will emit to the database::
+
+ SELECT text @@ to_tsquery('search string') FROM table
+
+The Postgresql text search functions such as ``to_tsquery()``
+and ``to_tsvector()`` are available
+explicitly using the standard :attr:`.func` construct. For example::
+
+ select([
+ func.to_tsvector('fat cats ate rats').match('cat & rat')
+ ])
+
+Emits the equivalent of::
+
+ SELECT to_tsvector('fat cats ate rats') @@ to_tsquery('cat & rat')
+
+The :class:`.postgresql.TSVECTOR` type can provide for explicit CAST::
+
+ from sqlalchemy.dialects.postgresql import TSVECTOR
+ from sqlalchemy import select, cast
+ select([cast("some text", TSVECTOR)])
+
+produces a statement equivalent to::
+
+ SELECT CAST('some text' AS TSVECTOR) AS anon_1
+
+
+FROM ONLY ...
+------------------------
+
+The dialect supports PostgreSQL's ONLY keyword for targeting only a particular
+table in an inheritance hierarchy. This can be used to produce the
+``SELECT ... FROM ONLY``, ``UPDATE ONLY ...``, and ``DELETE FROM ONLY ...``
+syntaxes. It uses SQLAlchemy's hints mechanism::
+
+ # SELECT ... FROM ONLY ...
+ result = table.select().with_hint(table, 'ONLY', 'postgresql')
+ print result.fetchall()
+
+ # UPDATE ONLY ...
+ table.update(values=dict(foo='bar')).with_hint('ONLY',
+ dialect_name='postgresql')
+
+ # DELETE FROM ONLY ...
+ table.delete().with_hint('ONLY', dialect_name='postgresql')
.. _postgresql_indexes:
@@ -136,9 +209,10 @@ Operator Classes
^^^^^^^^^^^^^^^^^
PostgreSQL allows the specification of an *operator class* for each column of
-an index (see http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html).
-The :class:`.Index` construct allows these to be specified via the ``postgresql_ops``
-keyword argument::
+an index (see
+http://www.postgresql.org/docs/8.3/interactive/indexes-opclass.html).
+The :class:`.Index` construct allows these to be specified via the
+``postgresql_ops`` keyword argument::
Index('my_index', my_table.c.id, my_table.c.data,
postgresql_ops={
@@ -150,15 +224,15 @@ keyword argument::
``postgresql_ops`` keyword argument to :class:`.Index` construct.
Note that the keys in the ``postgresql_ops`` dictionary are the "key" name of
-the :class:`.Column`, i.e. the name used to access it from the ``.c`` collection
-of :class:`.Table`, which can be configured to be different than the actual
-name of the column as expressed in the database.
+the :class:`.Column`, i.e. the name used to access it from the ``.c``
+collection of :class:`.Table`, which can be configured to be different than
+the actual name of the column as expressed in the database.
Index Types
^^^^^^^^^^^^
-PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well as
-the ability for users to create their own (see
+PostgreSQL provides several index types: B-Tree, Hash, GiST, and GIN, as well
+as the ability for users to create their own (see
http://www.postgresql.org/docs/8.3/static/indexes-types.html). These can be
specified on :class:`.Index` using the ``postgresql_using`` keyword argument::
@@ -169,13 +243,13 @@ underlying CREATE INDEX command, so it *must* be a valid index type for your
version of PostgreSQL.
"""
-
+from collections import defaultdict
import re
-from sqlalchemy import sql, schema, exc, util
-from sqlalchemy.engine import default, reflection
-from sqlalchemy.sql import compiler, expression, util as sql_util
-from sqlalchemy import types as sqltypes
+from ... import sql, schema, exc, util
+from ...engine import default, reflection
+from ...sql import compiler, expression, operators
+from ... import types as sqltypes
try:
from uuid import UUID as _python_UUID
@@ -194,7 +268,7 @@ RESERVED_WORDS = set(
"default", "deferrable", "desc", "distinct", "do", "else", "end",
"except", "false", "fetch", "for", "foreign", "from", "grant", "group",
"having", "in", "initially", "intersect", "into", "leading", "limit",
- "localtime", "localtimestamp", "new", "not", "null", "off", "offset",
+ "localtime", "localtimestamp", "new", "not", "null", "of", "off", "offset",
"old", "on", "only", "or", "order", "placing", "primary", "references",
"returning", "select", "session_user", "some", "symmetric", "table",
"then", "to", "trailing", "true", "union", "unique", "user", "using",
@@ -208,24 +282,30 @@ _DECIMAL_TYPES = (1231, 1700)
_FLOAT_TYPES = (700, 701, 1021, 1022)
_INT_TYPES = (20, 21, 23, 26, 1005, 1007, 1016)
+
class BYTEA(sqltypes.LargeBinary):
__visit_name__ = 'BYTEA'
+
class DOUBLE_PRECISION(sqltypes.Float):
__visit_name__ = 'DOUBLE_PRECISION'
+
class INET(sqltypes.TypeEngine):
__visit_name__ = "INET"
PGInet = INET
+
class CIDR(sqltypes.TypeEngine):
__visit_name__ = "CIDR"
PGCidr = CIDR
+
class MACADDR(sqltypes.TypeEngine):
__visit_name__ = "MACADDR"
PGMacAddr = MACADDR
+
class TIMESTAMP(sqltypes.TIMESTAMP):
def __init__(self, timezone=False, precision=None):
super(TIMESTAMP, self).__init__(timezone=timezone)
@@ -237,6 +317,7 @@ class TIME(sqltypes.TIME):
super(TIME, self).__init__(timezone=timezone)
self.precision = precision
+
class INTERVAL(sqltypes.TypeEngine):
"""Postgresql INTERVAL type.
@@ -245,6 +326,7 @@ class INTERVAL(sqltypes.TypeEngine):
"""
__visit_name__ = 'INTERVAL'
+
def __init__(self, precision=None):
self.precision = precision
@@ -258,8 +340,10 @@ class INTERVAL(sqltypes.TypeEngine):
PGInterval = INTERVAL
+
class BIT(sqltypes.TypeEngine):
__visit_name__ = 'BIT'
+
def __init__(self, length=None, varying=False):
if not varying:
# BIT without VARYING defaults to length 1
@@ -271,6 +355,7 @@ class BIT(sqltypes.TypeEngine):
PGBit = BIT
+
class UUID(sqltypes.TypeEngine):
"""Postgresql UUID type.
@@ -295,15 +380,15 @@ class UUID(sqltypes.TypeEngine):
"""
if as_uuid and _python_UUID is None:
raise NotImplementedError(
- "This version of Python does not support the native UUID type."
- )
+ "This version of Python does not support the native UUID type."
+ )
self.as_uuid = as_uuid
def bind_processor(self, dialect):
if self.as_uuid:
def process(value):
if value is not None:
- value = str(value)
+ value = util.text_type(value)
return value
return process
else:
@@ -321,19 +406,302 @@ class UUID(sqltypes.TypeEngine):
PGUuid = UUID
-class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
+class TSVECTOR(sqltypes.TypeEngine):
+ """The :class:`.postgresql.TSVECTOR` type implements the Postgresql
+ text search type TSVECTOR.
+
+ It can be used to do full text queries on natural language
+ documents.
+
+ .. versionadded:: 0.9.0
+
+ .. seealso::
+
+ :ref:`postgresql_match`
+
+ """
+ __visit_name__ = 'TSVECTOR'
+
+
+
+class _Slice(expression.ColumnElement):
+ __visit_name__ = 'slice'
+ type = sqltypes.NULLTYPE
+
+ def __init__(self, slice_, source_comparator):
+ self.start = source_comparator._check_literal(
+ source_comparator.expr,
+ operators.getitem, slice_.start)
+ self.stop = source_comparator._check_literal(
+ source_comparator.expr,
+ operators.getitem, slice_.stop)
+
+
+class Any(expression.ColumnElement):
+ """Represent the clause ``left operator ANY (right)``. ``right`` must be
+ an array expression.
+
+ .. seealso::
+
+ :class:`.postgresql.ARRAY`
+
+ :meth:`.postgresql.ARRAY.Comparator.any` - ARRAY-bound method
+
+ """
+ __visit_name__ = 'any'
+
+ def __init__(self, left, right, operator=operators.eq):
+ self.type = sqltypes.Boolean()
+ self.left = expression._literal_as_binds(left)
+ self.right = right
+ self.operator = operator
+
+
+class All(expression.ColumnElement):
+ """Represent the clause ``left operator ALL (right)``. ``right`` must be
+ an array expression.
+
+ .. seealso::
+
+ :class:`.postgresql.ARRAY`
+
+ :meth:`.postgresql.ARRAY.Comparator.all` - ARRAY-bound method
+
+ """
+ __visit_name__ = 'all'
+
+ def __init__(self, left, right, operator=operators.eq):
+ self.type = sqltypes.Boolean()
+ self.left = expression._literal_as_binds(left)
+ self.right = right
+ self.operator = operator
+
+
+class array(expression.Tuple):
+ """A Postgresql ARRAY literal.
+
+ This is used to produce ARRAY literals in SQL expressions, e.g.::
+
+ from sqlalchemy.dialects.postgresql import array
+ from sqlalchemy.dialects import postgresql
+ from sqlalchemy import select, func
+
+ stmt = select([
+ array([1,2]) + array([3,4,5])
+ ])
+
+ print stmt.compile(dialect=postgresql.dialect())
+
+ Produces the SQL::
+
+ SELECT ARRAY[%(param_1)s, %(param_2)s] ||
+ ARRAY[%(param_3)s, %(param_4)s, %(param_5)s]) AS anon_1
+
+ An instance of :class:`.array` will always have the datatype
+ :class:`.ARRAY`. The "inner" type of the array is inferred from
+ the values present, unless the ``type_`` keyword argument is passed::
+
+ array(['foo', 'bar'], type_=CHAR)
+
+ .. versionadded:: 0.8 Added the :class:`~.postgresql.array` literal type.
+
+ See also:
+
+ :class:`.postgresql.ARRAY`
+
+ """
+ __visit_name__ = 'array'
+
+ def __init__(self, clauses, **kw):
+ super(array, self).__init__(*clauses, **kw)
+ self.type = ARRAY(self.type)
+
+ def _bind_param(self, operator, obj):
+ return array(*[
+ expression.BindParameter(None, o, _compared_to_operator=operator,
+ _compared_to_type=self.type, unique=True)
+ for o in obj
+ ])
+
+ def self_group(self, against=None):
+ return self
+
+
+class ARRAY(sqltypes.Concatenable, sqltypes.TypeEngine):
"""Postgresql ARRAY type.
Represents values as Python lists.
- The ARRAY type may not be supported on all DBAPIs.
+ An :class:`.ARRAY` type is constructed given the "type"
+ of element::
+
+ mytable = Table("mytable", metadata,
+ Column("data", ARRAY(Integer))
+ )
+
+ The above type represents an N-dimensional array,
+ meaning Postgresql will interpret values with any number
+ of dimensions automatically. To produce an INSERT
+ construct that passes in a 1-dimensional array of integers::
+
+ connection.execute(
+ mytable.insert(),
+ data=[1,2,3]
+ )
+
+ The :class:`.ARRAY` type can be constructed given a fixed number
+ of dimensions::
+
+ mytable = Table("mytable", metadata,
+ Column("data", ARRAY(Integer, dimensions=2))
+ )
+
+ This has the effect of the :class:`.ARRAY` type
+ specifying that number of bracketed blocks when a :class:`.Table`
+ is used in a CREATE TABLE statement, or when the type is used
+ within a :func:`.expression.cast` construct; it also causes
+ the bind parameter and result set processing of the type
+ to optimize itself to expect exactly that number of dimensions.
+ Note that Postgresql itself still allows N dimensions with such a type.
+
+ SQL expressions of type :class:`.ARRAY` have support for "index" and
+ "slice" behavior. The Python ``[]`` operator works normally here, given
+ integer indexes or slices. Note that Postgresql arrays default
+ to 1-based indexing. The operator produces binary expression
+ constructs which will produce the appropriate SQL, both for
+ SELECT statements::
+
+ select([mytable.c.data[5], mytable.c.data[2:7]])
+
+ as well as UPDATE statements when the :meth:`.Update.values` method
+ is used::
+
+ mytable.update().values({
+ mytable.c.data[5]: 7,
+ mytable.c.data[2:7]: [1, 2, 3]
+ })
+
+ :class:`.ARRAY` provides special methods for containment operations,
+ e.g.::
+
+ mytable.c.data.contains([1, 2])
+
+ For a full list of special methods see :class:`.ARRAY.Comparator`.
+
+ .. versionadded:: 0.8 Added support for index and slice operations
+ to the :class:`.ARRAY` type, including support for UPDATE
+ statements, and special array containment operations.
+
+ The :class:`.ARRAY` type may not be supported on all DBAPIs.
It is known to work on psycopg2 and not pg8000.
+ See also:
+
+ :class:`.postgresql.array` - produce a literal array value.
"""
__visit_name__ = 'ARRAY'
- def __init__(self, item_type, mutable=False, as_tuple=False):
+ class Comparator(sqltypes.Concatenable.Comparator):
+ """Define comparison operations for :class:`.ARRAY`."""
+
+ def __getitem__(self, index):
+ if isinstance(index, slice):
+ index = _Slice(index, self)
+ return_type = self.type
+ else:
+ return_type = self.type.item_type
+ return self._binary_operate(self.expr, operators.getitem, index,
+ result_type=return_type)
+
+ def any(self, other, operator=operators.eq):
+ """Return ``other operator ANY (array)`` clause.
+
+ Argument places are switched, because ANY requires array
+ expression to be on the right hand-side.
+
+ E.g.::
+
+ from sqlalchemy.sql import operators
+
+ conn.execute(
+ select([table.c.data]).where(
+ table.c.data.any(7, operator=operators.lt)
+ )
+ )
+
+ :param other: expression to be compared
+ :param operator: an operator object from the
+ :mod:`sqlalchemy.sql.operators`
+ package, defaults to :func:`.operators.eq`.
+
+ .. seealso::
+
+ :class:`.postgresql.Any`
+
+ :meth:`.postgresql.ARRAY.Comparator.all`
+
+ """
+ return Any(other, self.expr, operator=operator)
+
+ def all(self, other, operator=operators.eq):
+ """Return ``other operator ALL (array)`` clause.
+
+ Argument places are switched, because ALL requires array
+ expression to be on the right hand-side.
+
+ E.g.::
+
+ from sqlalchemy.sql import operators
+
+ conn.execute(
+ select([table.c.data]).where(
+ table.c.data.all(7, operator=operators.lt)
+ )
+ )
+
+ :param other: expression to be compared
+ :param operator: an operator object from the
+ :mod:`sqlalchemy.sql.operators`
+ package, defaults to :func:`.operators.eq`.
+
+ .. seealso::
+
+ :class:`.postgresql.All`
+
+ :meth:`.postgresql.ARRAY.Comparator.any`
+
+ """
+ return All(other, self.expr, operator=operator)
+
+ def contains(self, other, **kwargs):
+ """Boolean expression. Test if elements are a superset of the
+ elements of the argument array expression.
+ """
+ return self.expr.op('@>')(other)
+
+ def contained_by(self, other):
+ """Boolean expression. Test if elements are a proper subset of the
+ elements of the argument array expression.
+ """
+ return self.expr.op('<@')(other)
+
+ def overlap(self, other):
+ """Boolean expression. Test if array has elements in common with
+ an argument array expression.
+ """
+ return self.expr.op('&&')(other)
+
+ def _adapt_expression(self, op, other_comparator):
+ if isinstance(op, operators.custom_op):
+ if op.opstring in ['@>', '<@', '&&']:
+ return op, sqltypes.Boolean
+ return sqltypes.Concatenable.Comparator.\
+ _adapt_expression(self, op, other_comparator)
+
+ comparator_factory = Comparator
+
+ def __init__(self, item_type, as_tuple=False, dimensions=None):
"""Construct an ARRAY.
E.g.::
@@ -345,31 +713,20 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
:param item_type: The data type of items of this array. Note that
dimensionality is irrelevant here, so multi-dimensional arrays like
``INTEGER[][]``, are constructed as ``ARRAY(Integer)``, not as
- ``ARRAY(ARRAY(Integer))`` or such. The type mapping figures out on
- the fly
-
- :param mutable=False: Specify whether lists passed to this
- class should be considered mutable - this enables
- "mutable types" mode in the ORM. Be sure to read the
- notes for :class:`.MutableType` regarding ORM
- performance implications.
-
- .. versionchanged:: 0.7.0
- Default changed from ``True``\ .
-
- .. versionchanged:: 0.7
- This functionality is now superseded by the
- ``sqlalchemy.ext.mutable`` extension described in
- :ref:`mutable_toplevel`.
+ ``ARRAY(ARRAY(Integer))`` or such.
:param as_tuple=False: Specify whether return results
should be converted to tuples from lists. DBAPIs such
as psycopg2 return lists by default. When tuples are
- returned, the results are hashable. This flag can only
- be set to ``True`` when ``mutable`` is set to
- ``False``.
+ returned, the results are hashable.
- .. versionadded:: 0.6.5
+ :param dimensions: if non-None, the ARRAY will assume a fixed
+ number of dimensions. This will cause the DDL emitted for this
+ ARRAY to include the exact number of bracket clauses ``[]``,
+ and will also optimize the performance of the type overall.
+ Note that PG arrays are always implicitly "non-dimensioned",
+ meaning they can store any number of dimensions no matter how
+ they were declared.
"""
if isinstance(item_type, ARRAY):
@@ -378,77 +735,68 @@ class ARRAY(sqltypes.MutableType, sqltypes.Concatenable, sqltypes.TypeEngine):
if isinstance(item_type, type):
item_type = item_type()
self.item_type = item_type
- self.mutable = mutable
- if mutable and as_tuple:
- raise exc.ArgumentError(
- "mutable must be set to False if as_tuple is True."
- )
self.as_tuple = as_tuple
-
- def copy_value(self, value):
- if value is None:
- return None
- elif self.mutable:
- return list(value)
- else:
- return value
+ self.dimensions = dimensions
def compare_values(self, x, y):
return x == y
- def is_mutable(self):
- return self.mutable
+ def _proc_array(self, arr, itemproc, dim, collection):
+ if dim is None:
+ arr = list(arr)
+ if dim == 1 or dim is None and (
+ # this has to be (list, tuple), or at least
+ # not hasattr('__iter__'), since Py3K strings
+ # etc. have __iter__
+ not arr or not isinstance(arr[0], (list, tuple))):
+ if itemproc:
+ return collection(itemproc(x) for x in arr)
+ else:
+ return collection(arr)
+ else:
+ return collection(
+ self._proc_array(
+ x, itemproc,
+ dim - 1 if dim is not None else None,
+ collection)
+ for x in arr
+ )
def bind_processor(self, dialect):
- item_proc = self.item_type.dialect_impl(dialect).bind_processor(dialect)
- if item_proc:
- def convert_item(item):
- if isinstance(item, (list, tuple)):
- return [convert_item(child) for child in item]
- else:
- return item_proc(item)
- else:
- def convert_item(item):
- if isinstance(item, (list, tuple)):
- return [convert_item(child) for child in item]
- else:
- return item
+ item_proc = self.item_type.\
+ dialect_impl(dialect).\
+ bind_processor(dialect)
+
def process(value):
if value is None:
return value
- return [convert_item(item) for item in value]
+ else:
+ return self._proc_array(
+ value,
+ item_proc,
+ self.dimensions,
+ list)
return process
def result_processor(self, dialect, coltype):
- item_proc = self.item_type.dialect_impl(dialect).result_processor(dialect, coltype)
- if item_proc:
- def convert_item(item):
- if isinstance(item, list):
- r = [convert_item(child) for child in item]
- if self.as_tuple:
- r = tuple(r)
- return r
- else:
- return item_proc(item)
- else:
- def convert_item(item):
- if isinstance(item, list):
- r = [convert_item(child) for child in item]
- if self.as_tuple:
- r = tuple(r)
- return r
- else:
- return item
+ item_proc = self.item_type.\
+ dialect_impl(dialect).\
+ result_processor(dialect, coltype)
+
def process(value):
if value is None:
return value
- r = [convert_item(item) for item in value]
- if self.as_tuple:
- r = tuple(r)
- return r
+ else:
+ return self._proc_array(
+ value,
+ item_proc,
+ self.dimensions,
+ tuple if self.as_tuple else list)
return process
+
PGArray = ARRAY
+
class ENUM(sqltypes.Enum):
"""Postgresql ENUM type.
@@ -585,71 +933,107 @@ class ENUM(sqltypes.Enum):
self.drop(bind=bind, checkfirst=checkfirst)
colspecs = {
- sqltypes.Interval:INTERVAL,
- sqltypes.Enum:ENUM,
+ sqltypes.Interval: INTERVAL,
+ sqltypes.Enum: ENUM,
}
ischema_names = {
- 'integer' : INTEGER,
- 'bigint' : BIGINT,
- 'smallint' : SMALLINT,
- 'character varying' : VARCHAR,
- 'character' : CHAR,
- '"char"' : sqltypes.String,
- 'name' : sqltypes.String,
- 'text' : TEXT,
- 'numeric' : NUMERIC,
- 'float' : FLOAT,
- 'real' : REAL,
+ 'integer': INTEGER,
+ 'bigint': BIGINT,
+ 'smallint': SMALLINT,
+ 'character varying': VARCHAR,
+ 'character': CHAR,
+ '"char"': sqltypes.String,
+ 'name': sqltypes.String,
+ 'text': TEXT,
+ 'numeric': NUMERIC,
+ 'float': FLOAT,
+ 'real': REAL,
'inet': INET,
'cidr': CIDR,
'uuid': UUID,
'bit': BIT,
'bit varying': BIT,
'macaddr': MACADDR,
- 'double precision' : DOUBLE_PRECISION,
- 'timestamp' : TIMESTAMP,
- 'timestamp with time zone' : TIMESTAMP,
- 'timestamp without time zone' : TIMESTAMP,
- 'time with time zone' : TIME,
- 'time without time zone' : TIME,
- 'date' : DATE,
+ 'double precision': DOUBLE_PRECISION,
+ 'timestamp': TIMESTAMP,
+ 'timestamp with time zone': TIMESTAMP,
+ 'timestamp without time zone': TIMESTAMP,
+ 'time with time zone': TIME,
+ 'time without time zone': TIME,
+ 'date': DATE,
'time': TIME,
- 'bytea' : BYTEA,
- 'boolean' : BOOLEAN,
- 'interval':INTERVAL,
- 'interval year to month':INTERVAL,
- 'interval day to second':INTERVAL,
+ 'bytea': BYTEA,
+ 'boolean': BOOLEAN,
+ 'interval': INTERVAL,
+ 'interval year to month': INTERVAL,
+ 'interval day to second': INTERVAL,
+ 'tsvector' : TSVECTOR
}
-
class PGCompiler(compiler.SQLCompiler):
- def visit_match_op(self, binary, **kw):
+ def visit_array(self, element, **kw):
+ return "ARRAY[%s]" % self.visit_clauselist(element, **kw)
+
+ def visit_slice(self, element, **kw):
+ return "%s:%s" % (
+ self.process(element.start, **kw),
+ self.process(element.stop, **kw),
+ )
+
+ def visit_any(self, element, **kw):
+ return "%s%sANY (%s)" % (
+ self.process(element.left, **kw),
+ compiler.OPERATORS[element.operator],
+ self.process(element.right, **kw)
+ )
+
+ def visit_all(self, element, **kw):
+ return "%s%sALL (%s)" % (
+ self.process(element.left, **kw),
+ compiler.OPERATORS[element.operator],
+ self.process(element.right, **kw)
+ )
+
+ def visit_getitem_binary(self, binary, operator, **kw):
+ return "%s[%s]" % (
+ self.process(binary.left, **kw),
+ self.process(binary.right, **kw)
+ )
+
+ def visit_match_op_binary(self, binary, operator, **kw):
return "%s @@ to_tsquery(%s)" % (
- self.process(binary.left),
- self.process(binary.right))
+ self.process(binary.left, **kw),
+ self.process(binary.right, **kw))
- def visit_ilike_op(self, binary, **kw):
+ def visit_ilike_op_binary(self, binary, operator, **kw):
escape = binary.modifiers.get("escape", None)
- return '%s ILIKE %s' % \
- (self.process(binary.left), self.process(binary.right)) \
- + (escape and
- (' ESCAPE ' + self.render_literal_value(escape, None))
- or '')
- def visit_notilike_op(self, binary, **kw):
+ return '%s ILIKE %s' % \
+ (self.process(binary.left, **kw),
+ self.process(binary.right, **kw)) \
+ + (
+ ' ESCAPE ' +
+ self.render_literal_value(escape, sqltypes.STRINGTYPE)
+ if escape else ''
+ )
+
+ def visit_notilike_op_binary(self, binary, operator, **kw):
escape = binary.modifiers.get("escape", None)
return '%s NOT ILIKE %s' % \
- (self.process(binary.left), self.process(binary.right)) \
- + (escape and
- (' ESCAPE ' + self.render_literal_value(escape, None))
- or '')
+ (self.process(binary.left, **kw),
+ self.process(binary.right, **kw)) \
+ + (
+ ' ESCAPE ' +
+ self.render_literal_value(escape, sqltypes.STRINGTYPE)
+ if escape else ''
+ )
def render_literal_value(self, value, type_):
value = super(PGCompiler, self).render_literal_value(value, type_)
- # TODO: need to inspect "standard_conforming_strings"
+
if self.dialect._backslash_escapes:
value = value.replace('\\', '\\\\')
return value
@@ -660,13 +1044,18 @@ class PGCompiler(compiler.SQLCompiler):
def limit_clause(self, select):
text = ""
if select._limit is not None:
- text += " \n LIMIT " + self.process(sql.literal(select._limit))
+ text += " \n LIMIT " + self.process(sql.literal(select._limit))
if select._offset is not None:
if select._limit is None:
text += " \n LIMIT ALL"
text += " OFFSET " + self.process(sql.literal(select._offset))
return text
+ def format_from_hint_text(self, sqltext, table, hint, iscrud):
+ if hint.upper() != 'ONLY':
+ raise exc.CompileError("Unrecognized hint: %r" % hint)
+ return "ONLY " + sqltext
+
def get_select_precolumns(self, select):
if select._distinct is not False:
if select._distinct is True:
@@ -674,70 +1063,72 @@ class PGCompiler(compiler.SQLCompiler):
elif isinstance(select._distinct, (list, tuple)):
return "DISTINCT ON (" + ', '.join(
[self.process(col) for col in select._distinct]
- )+ ") "
+ ) + ") "
else:
return "DISTINCT ON (" + self.process(select._distinct) + ") "
else:
return ""
def for_update_clause(self, select):
- if select.for_update == 'nowait':
- return " FOR UPDATE NOWAIT"
- elif select.for_update == 'read':
- return " FOR SHARE"
- elif select.for_update == 'read_nowait':
- return " FOR SHARE NOWAIT"
+
+ if select._for_update_arg.read:
+ tmp = " FOR SHARE"
else:
- return super(PGCompiler, self).for_update_clause(select)
+ tmp = " FOR UPDATE"
+
+ if select._for_update_arg.of:
+ tables = util.OrderedSet(
+ c.table if isinstance(c, expression.ColumnClause)
+ else c for c in select._for_update_arg.of)
+ tmp += " OF " + ", ".join(
+ self.process(table, ashint=True)
+ for table in tables
+ )
+
+ if select._for_update_arg.nowait:
+ tmp += " NOWAIT"
+
+ return tmp
def returning_clause(self, stmt, returning_cols):
columns = [
- self.process(
- self.label_select_column(None, c, asfrom=False),
- within_columns_clause=True,
- result_map=self.result_map)
+ self._label_select_column(None, c, True, False, {})
for c in expression._select_iterables(returning_cols)
]
return 'RETURNING ' + ', '.join(columns)
- def visit_extract(self, extract, **kwargs):
- field = self.extract_map.get(extract.field, extract.field)
- if extract.expr.type:
- affinity = extract.expr.type._type_affinity
- else:
- affinity = None
- casts = {
- sqltypes.Date:'date',
- sqltypes.DateTime:'timestamp',
- sqltypes.Interval:'interval', sqltypes.Time:'time'
- }
- cast = casts.get(affinity, None)
- if isinstance(extract.expr, sql.ColumnElement) and cast is not None:
- expr = extract.expr.op('::')(sql.literal_column(cast))
+ def visit_substring_func(self, func, **kw):
+ s = self.process(func.clauses.clauses[0], **kw)
+ start = self.process(func.clauses.clauses[1], **kw)
+ if len(func.clauses.clauses) > 2:
+ length = self.process(func.clauses.clauses[2], **kw)
+ return "SUBSTRING(%s FROM %s FOR %s)" % (s, start, length)
else:
- expr = extract.expr
- return "EXTRACT(%s FROM %s)" % (
- field, self.process(expr))
+ return "SUBSTRING(%s FROM %s)" % (s, start)
class PGDDLCompiler(compiler.DDLCompiler):
def get_column_specification(self, column, **kwargs):
+
colspec = self.preparer.format_column(column)
impl_type = column.type.dialect_impl(self.dialect)
if column.primary_key and \
column is column.table._autoincrement_column and \
- not isinstance(impl_type, sqltypes.SmallInteger) and \
(
+ self.dialect.supports_smallserial or
+ not isinstance(impl_type, sqltypes.SmallInteger)
+ ) and (
column.default is None or
(
isinstance(column.default, schema.Sequence) and
column.default.optional
- )
- ):
+ )):
if isinstance(impl_type, sqltypes.BigInteger):
colspec += " BIGSERIAL"
+ elif isinstance(impl_type, sqltypes.SmallInteger):
+ colspec += " SMALLSERIAL"
else:
colspec += " SERIAL"
else:
@@ -755,7 +1146,9 @@ class PGDDLCompiler(compiler.DDLCompiler):
return "CREATE TYPE %s AS ENUM (%s)" % (
self.preparer.format_type(type_),
- ",".join("'%s'" % e for e in type_.enums)
+ ", ".join(
+ self.sql_compiler.process(sql.literal(e), literal_binds=True)
+ for e in type_.enums)
)
def visit_drop_enum_type(self, drop):
@@ -768,46 +1161,67 @@ class PGDDLCompiler(compiler.DDLCompiler):
def visit_create_index(self, create):
preparer = self.preparer
index = create.element
+ self._verify_index_table(index)
text = "CREATE "
if index.unique:
text += "UNIQUE "
- ops = index.kwargs.get('postgresql_ops', {})
text += "INDEX %s ON %s " % (
- preparer.quote(
- self._index_identifier(index.name), index.quote),
+ self._prepared_index_name(index,
+ include_schema=False),
preparer.format_table(index.table)
)
if 'postgresql_using' in index.kwargs:
using = index.kwargs['postgresql_using']
- text += "USING %s " % preparer.quote(using, index.quote)
+ text += "USING %s " % preparer.quote(using)
+ ops = index.kwargs.get('postgresql_ops', {})
text += "(%s)" \
% (
', '.join([
- preparer.format_column(c) +
+ self.sql_compiler.process(
+ expr.self_group()
+ if not isinstance(expr, expression.ColumnClause)
+ else expr,
+ include_table=False, literal_binds=True) +
(c.key in ops and (' ' + ops[c.key]) or '')
- for c in index.columns])
+ for expr, c in zip(index.expressions, index.columns)])
)
- if "postgres_where" in index.kwargs:
- whereclause = index.kwargs['postgres_where']
- util.warn_deprecated(
- "The 'postgres_where' argument has been renamed "
- "to 'postgresql_where'.")
- elif 'postgresql_where' in index.kwargs:
+ if 'postgresql_where' in index.kwargs:
whereclause = index.kwargs['postgresql_where']
else:
whereclause = None
if whereclause is not None:
- whereclause = sql_util.expression_as_ddl(whereclause)
- where_compiled = self.sql_compiler.process(whereclause)
+ where_compiled = self.sql_compiler.process(
+ whereclause, include_table=False,
+ literal_binds=True)
text += " WHERE " + where_compiled
return text
+ def visit_exclude_constraint(self, constraint):
+ text = ""
+ if constraint.name is not None:
+ text += "CONSTRAINT %s " % \
+ self.preparer.format_constraint(constraint)
+ elements = []
+ for c in constraint.columns:
+ op = constraint.operators[c.name]
+ elements.append(self.preparer.quote(c.name) + ' WITH '+op)
+ text += "EXCLUDE USING %s (%s)" % (constraint.using, ', '.join(elements))
+ if constraint.where is not None:
+ text += ' WHERE (%s)' % self.sql_compiler.process(
+ constraint.where,
+ literal_binds=True)
+ text += self.define_constraint_deferrability(constraint)
+ return text
+
class PGTypeCompiler(compiler.GenericTypeCompiler):
+ def visit_TSVECTOR(self, type):
+ return "TSVECTOR"
+
def visit_INET(self, type_):
return "INET"
@@ -829,6 +1243,30 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
def visit_BIGINT(self, type_):
return "BIGINT"
+ def visit_HSTORE(self, type_):
+ return "HSTORE"
+
+ def visit_JSON(self, type_):
+ return "JSON"
+
+ def visit_INT4RANGE(self, type_):
+ return "INT4RANGE"
+
+ def visit_INT8RANGE(self, type_):
+ return "INT8RANGE"
+
+ def visit_NUMRANGE(self, type_):
+ return "NUMRANGE"
+
+ def visit_DATERANGE(self, type_):
+ return "DATERANGE"
+
+ def visit_TSRANGE(self, type_):
+ return "TSRANGE"
+
+ def visit_TSTZRANGE(self, type_):
+ return "TSTZRANGE"
+
def visit_datetime(self, type_):
return self.visit_TIMESTAMP(type_)
@@ -880,7 +1318,9 @@ class PGTypeCompiler(compiler.GenericTypeCompiler):
return "BYTEA"
def visit_ARRAY(self, type_):
- return self.process(type_.item_type) + '[]'
+ return self.process(type_.item_type) + ('[]' * (type_.dimensions
+ if type_.dimensions
+ is not None else 1))
class PGIdentifierPreparer(compiler.IdentifierPreparer):
@@ -897,11 +1337,12 @@ class PGIdentifierPreparer(compiler.IdentifierPreparer):
if not type_.name:
raise exc.CompileError("Postgresql ENUM type requires a name.")
- name = self.quote(type_.name, type_.quote)
+ name = self.quote(type_.name)
if not self.omit_schema and use_schema and type_.schema is not None:
- name = self.quote_schema(type_.schema, type_.quote) + "." + name
+ name = self.quote_schema(type_.schema) + "." + name
return name
+
class PGInspector(reflection.Inspector):
def __init__(self, conn):
@@ -913,11 +1354,14 @@ class PGInspector(reflection.Inspector):
return self.dialect.get_table_oid(self.bind, table_name, schema,
info_cache=self.info_cache)
+
class CreateEnumType(schema._CreateDropBase):
- __visit_name__ = "create_enum_type"
+ __visit_name__ = "create_enum_type"
+
class DropEnumType(schema._CreateDropBase):
- __visit_name__ = "drop_enum_type"
+ __visit_name__ = "drop_enum_type"
+
class PGExecutionContext(default.DefaultExecutionContext):
def fire_sequence(self, seq, type_):
@@ -947,7 +1391,8 @@ class PGExecutionContext(default.DefaultExecutionContext):
col = column.name
tab = tab[0:29 + max(0, (29 - len(col)))]
col = col[0:29 + max(0, (29 - len(tab)))]
- column._postgresql_seq_name = seq_name = "%s_%s_seq" % (tab, col)
+ name = "%s_%s_seq" % (tab, col)
+ column._postgresql_seq_name = seq_name = name
sch = column.table.schema
if sch is not None:
@@ -961,6 +1406,7 @@ class PGExecutionContext(default.DefaultExecutionContext):
return super(PGExecutionContext, self).get_insert_default(column)
+
class PGDialect(default.DefaultDialect):
name = 'postgresql'
supports_alter = True
@@ -969,6 +1415,7 @@ class PGDialect(default.DefaultDialect):
supports_native_enum = True
supports_native_boolean = True
+ supports_smallserial = True
supports_sequences = True
sequences_optional = True
@@ -977,6 +1424,7 @@ class PGDialect(default.DefaultDialect):
supports_default_values = True
supports_empty_insert = False
+ supports_multivalues_insert = True
default_paramstyle = 'pyformat'
ischema_names = ischema_names
colspecs = colspecs
@@ -989,12 +1437,14 @@ class PGDialect(default.DefaultDialect):
inspector = PGInspector
isolation_level = None
- # TODO: need to inspect "standard_conforming_strings"
_backslash_escapes = True
- def __init__(self, isolation_level=None, **kwargs):
+ def __init__(self, isolation_level=None, json_serializer=None,
+ json_deserializer=None, **kwargs):
default.DefaultDialect.__init__(self, **kwargs)
self.isolation_level = isolation_level
+ self._json_deserializer = json_deserializer
+ self._json_serializer = json_serializer
def initialize(self, connection):
super(PGDialect, self).initialize(connection)
@@ -1008,6 +1458,13 @@ class PGDialect(default.DefaultDialect):
# psycopg2, others may have placed ENUM here as well
self.colspecs.pop(ENUM, None)
+ # http://www.postgresql.org/docs/9.3/static/release-9-2.html#AEN116689
+ self.supports_smallserial = self.server_version_info >= (9, 2)
+
+ self._backslash_escapes = connection.scalar(
+ "show standard_conforming_strings"
+ ) == 'off'
+
def on_connect(self):
if self.isolation_level is not None:
def connect(conn):
@@ -1082,12 +1539,13 @@ class PGDialect(default.DefaultDialect):
return connection.scalar("select current_schema()")
def has_schema(self, connection, schema):
+ query = "select nspname from pg_namespace where lower(nspname)=:schema"
cursor = connection.execute(
sql.text(
- "select nspname from pg_namespace where lower(nspname)=:schema",
+ query,
bindparams=[
sql.bindparam(
- 'schema', unicode(schema.lower()),
+ 'schema', util.text_type(schema.lower()),
type_=sqltypes.Unicode)]
)
)
@@ -1103,7 +1561,7 @@ class PGDialect(default.DefaultDialect):
"n.oid=c.relnamespace where n.nspname=current_schema() and "
"relname=:name",
bindparams=[
- sql.bindparam('name', unicode(table_name),
+ sql.bindparam('name', util.text_type(table_name),
type_=sqltypes.Unicode)]
)
)
@@ -1115,9 +1573,9 @@ class PGDialect(default.DefaultDialect):
"relname=:name",
bindparams=[
sql.bindparam('name',
- unicode(table_name), type_=sqltypes.Unicode),
+ util.text_type(table_name), type_=sqltypes.Unicode),
sql.bindparam('schema',
- unicode(schema), type_=sqltypes.Unicode)]
+ util.text_type(schema), type_=sqltypes.Unicode)]
)
)
return bool(cursor.first())
@@ -1131,7 +1589,7 @@ class PGDialect(default.DefaultDialect):
"n.nspname=current_schema() "
"and relname=:name",
bindparams=[
- sql.bindparam('name', unicode(sequence_name),
+ sql.bindparam('name', util.text_type(sequence_name),
type_=sqltypes.Unicode)
]
)
@@ -1143,10 +1601,10 @@ class PGDialect(default.DefaultDialect):
"n.oid=c.relnamespace where relkind='S' and "
"n.nspname=:schema and relname=:name",
bindparams=[
- sql.bindparam('name', unicode(sequence_name),
+ sql.bindparam('name', util.text_type(sequence_name),
type_=sqltypes.Unicode),
sql.bindparam('schema',
- unicode(schema), type_=sqltypes.Unicode)
+ util.text_type(schema), type_=sqltypes.Unicode)
]
)
)
@@ -1154,12 +1612,6 @@ class PGDialect(default.DefaultDialect):
return bool(cursor.first())
def has_type(self, connection, type_name, schema=None):
- bindparams = [
- sql.bindparam('typname',
- unicode(type_name), type_=sqltypes.Unicode),
- sql.bindparam('nspname',
- unicode(schema), type_=sqltypes.Unicode),
- ]
if schema is not None:
query = """
SELECT EXISTS (
@@ -1169,6 +1621,7 @@ class PGDialect(default.DefaultDialect):
AND n.nspname = :nspname
)
"""
+ query = sql.text(query)
else:
query = """
SELECT EXISTS (
@@ -1177,12 +1630,25 @@ class PGDialect(default.DefaultDialect):
AND pg_type_is_visible(t.oid)
)
"""
- cursor = connection.execute(sql.text(query, bindparams=bindparams))
+ query = sql.text(query)
+ query = query.bindparams(
+ sql.bindparam('typname',
+ util.text_type(type_name), type_=sqltypes.Unicode),
+ )
+ if schema is not None:
+ query = query.bindparams(
+ sql.bindparam('nspname',
+ util.text_type(schema), type_=sqltypes.Unicode),
+ )
+ cursor = connection.execute(query)
return bool(cursor.scalar())
def _get_server_version_info(self, connection):
v = connection.execute("select version()").scalar()
- m = re.match('PostgreSQL (\d+)\.(\d+)(?:\.(\d+))?(?:devel)?', v)
+ m = re.match(
+ '.*(?:PostgreSQL|EnterpriseDB) '
+ '(\d+)\.(\d+)(?:\.(\d+))?(?:\.\d+)?(?:devel)?',
+ v)
if not m:
raise AssertionError(
"Could not determine version from string '%s'" % v)
@@ -1211,15 +1677,13 @@ class PGDialect(default.DefaultDialect):
""" % schema_where_clause
# Since we're binding to unicode, table_name and schema_name must be
# unicode.
- table_name = unicode(table_name)
+ table_name = util.text_type(table_name)
if schema is not None:
- schema = unicode(schema)
- s = sql.text(query, bindparams=[
- sql.bindparam('table_name', type_=sqltypes.Unicode),
- sql.bindparam('schema', type_=sqltypes.Unicode)
- ],
- typemap={'oid':sqltypes.Integer}
- )
+ schema = util.text_type(schema)
+ s = sql.text(query).bindparams(table_name=sqltypes.Unicode)
+ s = s.columns(oid=sqltypes.Integer)
+ if schema:
+ s = s.bindparams(sql.bindparam('schema', type_=sqltypes.Unicode))
c = connection.execute(s, table_name=table_name, schema=schema)
table_oid = c.scalar()
if table_oid is None:
@@ -1235,13 +1699,13 @@ class PGDialect(default.DefaultDialect):
"""
rp = connection.execute(s)
# what about system tables?
- # Py3K
- #schema_names = [row[0] for row in rp \
- # if not row[0].startswith('pg_')]
- # Py2K
- schema_names = [row[0].decode(self.encoding) for row in rp \
+
+ if util.py2k:
+ schema_names = [row[0].decode(self.encoding) for row in rp \
+ if not row[0].startswith('pg_')]
+ else:
+ schema_names = [row[0] for row in rp \
if not row[0].startswith('pg_')]
- # end Py2K
return schema_names
@reflection.cache
@@ -1252,17 +1716,16 @@ class PGDialect(default.DefaultDialect):
current_schema = self.default_schema_name
result = connection.execute(
- sql.text(u"SELECT relname FROM pg_class c "
+ sql.text("SELECT relname FROM pg_class c "
"WHERE relkind = 'r' "
"AND '%s' = (select nspname from pg_namespace n "
"where n.oid = c.relnamespace) " %
current_schema,
- typemap = {'relname':sqltypes.Unicode}
+ typemap={'relname': sqltypes.Unicode}
)
)
return [row[0] for row in result]
-
@reflection.cache
def get_view_names(self, connection, schema=None, **kw):
if schema is not None:
@@ -1276,12 +1739,12 @@ class PGDialect(default.DefaultDialect):
AND '%(schema)s' = (select nspname from pg_namespace n
where n.oid = c.relnamespace)
""" % dict(schema=current_schema)
- # Py3K
- #view_names = [row[0] for row in connection.execute(s)]
- # Py2K
- view_names = [row[0].decode(self.encoding)
+
+ if util.py2k:
+ view_names = [row[0].decode(self.encoding)
for row in connection.execute(s)]
- # end Py2K
+ else:
+ view_names = [row[0] for row in connection.execute(s)]
return view_names
@reflection.cache
@@ -1298,11 +1761,10 @@ class PGDialect(default.DefaultDialect):
rp = connection.execute(sql.text(s),
view_name=view_name, schema=current_schema)
if rp:
- # Py3K
- #view_def = rp.scalar()
- # Py2K
- view_def = rp.scalar().decode(self.encoding)
- # end Py2K
+ if util.py2k:
+ view_def = rp.scalar().decode(self.encoding)
+ else:
+ view_def = rp.scalar()
return view_def
@reflection.cache
@@ -1313,8 +1775,7 @@ class PGDialect(default.DefaultDialect):
SQL_COLS = """
SELECT a.attname,
pg_catalog.format_type(a.atttypid, a.atttypmod),
- (SELECT substring(pg_catalog.pg_get_expr(d.adbin, d.adrelid)
- for 128)
+ (SELECT pg_catalog.pg_get_expr(d.adbin, d.adrelid)
FROM pg_catalog.pg_attrdef d
WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum
AND a.atthasdef)
@@ -1327,7 +1788,7 @@ class PGDialect(default.DefaultDialect):
"""
s = sql.text(SQL_COLS,
bindparams=[sql.bindparam('table_oid', type_=sqltypes.Integer)],
- typemap={'attname':sqltypes.Unicode, 'default':sqltypes.Unicode}
+ typemap={'attname': sqltypes.Unicode, 'default': sqltypes.Unicode}
)
c = connection.execute(s, table_oid=table_oid)
rows = c.fetchall()
@@ -1337,117 +1798,125 @@ class PGDialect(default.DefaultDialect):
# format columns
columns = []
for name, format_type, default, notnull, attnum, table_oid in rows:
- ## strip (5) from character varying(5), timestamp(5)
- # with time zone, etc
- attype = re.sub(r'\([\d,]+\)', '', format_type)
-
- # strip '[]' from integer[], etc.
- attype = re.sub(r'\[\]', '', attype)
-
- nullable = not notnull
- is_array = format_type.endswith('[]')
- charlen = re.search('\(([\d,]+)\)', format_type)
- if charlen:
- charlen = charlen.group(1)
- kwargs = {}
- args = None
-
- if attype == 'numeric':
- if charlen:
- prec, scale = charlen.split(',')
- args = (int(prec), int(scale))
- else:
- args = ()
- elif attype == 'double precision':
- args = (53, )
- elif attype == 'integer':
- args = ()
- elif attype in ('timestamp with time zone',
- 'time with time zone'):
- kwargs['timezone'] = True
- if charlen:
- kwargs['precision'] = int(charlen)
- args = ()
- elif attype in ('timestamp without time zone',
- 'time without time zone', 'time'):
- kwargs['timezone'] = False
- if charlen:
- kwargs['precision'] = int(charlen)
- args = ()
- elif attype == 'bit varying':
- kwargs['varying'] = True
- if charlen:
- args = (int(charlen),)
- else:
- args = ()
- elif attype in ('interval','interval year to month',
- 'interval day to second'):
- if charlen:
- kwargs['precision'] = int(charlen)
- args = ()
- elif charlen:
- args = (int(charlen),)
- else:
- args = ()
-
- while True:
- if attype in self.ischema_names:
- coltype = self.ischema_names[attype]
- break
- elif attype in enums:
- enum = enums[attype]
- coltype = ENUM
- if "." in attype:
- kwargs['schema'], kwargs['name'] = attype.split('.')
- else:
- kwargs['name'] = attype
- args = tuple(enum['labels'])
- break
- elif attype in domains:
- domain = domains[attype]
- attype = domain['attype']
- # A table can't override whether the domain is nullable.
- nullable = domain['nullable']
- if domain['default'] and not default:
- # It can, however, override the default
- # value, but can't set it to null.
- default = domain['default']
- continue
- else:
- coltype = None
- break
-
- if coltype:
- coltype = coltype(*args, **kwargs)
- if is_array:
- coltype = ARRAY(coltype)
- else:
- util.warn("Did not recognize type '%s' of column '%s'" %
- (attype, name))
- coltype = sqltypes.NULLTYPE
- # adjust the default value
- autoincrement = False
- if default is not None:
- match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
- if match is not None:
- autoincrement = True
- # the default is related to a Sequence
- sch = schema
- if '.' not in match.group(2) and sch is not None:
- # unconditionally quote the schema name. this could
- # later be enhanced to obey quoting rules /
- # "quote schema"
- default = match.group(1) + \
- ('"%s"' % sch) + '.' + \
- match.group(2) + match.group(3)
-
- column_info = dict(name=name, type=coltype, nullable=nullable,
- default=default, autoincrement=autoincrement)
+ column_info = self._get_column_info(
+ name, format_type, default, notnull, domains, enums, schema)
columns.append(column_info)
return columns
+ def _get_column_info(self, name, format_type, default,
+ notnull, domains, enums, schema):
+ ## strip (*) from character varying(5), timestamp(5)
+ # with time zone, geometry(POLYGON), etc.
+ attype = re.sub(r'\(.*\)', '', format_type)
+
+ # strip '[]' from integer[], etc.
+ attype = re.sub(r'\[\]', '', attype)
+
+ nullable = not notnull
+ is_array = format_type.endswith('[]')
+ charlen = re.search('\(([\d,]+)\)', format_type)
+ if charlen:
+ charlen = charlen.group(1)
+ args = re.search('\((.*)\)', format_type)
+ if args and args.group(1):
+ args = tuple(re.split('\s*,\s*', args.group(1)))
+ else:
+ args = ()
+ kwargs = {}
+
+ if attype == 'numeric':
+ if charlen:
+ prec, scale = charlen.split(',')
+ args = (int(prec), int(scale))
+ else:
+ args = ()
+ elif attype == 'double precision':
+ args = (53, )
+ elif attype == 'integer':
+ args = ()
+ elif attype in ('timestamp with time zone',
+ 'time with time zone'):
+ kwargs['timezone'] = True
+ if charlen:
+ kwargs['precision'] = int(charlen)
+ args = ()
+ elif attype in ('timestamp without time zone',
+ 'time without time zone', 'time'):
+ kwargs['timezone'] = False
+ if charlen:
+ kwargs['precision'] = int(charlen)
+ args = ()
+ elif attype == 'bit varying':
+ kwargs['varying'] = True
+ if charlen:
+ args = (int(charlen),)
+ else:
+ args = ()
+ elif attype in ('interval', 'interval year to month',
+ 'interval day to second'):
+ if charlen:
+ kwargs['precision'] = int(charlen)
+ args = ()
+ elif charlen:
+ args = (int(charlen),)
+
+ while True:
+ if attype in self.ischema_names:
+ coltype = self.ischema_names[attype]
+ break
+ elif attype in enums:
+ enum = enums[attype]
+ coltype = ENUM
+ if "." in attype:
+ kwargs['schema'], kwargs['name'] = attype.split('.')
+ else:
+ kwargs['name'] = attype
+ args = tuple(enum['labels'])
+ break
+ elif attype in domains:
+ domain = domains[attype]
+ attype = domain['attype']
+ # A table can't override whether the domain is nullable.
+ nullable = domain['nullable']
+ if domain['default'] and not default:
+ # It can, however, override the default
+ # value, but can't set it to null.
+ default = domain['default']
+ continue
+ else:
+ coltype = None
+ break
+
+ if coltype:
+ coltype = coltype(*args, **kwargs)
+ if is_array:
+ coltype = ARRAY(coltype)
+ else:
+ util.warn("Did not recognize type '%s' of column '%s'" %
+ (attype, name))
+ coltype = sqltypes.NULLTYPE
+ # adjust the default value
+ autoincrement = False
+ if default is not None:
+ match = re.search(r"""(nextval\(')([^']+)('.*$)""", default)
+ if match is not None:
+ autoincrement = True
+ # the default is related to a Sequence
+ sch = schema
+ if '.' not in match.group(2) and sch is not None:
+ # unconditionally quote the schema name. this could
+ # later be enhanced to obey quoting rules /
+ # "quote schema"
+ default = match.group(1) + \
+ ('"%s"' % sch) + '.' + \
+ match.group(2) + match.group(3)
+
+ column_info = dict(name=name, type=coltype, nullable=nullable,
+ default=default, autoincrement=autoincrement)
+ return column_info
+
@reflection.cache
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
table_oid = self.get_table_oid(connection, table_name, schema,
info_cache=kw.get('info_cache'))
@@ -1479,16 +1948,7 @@ class PGDialect(default.DefaultDialect):
"""
t = sql.text(PK_SQL, typemap={'attname': sqltypes.Unicode})
c = connection.execute(t, table_oid=table_oid)
- primary_keys = [r[0] for r in c.fetchall()]
- return primary_keys
-
- @reflection.cache
- def get_pk_constraint(self, connection, table_name, schema=None, **kw):
- cols = self.get_primary_keys(connection, table_name,
- schema=schema, **kw)
-
- table_oid = self.get_table_oid(connection, table_name, schema,
- info_cache=kw.get('info_cache'))
+ cols = [r[0] for r in c.fetchall()]
PK_CONS_SQL = """
SELECT conname
@@ -1496,13 +1956,11 @@ class PGDialect(default.DefaultDialect):
WHERE r.conrelid = :table_oid AND r.contype = 'p'
ORDER BY 1
"""
- t = sql.text(PK_CONS_SQL, typemap={'conname':sqltypes.Unicode})
+ t = sql.text(PK_CONS_SQL, typemap={'conname': sqltypes.Unicode})
c = connection.execute(t, table_oid=table_oid)
name = c.scalar()
- return {
- 'constrained_columns':cols,
- 'name':name
- }
+
+ return {'constrained_columns': cols, 'name': name}
@reflection.cache
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
@@ -1524,22 +1982,34 @@ class PGDialect(default.DefaultDialect):
n.oid = c.relnamespace
ORDER BY 1
"""
+ # http://www.postgresql.org/docs/9.0/static/sql-createtable.html
+ FK_REGEX = re.compile(
+ r'FOREIGN KEY \((.*?)\) REFERENCES (?:(.*?)\.)?(.*?)\((.*?)\)'
+ r'[\s]?(MATCH (FULL|PARTIAL|SIMPLE)+)?'
+ r'[\s]?(ON UPDATE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?'
+ r'[\s]?(ON DELETE (CASCADE|RESTRICT|NO ACTION|SET NULL|SET DEFAULT)+)?'
+ r'[\s]?(DEFERRABLE|NOT DEFERRABLE)?'
+ r'[\s]?(INITIALLY (DEFERRED|IMMEDIATE)+)?'
+ )
t = sql.text(FK_SQL, typemap={
- 'conname':sqltypes.Unicode,
- 'condef':sqltypes.Unicode})
+ 'conname': sqltypes.Unicode,
+ 'condef': sqltypes.Unicode})
c = connection.execute(t, table=table_oid)
fkeys = []
for conname, condef, conschema in c.fetchall():
- m = re.search('FOREIGN KEY \((.*?)\) REFERENCES '
- '(?:(.*?)\.)?(.*?)\((.*?)\)', condef).groups()
+ m = re.search(FK_REGEX, condef).groups()
constrained_columns, referred_schema, \
- referred_table, referred_columns = m
+ referred_table, referred_columns, \
+ _, match, _, onupdate, _, ondelete, \
+ deferrable, _, initially = m
+ if deferrable is not None:
+ deferrable = True if deferrable == 'DEFERRABLE' else False
constrained_columns = [preparer._unquote_identifier(x)
for x in re.split(r'\s*,\s*', constrained_columns)]
if referred_schema:
- referred_schema =\
+ referred_schema = \
preparer._unquote_identifier(referred_schema)
elif schema is not None and schema == conschema:
# no schema was returned by pg_get_constraintdef(). This
@@ -1553,11 +2023,18 @@ class PGDialect(default.DefaultDialect):
referred_columns = [preparer._unquote_identifier(x)
for x in re.split(r'\s*,\s', referred_columns)]
fkey_d = {
- 'name' : conname,
- 'constrained_columns' : constrained_columns,
- 'referred_schema' : referred_schema,
- 'referred_table' : referred_table,
- 'referred_columns' : referred_columns
+ 'name': conname,
+ 'constrained_columns': constrained_columns,
+ 'referred_schema': referred_schema,
+ 'referred_table': referred_table,
+ 'referred_columns': referred_columns,
+ 'options': {
+ 'onupdate': onupdate,
+ 'ondelete': ondelete,
+ 'deferrable': deferrable,
+ 'initially': initially,
+ 'match': match
+ }
}
fkeys.append(fkey_d)
return fkeys
@@ -1567,11 +2044,14 @@ class PGDialect(default.DefaultDialect):
table_oid = self.get_table_oid(connection, table_name, schema,
info_cache=kw.get('info_cache'))
+ # cast indkey as varchar since it's an int2vector,
+ # returned as a list by some drivers such as pypostgresql
+
IDX_SQL = """
SELECT
i.relname as relname,
ix.indisunique, ix.indexprs, ix.indpred,
- a.attname
+ a.attname, a.attnum, ix.indkey::varchar
FROM
pg_class t
join pg_index ix on t.oid = ix.indrelid
@@ -1588,14 +2068,15 @@ class PGDialect(default.DefaultDialect):
i.relname
"""
- t = sql.text(IDX_SQL, typemap={'attname':sqltypes.Unicode})
+ t = sql.text(IDX_SQL, typemap={'attname': sqltypes.Unicode})
c = connection.execute(t, table_oid=table_oid)
- index_names = {}
- indexes = []
+ indexes = defaultdict(lambda: defaultdict(dict))
+
sv_idx_name = None
for row in c.fetchall():
- idx_name, unique, expr, prd, col = row
+ idx_name, unique, expr, prd, col, col_num, idx_key = row
+
if expr:
if idx_name != sv_idx_name:
util.warn(
@@ -1604,22 +2085,61 @@ class PGDialect(default.DefaultDialect):
% idx_name)
sv_idx_name = idx_name
continue
+
if prd and not idx_name == sv_idx_name:
util.warn(
"Predicate of partial index %s ignored during reflection"
% idx_name)
sv_idx_name = idx_name
- if idx_name in index_names:
- index_d = index_names[idx_name]
- else:
- index_d = {'column_names':[]}
- indexes.append(index_d)
- index_names[idx_name] = index_d
- index_d['name'] = idx_name
+
+ index = indexes[idx_name]
if col is not None:
- index_d['column_names'].append(col)
- index_d['unique'] = unique
- return indexes
+ index['cols'][col_num] = col
+ index['key'] = [int(k.strip()) for k in idx_key.split()]
+ index['unique'] = unique
+
+ return [
+ {'name': name,
+ 'unique': idx['unique'],
+ 'column_names': [idx['cols'][i] for i in idx['key']]}
+ for name, idx in indexes.items()
+ ]
+
+ @reflection.cache
+ def get_unique_constraints(self, connection, table_name,
+ schema=None, **kw):
+ table_oid = self.get_table_oid(connection, table_name, schema,
+ info_cache=kw.get('info_cache'))
+
+ UNIQUE_SQL = """
+ SELECT
+ cons.conname as name,
+ cons.conkey as key,
+ a.attnum as col_num,
+ a.attname as col_name
+ FROM
+ pg_catalog.pg_constraint cons
+ join pg_attribute a
+ on cons.conrelid = a.attrelid AND a.attnum = ANY(cons.conkey)
+ WHERE
+ cons.conrelid = :table_oid AND
+ cons.contype = 'u'
+ """
+
+ t = sql.text(UNIQUE_SQL, typemap={'col_name': sqltypes.Unicode})
+ c = connection.execute(t, table_oid=table_oid)
+
+ uniques = defaultdict(lambda: defaultdict(dict))
+ for row in c.fetchall():
+ uc = uniques[row.name]
+ uc["key"] = row.key
+ uc["cols"][row.col_num] = row.col_name
+
+ return [
+ {'name': name,
+ 'column_names': [uc["cols"][i] for i in uc["key"]]}
+ for name, uc in uniques.items()
+ ]
def _load_enums(self, connection):
if not self.supports_native_enum:
@@ -1641,8 +2161,8 @@ class PGDialect(default.DefaultDialect):
"""
s = sql.text(SQL_ENUMS, typemap={
- 'attname':sqltypes.Unicode,
- 'label':sqltypes.Unicode})
+ 'attname': sqltypes.Unicode,
+ 'label': sqltypes.Unicode})
c = connection.execute(s)
enums = {}
@@ -1679,7 +2199,7 @@ class PGDialect(default.DefaultDialect):
WHERE t.typtype = 'd'
"""
- s = sql.text(SQL_DOMAINS, typemap={'attname':sqltypes.Unicode})
+ s = sql.text(SQL_DOMAINS, typemap={'attname': sqltypes.Unicode})
c = connection.execute(s)
domains = {}
@@ -1696,10 +2216,9 @@ class PGDialect(default.DefaultDialect):
name = "%s.%s" % (domain['schema'], domain['name'])
domains[name] = {
- 'attype':attype,
+ 'attype': attype,
'nullable': domain['nullable'],
'default': domain['default']
}
return domains
-
diff --git a/libs/sqlalchemy/dialects/postgresql/constraints.py b/libs/sqlalchemy/dialects/postgresql/constraints.py
new file mode 100644
index 00000000..f45cef1a
--- /dev/null
+++ b/libs/sqlalchemy/dialects/postgresql/constraints.py
@@ -0,0 +1,73 @@
+# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+from sqlalchemy.schema import ColumnCollectionConstraint
+from sqlalchemy.sql import expression
+
+class ExcludeConstraint(ColumnCollectionConstraint):
+ """A table-level EXCLUDE constraint.
+
+ Defines an EXCLUDE constraint as described in the `postgres
+ documentation`__.
+
+ __ http://www.postgresql.org/docs/9.0/static/sql-createtable.html#SQL-CREATETABLE-EXCLUDE
+ """
+
+ __visit_name__ = 'exclude_constraint'
+
+ where = None
+
+ def __init__(self, *elements, **kw):
+ """
+ :param \*elements:
+ A sequence of two tuples of the form ``(column, operator)`` where
+ column must be a column name or Column object and operator must
+ be a string containing the operator to use.
+
+ :param name:
+ Optional, the in-database name of this constraint.
+
+ :param deferrable:
+ Optional bool. If set, emit DEFERRABLE or NOT DEFERRABLE when
+ issuing DDL for this constraint.
+
+ :param initially:
+ Optional string. If set, emit INITIALLY when issuing DDL
+ for this constraint.
+
+ :param using:
+ Optional string. If set, emit USING when issuing DDL
+ for this constraint. Defaults to 'gist'.
+
+ :param where:
+ Optional string. If set, emit WHERE when issuing DDL
+ for this constraint.
+
+ """
+ ColumnCollectionConstraint.__init__(
+ self,
+ *[col for col, op in elements],
+ name=kw.get('name'),
+ deferrable=kw.get('deferrable'),
+ initially=kw.get('initially')
+ )
+ self.operators = {}
+ for col_or_string, op in elements:
+ name = getattr(col_or_string, 'name', col_or_string)
+ self.operators[name] = op
+ self.using = kw.get('using', 'gist')
+ where = kw.get('where')
+ if where:
+ self.where = expression._literal_as_text(where)
+
+ def copy(self, **kw):
+ elements = [(col, self.operators[col])
+ for col in self.columns.keys()]
+ c = self.__class__(*elements,
+ name=self.name,
+ deferrable=self.deferrable,
+ initially=self.initially)
+ c.dispatch._update(self.dispatch)
+ return c
+
diff --git a/libs/sqlalchemy/dialects/postgresql/hstore.py b/libs/sqlalchemy/dialects/postgresql/hstore.py
new file mode 100644
index 00000000..76562088
--- /dev/null
+++ b/libs/sqlalchemy/dialects/postgresql/hstore.py
@@ -0,0 +1,369 @@
+# postgresql/hstore.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+import re
+
+from .base import ARRAY, ischema_names
+from ... import types as sqltypes
+from ...sql import functions as sqlfunc
+from ...sql.operators import custom_op
+from ... import util
+
+__all__ = ('HSTORE', 'hstore')
+
+# My best guess at the parsing rules of hstore literals, since no formal
+# grammar is given. This is mostly reverse engineered from PG's input parser
+# behavior.
+HSTORE_PAIR_RE = re.compile(r"""
+(
+ "(?P (\\ . | [^"])* )" # Quoted key
+)
+[ ]* => [ ]* # Pair operator, optional adjoining whitespace
+(
+ (?P NULL ) # NULL value
+ | "(?P (\\ . | [^"])* )" # Quoted value
+)
+""", re.VERBOSE)
+
+HSTORE_DELIMITER_RE = re.compile(r"""
+[ ]* , [ ]*
+""", re.VERBOSE)
+
+
+def _parse_error(hstore_str, pos):
+ """format an unmarshalling error."""
+
+ ctx = 20
+ hslen = len(hstore_str)
+
+ parsed_tail = hstore_str[max(pos - ctx - 1, 0):min(pos, hslen)]
+ residual = hstore_str[min(pos, hslen):min(pos + ctx + 1, hslen)]
+
+ if len(parsed_tail) > ctx:
+ parsed_tail = '[...]' + parsed_tail[1:]
+ if len(residual) > ctx:
+ residual = residual[:-1] + '[...]'
+
+ return "After %r, could not parse residual at position %d: %r" % (
+ parsed_tail, pos, residual)
+
+
+def _parse_hstore(hstore_str):
+ """Parse an hstore from it's literal string representation.
+
+ Attempts to approximate PG's hstore input parsing rules as closely as
+ possible. Although currently this is not strictly necessary, since the
+ current implementation of hstore's output syntax is stricter than what it
+ accepts as input, the documentation makes no guarantees that will always
+ be the case.
+
+
+
+ """
+ result = {}
+ pos = 0
+ pair_match = HSTORE_PAIR_RE.match(hstore_str)
+
+ while pair_match is not None:
+ key = pair_match.group('key').replace(r'\"', '"').replace("\\\\", "\\")
+ if pair_match.group('value_null'):
+ value = None
+ else:
+ value = pair_match.group('value').replace(r'\"', '"').replace("\\\\", "\\")
+ result[key] = value
+
+ pos += pair_match.end()
+
+ delim_match = HSTORE_DELIMITER_RE.match(hstore_str[pos:])
+ if delim_match is not None:
+ pos += delim_match.end()
+
+ pair_match = HSTORE_PAIR_RE.match(hstore_str[pos:])
+
+ if pos != len(hstore_str):
+ raise ValueError(_parse_error(hstore_str, pos))
+
+ return result
+
+
+def _serialize_hstore(val):
+ """Serialize a dictionary into an hstore literal. Keys and values must
+ both be strings (except None for values).
+
+ """
+ def esc(s, position):
+ if position == 'value' and s is None:
+ return 'NULL'
+ elif isinstance(s, util.string_types):
+ return '"%s"' % s.replace("\\", "\\\\").replace('"', r'\"')
+ else:
+ raise ValueError("%r in %s position is not a string." %
+ (s, position))
+
+ return ', '.join('%s=>%s' % (esc(k, 'key'), esc(v, 'value'))
+ for k, v in val.items())
+
+
+class HSTORE(sqltypes.Concatenable, sqltypes.TypeEngine):
+ """Represent the Postgresql HSTORE type.
+
+ The :class:`.HSTORE` type stores dictionaries containing strings, e.g.::
+
+ data_table = Table('data_table', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', HSTORE)
+ )
+
+ with engine.connect() as conn:
+ conn.execute(
+ data_table.insert(),
+ data = {"key1": "value1", "key2": "value2"}
+ )
+
+ :class:`.HSTORE` provides for a wide range of operations, including:
+
+ * Index operations::
+
+ data_table.c.data['some key'] == 'some value'
+
+ * Containment operations::
+
+ data_table.c.data.has_key('some key')
+
+ data_table.c.data.has_all(['one', 'two', 'three'])
+
+ * Concatenation::
+
+ data_table.c.data + {"k1": "v1"}
+
+ For a full list of special methods see :class:`.HSTORE.comparator_factory`.
+
+ For usage with the SQLAlchemy ORM, it may be desirable to combine
+ the usage of :class:`.HSTORE` with :class:`.MutableDict` dictionary
+ now part of the :mod:`sqlalchemy.ext.mutable`
+ extension. This extension will allow "in-place" changes to the
+ dictionary, e.g. addition of new keys or replacement/removal of existing
+ keys to/from the current dictionary, to produce events which will be detected
+ by the unit of work::
+
+ from sqlalchemy.ext.mutable import MutableDict
+
+ class MyClass(Base):
+ __tablename__ = 'data_table'
+
+ id = Column(Integer, primary_key=True)
+ data = Column(MutableDict.as_mutable(HSTORE))
+
+ my_object = session.query(MyClass).one()
+
+ # in-place mutation, requires Mutable extension
+ # in order for the ORM to detect
+ my_object.data['some_key'] = 'some value'
+
+ session.commit()
+
+ When the :mod:`sqlalchemy.ext.mutable` extension is not used, the ORM
+ will not be alerted to any changes to the contents of an existing dictionary,
+ unless that dictionary value is re-assigned to the HSTORE-attribute itself,
+ thus generating a change event.
+
+ .. versionadded:: 0.8
+
+ .. seealso::
+
+ :class:`.hstore` - render the Postgresql ``hstore()`` function.
+
+
+ """
+
+ __visit_name__ = 'HSTORE'
+
+ class comparator_factory(sqltypes.Concatenable.Comparator):
+ """Define comparison operations for :class:`.HSTORE`."""
+
+ def has_key(self, other):
+ """Boolean expression. Test for presence of a key. Note that the
+ key may be a SQLA expression.
+ """
+ return self.expr.op('?')(other)
+
+ def has_all(self, other):
+ """Boolean expression. Test for presence of all keys in the PG
+ array.
+ """
+ return self.expr.op('?&')(other)
+
+ def has_any(self, other):
+ """Boolean expression. Test for presence of any key in the PG
+ array.
+ """
+ return self.expr.op('?|')(other)
+
+ def defined(self, key):
+ """Boolean expression. Test for presence of a non-NULL value for
+ the key. Note that the key may be a SQLA expression.
+ """
+ return _HStoreDefinedFunction(self.expr, key)
+
+ def contains(self, other, **kwargs):
+ """Boolean expression. Test if keys are a superset of the keys of
+ the argument hstore expression.
+ """
+ return self.expr.op('@>')(other)
+
+ def contained_by(self, other):
+ """Boolean expression. Test if keys are a proper subset of the
+ keys of the argument hstore expression.
+ """
+ return self.expr.op('<@')(other)
+
+ def __getitem__(self, other):
+ """Text expression. Get the value at a given key. Note that the
+ key may be a SQLA expression.
+ """
+ return self.expr.op('->', precedence=5)(other)
+
+ def delete(self, key):
+ """HStore expression. Returns the contents of this hstore with the
+ given key deleted. Note that the key may be a SQLA expression.
+ """
+ if isinstance(key, dict):
+ key = _serialize_hstore(key)
+ return _HStoreDeleteFunction(self.expr, key)
+
+ def slice(self, array):
+ """HStore expression. Returns a subset of an hstore defined by
+ array of keys.
+ """
+ return _HStoreSliceFunction(self.expr, array)
+
+ def keys(self):
+ """Text array expression. Returns array of keys."""
+ return _HStoreKeysFunction(self.expr)
+
+ def vals(self):
+ """Text array expression. Returns array of values."""
+ return _HStoreValsFunction(self.expr)
+
+ def array(self):
+ """Text array expression. Returns array of alternating keys and
+ values.
+ """
+ return _HStoreArrayFunction(self.expr)
+
+ def matrix(self):
+ """Text array expression. Returns array of [key, value] pairs."""
+ return _HStoreMatrixFunction(self.expr)
+
+ def _adapt_expression(self, op, other_comparator):
+ if isinstance(op, custom_op):
+ if op.opstring in ['?', '?&', '?|', '@>', '<@']:
+ return op, sqltypes.Boolean
+ elif op.opstring == '->':
+ return op, sqltypes.Text
+ return sqltypes.Concatenable.Comparator.\
+ _adapt_expression(self, op, other_comparator)
+
+ def bind_processor(self, dialect):
+ if util.py2k:
+ encoding = dialect.encoding
+ def process(value):
+ if isinstance(value, dict):
+ return _serialize_hstore(value).encode(encoding)
+ else:
+ return value
+ else:
+ def process(value):
+ if isinstance(value, dict):
+ return _serialize_hstore(value)
+ else:
+ return value
+ return process
+
+ def result_processor(self, dialect, coltype):
+ if util.py2k:
+ encoding = dialect.encoding
+ def process(value):
+ if value is not None:
+ return _parse_hstore(value.decode(encoding))
+ else:
+ return value
+ else:
+ def process(value):
+ if value is not None:
+ return _parse_hstore(value)
+ else:
+ return value
+ return process
+
+
+ischema_names['hstore'] = HSTORE
+
+
+class hstore(sqlfunc.GenericFunction):
+ """Construct an hstore value within a SQL expression using the
+ Postgresql ``hstore()`` function.
+
+ The :class:`.hstore` function accepts one or two arguments as described
+ in the Postgresql documentation.
+
+ E.g.::
+
+ from sqlalchemy.dialects.postgresql import array, hstore
+
+ select([hstore('key1', 'value1')])
+
+ select([
+ hstore(
+ array(['key1', 'key2', 'key3']),
+ array(['value1', 'value2', 'value3'])
+ )
+ ])
+
+ .. versionadded:: 0.8
+
+ .. seealso::
+
+ :class:`.HSTORE` - the Postgresql ``HSTORE`` datatype.
+
+ """
+ type = HSTORE
+ name = 'hstore'
+
+
+class _HStoreDefinedFunction(sqlfunc.GenericFunction):
+ type = sqltypes.Boolean
+ name = 'defined'
+
+
+class _HStoreDeleteFunction(sqlfunc.GenericFunction):
+ type = HSTORE
+ name = 'delete'
+
+
+class _HStoreSliceFunction(sqlfunc.GenericFunction):
+ type = HSTORE
+ name = 'slice'
+
+
+class _HStoreKeysFunction(sqlfunc.GenericFunction):
+ type = ARRAY(sqltypes.Text)
+ name = 'akeys'
+
+
+class _HStoreValsFunction(sqlfunc.GenericFunction):
+ type = ARRAY(sqltypes.Text)
+ name = 'avals'
+
+
+class _HStoreArrayFunction(sqlfunc.GenericFunction):
+ type = ARRAY(sqltypes.Text)
+ name = 'hstore_to_array'
+
+
+class _HStoreMatrixFunction(sqlfunc.GenericFunction):
+ type = ARRAY(sqltypes.Text)
+ name = 'hstore_to_matrix'
diff --git a/libs/sqlalchemy/dialects/postgresql/json.py b/libs/sqlalchemy/dialects/postgresql/json.py
new file mode 100644
index 00000000..2e29185e
--- /dev/null
+++ b/libs/sqlalchemy/dialects/postgresql/json.py
@@ -0,0 +1,199 @@
+# postgresql/json.py
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+from __future__ import absolute_import
+
+import json
+
+from .base import ischema_names
+from ... import types as sqltypes
+from ...sql.operators import custom_op
+from ... import sql
+from ...sql import elements
+from ... import util
+
+__all__ = ('JSON', 'JSONElement')
+
+
+class JSONElement(elements.BinaryExpression):
+ """Represents accessing an element of a :class:`.JSON` value.
+
+ The :class:`.JSONElement` is produced whenever using the Python index
+ operator on an expression that has the type :class:`.JSON`::
+
+ expr = mytable.c.json_data['some_key']
+
+ The expression typically compiles to a JSON access such as ``col -> key``.
+ Modifiers are then available for typing behavior, including :meth:`.JSONElement.cast`
+ and :attr:`.JSONElement.astext`.
+
+ """
+ def __init__(self, left, right, astext=False, opstring=None, result_type=None):
+ self._astext = astext
+ if opstring is None:
+ if hasattr(right, '__iter__') and \
+ not isinstance(right, util.string_types):
+ opstring = "#>"
+ right = "{%s}" % (", ".join(util.text_type(elem) for elem in right))
+ else:
+ opstring = "->"
+
+ self._json_opstring = opstring
+ operator = custom_op(opstring, precedence=5)
+ right = left._check_literal(left, operator, right)
+ super(JSONElement, self).__init__(left, right, operator, type_=result_type)
+
+ @property
+ def astext(self):
+ """Convert this :class:`.JSONElement` to use the 'astext' operator
+ when evaluated.
+
+ E.g.::
+
+ select([data_table.c.data['some key'].astext])
+
+ .. seealso::
+
+ :meth:`.JSONElement.cast`
+
+ """
+ if self._astext:
+ return self
+ else:
+ return JSONElement(
+ self.left,
+ self.right,
+ astext=True,
+ opstring=self._json_opstring + ">",
+ result_type=sqltypes.String(convert_unicode=True)
+ )
+
+ def cast(self, type_):
+ """Convert this :class:`.JSONElement` to apply both the 'astext' operator
+ as well as an explicit type cast when evaulated.
+
+ E.g.::
+
+ select([data_table.c.data['some key'].cast(Integer)])
+
+ .. seealso::
+
+ :attr:`.JSONElement.astext`
+
+ """
+ if not self._astext:
+ return self.astext.cast(type_)
+ else:
+ return sql.cast(self, type_)
+
+
+class JSON(sqltypes.TypeEngine):
+ """Represent the Postgresql JSON type.
+
+ The :class:`.JSON` type stores arbitrary JSON format data, e.g.::
+
+ data_table = Table('data_table', metadata,
+ Column('id', Integer, primary_key=True),
+ Column('data', JSON)
+ )
+
+ with engine.connect() as conn:
+ conn.execute(
+ data_table.insert(),
+ data = {"key1": "value1", "key2": "value2"}
+ )
+
+ :class:`.JSON` provides several operations:
+
+ * Index operations::
+
+ data_table.c.data['some key']
+
+ * Index operations returning text (required for text comparison)::
+
+ data_table.c.data['some key'].astext == 'some value'
+
+ * Index operations with a built-in CAST call::
+
+ data_table.c.data['some key'].cast(Integer) == 5
+
+ * Path index operations::
+
+ data_table.c.data[('key_1', 'key_2', ..., 'key_n')]
+
+ * Path index operations returning text (required for text comparison)::
+
+ data_table.c.data[('key_1', 'key_2', ..., 'key_n')].astext == 'some value'
+
+ Index operations return an instance of :class:`.JSONElement`, which represents
+ an expression such as ``column -> index``. This element then defines
+ methods such as :attr:`.JSONElement.astext` and :meth:`.JSONElement.cast`
+ for setting up type behavior.
+
+ The :class:`.JSON` type, when used with the SQLAlchemy ORM, does not detect
+ in-place mutations to the structure. In order to detect these, the
+ :mod:`sqlalchemy.ext.mutable` extension must be used. This extension will
+ allow "in-place" changes to the datastructure to produce events which
+ will be detected by the unit of work. See the example at :class:`.HSTORE`
+ for a simple example involving a dictionary.
+
+ Custom serializers and deserializers are specified at the dialect level,
+ that is using :func:`.create_engine`. The reason for this is that when
+ using psycopg2, the DBAPI only allows serializers at the per-cursor
+ or per-connection level. E.g.::
+
+ engine = create_engine("postgresql://scott:tiger@localhost/test",
+ json_serializer=my_serialize_fn,
+ json_deserializer=my_deserialize_fn
+ )
+
+ When using the psycopg2 dialect, the json_deserializer is registered
+ against the database using ``psycopg2.extras.register_default_json``.
+
+ .. versionadded:: 0.9
+
+ """
+
+ __visit_name__ = 'JSON'
+
+ class comparator_factory(sqltypes.Concatenable.Comparator):
+ """Define comparison operations for :class:`.JSON`."""
+
+ def __getitem__(self, other):
+ """Get the value at a given key."""
+
+ return JSONElement(self.expr, other)
+
+ def _adapt_expression(self, op, other_comparator):
+ if isinstance(op, custom_op):
+ if op.opstring == '->':
+ return op, sqltypes.Text
+ return sqltypes.Concatenable.Comparator.\
+ _adapt_expression(self, op, other_comparator)
+
+ def bind_processor(self, dialect):
+ json_serializer = dialect._json_serializer or json.dumps
+ if util.py2k:
+ encoding = dialect.encoding
+ def process(value):
+ return json_serializer(value).encode(encoding)
+ else:
+ def process(value):
+ return json_serializer(value)
+ return process
+
+ def result_processor(self, dialect, coltype):
+ json_deserializer = dialect._json_deserializer or json.loads
+ if util.py2k:
+ encoding = dialect.encoding
+ def process(value):
+ return json_deserializer(value.decode(encoding))
+ else:
+ def process(value):
+ return json_deserializer(value)
+ return process
+
+
+ischema_names['json'] = JSON
diff --git a/libs/sqlalchemy/dialects/postgresql/pg8000.py b/libs/sqlalchemy/dialects/postgresql/pg8000.py
index dc72555e..bc73f975 100644
--- a/libs/sqlalchemy/dialects/postgresql/pg8000.py
+++ b/libs/sqlalchemy/dialects/postgresql/pg8000.py
@@ -1,16 +1,15 @@
# postgresql/pg8000.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the PostgreSQL database via the pg8000 driver.
-
-Connecting
-----------
-
-URLs are of the form
-``postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...]``.
+"""
+.. dialect:: postgresql+pg8000
+ :name: pg8000
+ :dbapi: pg8000
+ :connectstring: postgresql+pg8000://user:password@host:port/dbname[?key=value&key=value...]
+ :url: http://pybrary.net/pg8000/
Unicode
-------
@@ -27,19 +26,22 @@ Passing data from/to the Interval type is not supported as of
yet.
"""
-from sqlalchemy import util, exc
-from sqlalchemy.util.compat import decimal
-from sqlalchemy import processors
-from sqlalchemy import types as sqltypes
-from sqlalchemy.dialects.postgresql.base import PGDialect, \
+from ... import util, exc
+import decimal
+from ... import processors
+from ... import types as sqltypes
+from .base import PGDialect, \
PGCompiler, PGIdentifierPreparer, PGExecutionContext,\
_DECIMAL_TYPES, _FLOAT_TYPES, _INT_TYPES
+
class _PGNumeric(sqltypes.Numeric):
def result_processor(self, dialect, coltype):
if self.asdecimal:
if coltype in _FLOAT_TYPES:
- return processors.to_decimal_processor_factory(decimal.Decimal)
+ return processors.to_decimal_processor_factory(
+ decimal.Decimal,
+ self._effective_decimal_return_scale)
elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
# pg8000 returns Decimal natively for 1700
return None
@@ -61,17 +63,20 @@ class _PGNumericNoBind(_PGNumeric):
def bind_processor(self, dialect):
return None
+
class PGExecutionContext_pg8000(PGExecutionContext):
pass
class PGCompiler_pg8000(PGCompiler):
- def visit_mod(self, binary, **kw):
- return self.process(binary.left) + " %% " + self.process(binary.right)
+ def visit_mod_binary(self, binary, operator, **kw):
+ return self.process(binary.left, **kw) + " %% " + \
+ self.process(binary.right, **kw)
def post_process_text(self, text):
if '%%' in text:
- util.warn("The SQLAlchemy postgresql dialect now automatically escapes '%' in text() "
+ util.warn("The SQLAlchemy postgresql dialect "
+ "now automatically escapes '%' in text() "
"expressions to '%%'.")
return text.replace('%', '%%')
@@ -99,8 +104,8 @@ class PGDialect_pg8000(PGDialect):
colspecs = util.update_copy(
PGDialect.colspecs,
{
- sqltypes.Numeric : _PGNumericNoBind,
- sqltypes.Float : _PGNumeric
+ sqltypes.Numeric: _PGNumericNoBind,
+ sqltypes.Float: _PGNumeric
}
)
diff --git a/libs/sqlalchemy/dialects/postgresql/psycopg2.py b/libs/sqlalchemy/dialects/postgresql/psycopg2.py
index ecc8d331..e9f64f82 100644
--- a/libs/sqlalchemy/dialects/postgresql/psycopg2.py
+++ b/libs/sqlalchemy/dialects/postgresql/psycopg2.py
@@ -1,42 +1,39 @@
# postgresql/psycopg2.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the PostgreSQL database via the psycopg2 driver.
+"""
+.. dialect:: postgresql+psycopg2
+ :name: psycopg2
+ :dbapi: psycopg2
+ :connectstring: postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]
+ :url: http://pypi.python.org/pypi/psycopg2/
-Driver
-------
-
-The psycopg2 driver is available at http://pypi.python.org/pypi/psycopg2/ .
-The dialect has several behaviors which are specifically tailored towards compatibility
-with this module.
-
-Note that psycopg1 is **not** supported.
-
-Connecting
-----------
-
-URLs are of the form
-``postgresql+psycopg2://user:password@host:port/dbname[?key=value&key=value...]``.
+psycopg2 Connect Arguments
+-----------------------------------
psycopg2-specific keyword arguments which are accepted by
:func:`.create_engine()` are:
-* *server_side_cursors* - Enable the usage of "server side cursors" for SQL
+* ``server_side_cursors``: Enable the usage of "server side cursors" for SQL
statements which support this feature. What this essentially means from a
psycopg2 point of view is that the cursor is created using a name, e.g.
``connection.cursor('some name')``, which has the effect that result rows are
not immediately pre-fetched and buffered after statement execution, but are
instead left on the server and only retrieved as needed. SQLAlchemy's
- :class:`~sqlalchemy.engine.base.ResultProxy` uses special row-buffering
+ :class:`~sqlalchemy.engine.ResultProxy` uses special row-buffering
behavior when this feature is enabled, such that groups of 100 rows at a
time are fetched over the wire to reduce conversational overhead.
Note that the ``stream_results=True`` execution option is a more targeted
way of enabling this mode on a per-execution basis.
-* *use_native_unicode* - Enable the usage of Psycopg2 "native unicode" mode
- per connection. True by default.
+* ``use_native_unicode``: Enable the usage of Psycopg2 "native unicode" mode
+ per connection. True by default.
+* ``isolation_level``: This option, available for all Posgtresql dialects,
+ includes the ``AUTOCOMMIT`` isolation level when using the psycopg2
+ dialect. See :ref:`psycopg2_isolation_level`.
+
Unix Domain Connections
------------------------
@@ -66,11 +63,14 @@ The following DBAPI-specific options are respected when used with
:meth:`.Query.execution_options`, in addition to those not specific to DBAPIs:
* isolation_level - Set the transaction isolation level for the lifespan of a
- :class:`.Connection` (can only be set on a connection, not a statement or query).
- This includes the options ``SERIALIZABLE``, ``READ COMMITTED``,
- ``READ UNCOMMITTED`` and ``REPEATABLE READ``.
-* stream_results - Enable or disable usage of server side cursors.
- If ``None`` or not set, the ``server_side_cursors`` option of the :class:`.Engine` is used.
+ :class:`.Connection` (can only be set on a connection, not a statement
+ or query). See :ref:`psycopg2_isolation_level`.
+
+* stream_results - Enable or disable usage of psycopg2 server side cursors -
+ this feature makes use of "named" cursors in combination with special
+ result handling methods so that result rows are not fully buffered.
+ If ``None`` or not set, the ``server_side_cursors`` option of the
+ :class:`.Engine` is used.
Unicode
-------
@@ -98,13 +98,14 @@ on all new connections based on the value passed to
This overrides the encoding specified in the Postgresql client configuration.
.. versionadded:: 0.7.3
- The psycopg2-specific ``client_encoding`` parameter to :func:`.create_engine`.
+ The psycopg2-specific ``client_encoding`` parameter to
+ :func:`.create_engine`.
SQLAlchemy can also be instructed to skip the usage of the psycopg2
``UNICODE`` extension and to instead utilize it's own unicode encode/decode
services, which are normally reserved only for those DBAPIs that don't
-fully support unicode directly. Passing ``use_native_unicode=False``
-to :func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``.
+fully support unicode directly. Passing ``use_native_unicode=False`` to
+:func:`.create_engine` will disable usage of ``psycopg2.extensions.UNICODE``.
SQLAlchemy will instead encode data itself into Python bytestrings on the way
in and coerce from bytes on the way back,
using the value of the :func:`.create_engine` ``encoding`` parameter, which
@@ -118,16 +119,31 @@ Transactions
The psycopg2 dialect fully supports SAVEPOINT and two-phase commit operations.
-.. _psycopg2_isolation:
+.. _psycopg2_isolation_level:
-Transaction Isolation Level
----------------------------
+Psycopg2 Transaction Isolation Level
+-------------------------------------
-The ``isolation_level`` parameter of :func:`.create_engine` here makes use
+As discussed in :ref:`postgresql_isolation_level`,
+all Postgresql dialects support setting of transaction isolation level
+both via the ``isolation_level`` parameter passed to :func:`.create_engine`,
+as well as the ``isolation_level`` argument used by :meth:`.Connection.execution_options`.
+When using the psycopg2 dialect, these options make use of
psycopg2's ``set_isolation_level()`` connection method, rather than
-issuing a ``SET SESSION CHARACTERISTICS`` command. This because psycopg2
-resets the isolation level on each new transaction, and needs to know
-at the API level what level should be used.
+emitting a Postgresql directive; this is because psycopg2's API-level
+setting is always emitted at the start of each transaction in any case.
+
+The psycopg2 dialect supports these constants for isolation level:
+
+* ``READ COMMITTED``
+* ``READ UNCOMMITTED``
+* ``REPEATABLE READ``
+* ``SERIALIZABLE``
+* ``AUTOCOMMIT``
+
+.. versionadded:: 0.8.2 support for AUTOCOMMIT isolation level when using
+ psycopg2.
+
NOTICE logging
---------------
@@ -138,22 +154,32 @@ The psycopg2 dialect will log Postgresql NOTICE messages via the
import logging
logging.getLogger('sqlalchemy.dialects.postgresql').setLevel(logging.INFO)
+HSTORE type
+------------
+
+The psycopg2 dialect will make use of the
+``psycopg2.extensions.register_hstore()`` extension when using the HSTORE
+type. This replaces SQLAlchemy's pure-Python HSTORE coercion which takes
+effect for other DBAPIs.
"""
+from __future__ import absolute_import
import re
import logging
-from sqlalchemy import util, exc
-from sqlalchemy.util.compat import decimal
-from sqlalchemy import processors
-from sqlalchemy.engine import base
-from sqlalchemy.sql import expression
-from sqlalchemy import types as sqltypes
-from sqlalchemy.dialects.postgresql.base import PGDialect, PGCompiler, \
+from ... import util, exc
+import decimal
+from ... import processors
+from ...engine import result as _result
+from ...sql import expression
+from ... import types as sqltypes
+from .base import PGDialect, PGCompiler, \
PGIdentifierPreparer, PGExecutionContext, \
ENUM, ARRAY, _DECIMAL_TYPES, _FLOAT_TYPES,\
_INT_TYPES
+from .hstore import HSTORE
+from .json import JSON
logger = logging.getLogger('sqlalchemy.dialects.postgresql')
@@ -166,7 +192,9 @@ class _PGNumeric(sqltypes.Numeric):
def result_processor(self, dialect, coltype):
if self.asdecimal:
if coltype in _FLOAT_TYPES:
- return processors.to_decimal_processor_factory(decimal.Decimal)
+ return processors.to_decimal_processor_factory(
+ decimal.Decimal,
+ self._effective_decimal_return_scale)
elif coltype in _DECIMAL_TYPES or coltype in _INT_TYPES:
# pg8000 returns Decimal natively for 1700
return None
@@ -183,24 +211,37 @@ class _PGNumeric(sqltypes.Numeric):
raise exc.InvalidRequestError(
"Unknown PG numeric type: %d" % coltype)
-class _PGEnum(ENUM):
- def __init__(self, *arg, **kw):
- super(_PGEnum, self).__init__(*arg, **kw)
- # Py2K
- if self.convert_unicode:
- self.convert_unicode = "force"
- # end Py2K
-class _PGArray(ARRAY):
- def __init__(self, *arg, **kw):
- super(_PGArray, self).__init__(*arg, **kw)
- # Py2K
- # FIXME: this check won't work for setups that
- # have convert_unicode only on their create_engine().
- if isinstance(self.item_type, sqltypes.String) and \
- self.item_type.convert_unicode:
- self.item_type.convert_unicode = "force"
- # end Py2K
+class _PGEnum(ENUM):
+ def result_processor(self, dialect, coltype):
+ if util.py2k and self.convert_unicode is True:
+ # we can't easily use PG's extensions here because
+ # the OID is on the fly, and we need to give it a python
+ # function anyway - not really worth it.
+ self.convert_unicode = "force_nocheck"
+ return super(_PGEnum, self).result_processor(dialect, coltype)
+
+class _PGHStore(HSTORE):
+ def bind_processor(self, dialect):
+ if dialect._has_native_hstore:
+ return None
+ else:
+ return super(_PGHStore, self).bind_processor(dialect)
+
+ def result_processor(self, dialect, coltype):
+ if dialect._has_native_hstore:
+ return None
+ else:
+ return super(_PGHStore, self).result_processor(dialect, coltype)
+
+
+class _PGJSON(JSON):
+
+ def result_processor(self, dialect, coltype):
+ if dialect._has_native_json:
+ return None
+ else:
+ return super(_PGJSON, self).result_processor(dialect, coltype)
# When we're handed literal SQL, ensure it's a SELECT-query. Since
# 8.3, combining cursors and "FOR UPDATE" has been fine.
@@ -210,6 +251,7 @@ SERVER_SIDE_CURSOR_RE = re.compile(
_server_side_id = util.counter()
+
class PGExecutionContext_psycopg2(PGExecutionContext):
def create_cursor(self):
# TODO: coverage for server side cursors + select.for_update()
@@ -221,12 +263,13 @@ class PGExecutionContext_psycopg2(PGExecutionContext):
or \
(
(not self.compiled or
- isinstance(self.compiled.statement, expression._TextClause))
+ isinstance(self.compiled.statement, expression.TextClause))
and self.statement and SERVER_SIDE_CURSOR_RE.match(self.statement))
)
)
else:
- is_server_side = self.execution_options.get('stream_results', False)
+ is_server_side = \
+ self.execution_options.get('stream_results', False)
self.__is_server_side = is_server_side
if is_server_side:
@@ -243,9 +286,9 @@ class PGExecutionContext_psycopg2(PGExecutionContext):
self._log_notices(self.cursor)
if self.__is_server_side:
- return base.BufferedRowResultProxy(self)
+ return _result.BufferedRowResultProxy(self)
else:
- return base.ResultProxy(self)
+ return _result.ResultProxy(self)
def _log_notices(self, cursor):
for notice in cursor.connection.notices:
@@ -257,8 +300,9 @@ class PGExecutionContext_psycopg2(PGExecutionContext):
class PGCompiler_psycopg2(PGCompiler):
- def visit_mod(self, binary, **kw):
- return self.process(binary.left) + " %% " + self.process(binary.right)
+ def visit_mod_binary(self, binary, operator, **kw):
+ return self.process(binary.left, **kw) + " %% " + \
+ self.process(binary.right, **kw)
def post_process_text(self, text):
return text.replace('%', '%%')
@@ -269,11 +313,12 @@ class PGIdentifierPreparer_psycopg2(PGIdentifierPreparer):
value = value.replace(self.escape_quote, self.escape_to_quote)
return value.replace('%', '%%')
+
class PGDialect_psycopg2(PGDialect):
driver = 'psycopg2'
- # Py2K
- supports_unicode_statements = False
- # end Py2K
+ if util.py2k:
+ supports_unicode_statements = False
+
default_paramstyle = 'pyformat'
supports_sane_multi_rowcount = False
execution_ctx_cls = PGExecutionContext_psycopg2
@@ -281,21 +326,28 @@ class PGDialect_psycopg2(PGDialect):
preparer = PGIdentifierPreparer_psycopg2
psycopg2_version = (0, 0)
+ _has_native_hstore = False
+ _has_native_json = False
+
colspecs = util.update_copy(
PGDialect.colspecs,
{
- sqltypes.Numeric : _PGNumeric,
- ENUM : _PGEnum, # needs force_unicode
- sqltypes.Enum : _PGEnum, # needs force_unicode
- ARRAY : _PGArray, # needs force_unicode
+ sqltypes.Numeric: _PGNumeric,
+ ENUM: _PGEnum, # needs force_unicode
+ sqltypes.Enum: _PGEnum, # needs force_unicode
+ HSTORE: _PGHStore,
+ JSON: _PGJSON
}
)
def __init__(self, server_side_cursors=False, use_native_unicode=True,
- client_encoding=None, **kwargs):
+ client_encoding=None,
+ use_native_hstore=True,
+ **kwargs):
PGDialect.__init__(self, **kwargs)
self.server_side_cursors = server_side_cursors
self.use_native_unicode = use_native_unicode
+ self.use_native_hstore = use_native_hstore
self.supports_unicode_binds = use_native_unicode
self.client_encoding = client_encoding
if self.dbapi and hasattr(self.dbapi, '__version__'):
@@ -307,19 +359,27 @@ class PGDialect_psycopg2(PGDialect):
for x in m.group(1, 2, 3)
if x is not None)
+ def initialize(self, connection):
+ super(PGDialect_psycopg2, self).initialize(connection)
+ self._has_native_hstore = self.use_native_hstore and \
+ self._hstore_oids(connection.connection) \
+ is not None
+ self._has_native_json = self.psycopg2_version >= (2, 5)
+
@classmethod
def dbapi(cls):
- psycopg = __import__('psycopg2')
- return psycopg
+ import psycopg2
+ return psycopg2
@util.memoized_property
def _isolation_lookup(self):
- extensions = __import__('psycopg2.extensions').extensions
+ from psycopg2 import extensions
return {
- 'READ COMMITTED':extensions.ISOLATION_LEVEL_READ_COMMITTED,
- 'READ UNCOMMITTED':extensions.ISOLATION_LEVEL_READ_UNCOMMITTED,
- 'REPEATABLE READ':extensions.ISOLATION_LEVEL_REPEATABLE_READ,
- 'SERIALIZABLE':extensions.ISOLATION_LEVEL_SERIALIZABLE
+ 'AUTOCOMMIT': extensions.ISOLATION_LEVEL_AUTOCOMMIT,
+ 'READ COMMITTED': extensions.ISOLATION_LEVEL_READ_COMMITTED,
+ 'READ UNCOMMITTED': extensions.ISOLATION_LEVEL_READ_UNCOMMITTED,
+ 'REPEATABLE READ': extensions.ISOLATION_LEVEL_REPEATABLE_READ,
+ 'SERIALIZABLE': extensions.ISOLATION_LEVEL_SERIALIZABLE
}
def set_isolation_level(self, connection, level):
@@ -335,6 +395,8 @@ class PGDialect_psycopg2(PGDialect):
connection.set_isolation_level(level)
def on_connect(self):
+ from psycopg2 import extras, extensions
+
fns = []
if self.client_encoding is not None:
def on_connect(conn):
@@ -347,9 +409,28 @@ class PGDialect_psycopg2(PGDialect):
fns.append(on_connect)
if self.dbapi and self.use_native_unicode:
- extensions = __import__('psycopg2.extensions').extensions
def on_connect(conn):
extensions.register_type(extensions.UNICODE, conn)
+ extensions.register_type(extensions.UNICODEARRAY, conn)
+ fns.append(on_connect)
+
+ if self.dbapi and self.use_native_hstore:
+ def on_connect(conn):
+ hstore_oids = self._hstore_oids(conn)
+ if hstore_oids is not None:
+ oid, array_oid = hstore_oids
+ if util.py2k:
+ extras.register_hstore(conn, oid=oid,
+ array_oid=array_oid,
+ unicode=True)
+ else:
+ extras.register_hstore(conn, oid=oid,
+ array_oid=array_oid)
+ fns.append(on_connect)
+
+ if self.dbapi and self._json_deserializer:
+ def on_connect(conn):
+ extras.register_default_json(conn, loads=self._json_deserializer)
fns.append(on_connect)
if fns:
@@ -360,6 +441,15 @@ class PGDialect_psycopg2(PGDialect):
else:
return None
+ @util.memoized_instancemethod
+ def _hstore_oids(self, conn):
+ if self.psycopg2_version >= (2, 4):
+ from psycopg2 import extras
+ oids = extras.HstoreAdapter.get_oids(conn)
+ if oids is not None and oids[0]:
+ return oids[0:2]
+ return None
+
def create_connect_args(self, url):
opts = url.translate_connect_args(username='user')
if 'port' in opts:
@@ -368,24 +458,27 @@ class PGDialect_psycopg2(PGDialect):
return ([], opts)
def is_disconnect(self, e, connection, cursor):
- if isinstance(e, self.dbapi.OperationalError):
- # these error messages from libpq: interfaces/libpq/fe-misc.c.
- # TODO: these are sent through gettext in libpq and we can't
- # check within other locales - consider using connection.closed
- return 'terminating connection' in str(e) or \
- 'closed the connection' in str(e) or \
- 'connection not open' in str(e) or \
- 'could not receive data from server' in str(e)
- elif isinstance(e, self.dbapi.InterfaceError):
- # psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h
- return 'connection already closed' in str(e) or \
- 'cursor already closed' in str(e)
- elif isinstance(e, self.dbapi.ProgrammingError):
- # not sure where this path is originally from, it may
- # be obsolete. It really says "losed", not "closed".
- return "losed the connection unexpectedly" in str(e)
- else:
- return False
+ if isinstance(e, self.dbapi.Error):
+ str_e = str(e).partition("\n")[0]
+ for msg in [
+ # these error messages from libpq: interfaces/libpq/fe-misc.c
+ # and interfaces/libpq/fe-secure.c.
+ # TODO: these are sent through gettext in libpq and we can't
+ # check within other locales - consider using connection.closed
+ 'terminating connection',
+ 'closed the connection',
+ 'connection not open',
+ 'could not receive data from server',
+ # psycopg2 client errors, psycopg2/conenction.h, psycopg2/cursor.h
+ 'connection already closed',
+ 'cursor already closed',
+ # not sure where this path is originally from, it may
+ # be obsolete. It really says "losed", not "closed".
+ 'losed the connection unexpectedly'
+ ]:
+ idx = str_e.find(msg)
+ if idx >= 0 and '"' not in str_e[:idx]:
+ return True
+ return False
dialect = PGDialect_psycopg2
-
diff --git a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py b/libs/sqlalchemy/dialects/postgresql/pypostgresql.py
index 5303d047..f030d2c1 100644
--- a/libs/sqlalchemy/dialects/postgresql/pypostgresql.py
+++ b/libs/sqlalchemy/dialects/postgresql/pypostgresql.py
@@ -1,22 +1,23 @@
# postgresql/pypostgresql.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the PostgreSQL database via py-postgresql.
-
-Connecting
-----------
-
-URLs are of the form ``postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...]``.
+"""
+.. dialect:: postgresql+pypostgresql
+ :name: py-postgresql
+ :dbapi: pypostgresql
+ :connectstring: postgresql+pypostgresql://user:password@host:port/dbname[?key=value&key=value...]
+ :url: http://python.projects.pgfoundry.org/
"""
-from sqlalchemy import util
-from sqlalchemy import types as sqltypes
-from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext
-from sqlalchemy import processors
+from ... import util
+from ... import types as sqltypes
+from .base import PGDialect, PGExecutionContext
+from ... import processors
+
class PGNumeric(sqltypes.Numeric):
def bind_processor(self, dialect):
@@ -28,9 +29,11 @@ class PGNumeric(sqltypes.Numeric):
else:
return processors.to_float
+
class PGExecutionContext_pypostgresql(PGExecutionContext):
pass
+
class PGDialect_pypostgresql(PGDialect):
driver = 'pypostgresql'
@@ -48,8 +51,10 @@ class PGDialect_pypostgresql(PGDialect):
colspecs = util.update_copy(
PGDialect.colspecs,
{
- sqltypes.Numeric : PGNumeric,
- sqltypes.Float: sqltypes.Float, # prevents PGNumeric from being used
+ sqltypes.Numeric: PGNumeric,
+
+ # prevents PGNumeric from being used
+ sqltypes.Float: sqltypes.Float,
}
)
diff --git a/libs/sqlalchemy/dialects/postgresql/ranges.py b/libs/sqlalchemy/dialects/postgresql/ranges.py
new file mode 100644
index 00000000..57b0c4c3
--- /dev/null
+++ b/libs/sqlalchemy/dialects/postgresql/ranges.py
@@ -0,0 +1,160 @@
+# Copyright (C) 2013-2014 the SQLAlchemy authors and contributors
+#
+# This module is part of SQLAlchemy and is released under
+# the MIT License: http://www.opensource.org/licenses/mit-license.php
+
+from .base import ischema_names
+from ... import types as sqltypes
+
+__all__ = ('INT4RANGE', 'INT8RANGE', 'NUMRANGE')
+
+class RangeOperators(object):
+ """
+ This mixin provides functionality for the Range Operators
+ listed in Table 9-44 of the `postgres documentation`__ for Range
+ Functions and Operators. It is used by all the range types
+ provided in the ``postgres`` dialect and can likely be used for
+ any range types you create yourself.
+
+ __ http://www.postgresql.org/docs/devel/static/functions-range.html
+
+ No extra support is provided for the Range Functions listed in
+ Table 9-45 of the postgres documentation. For these, the normal
+ :func:`~sqlalchemy.sql.expression.func` object should be used.
+
+ .. versionadded:: 0.8.2 Support for Postgresql RANGE operations.
+
+ """
+
+ class comparator_factory(sqltypes.Concatenable.Comparator):
+ """Define comparison operations for range types."""
+
+ def __ne__(self, other):
+ "Boolean expression. Returns true if two ranges are not equal"
+ return self.expr.op('<>')(other)
+
+ def contains(self, other, **kw):
+ """Boolean expression. Returns true if the right hand operand,
+ which can be an element or a range, is contained within the
+ column.
+ """
+ return self.expr.op('@>')(other)
+
+ def contained_by(self, other):
+ """Boolean expression. Returns true if the column is contained
+ within the right hand operand.
+ """
+ return self.expr.op('<@')(other)
+
+ def overlaps(self, other):
+ """Boolean expression. Returns true if the column overlaps
+ (has points in common with) the right hand operand.
+ """
+ return self.expr.op('&&')(other)
+
+ def strictly_left_of(self, other):
+ """Boolean expression. Returns true if the column is strictly
+ left of the right hand operand.
+ """
+ return self.expr.op('<<')(other)
+
+ __lshift__ = strictly_left_of
+
+ def strictly_right_of(self, other):
+ """Boolean expression. Returns true if the column is strictly
+ right of the right hand operand.
+ """
+ return self.expr.op('>>')(other)
+
+ __rshift__ = strictly_right_of
+
+ def not_extend_right_of(self, other):
+ """Boolean expression. Returns true if the range in the column
+ does not extend right of the range in the operand.
+ """
+ return self.expr.op('&<')(other)
+
+ def not_extend_left_of(self, other):
+ """Boolean expression. Returns true if the range in the column
+ does not extend left of the range in the operand.
+ """
+ return self.expr.op('&>')(other)
+
+ def adjacent_to(self, other):
+ """Boolean expression. Returns true if the range in the column
+ is adjacent to the range in the operand.
+ """
+ return self.expr.op('-|-')(other)
+
+ def __add__(self, other):
+ """Range expression. Returns the union of the two ranges.
+ Will raise an exception if the resulting range is not
+ contigous.
+ """
+ return self.expr.op('+')(other)
+
+class INT4RANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql INT4RANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'INT4RANGE'
+
+ischema_names['int4range'] = INT4RANGE
+
+class INT8RANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql INT8RANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'INT8RANGE'
+
+ischema_names['int8range'] = INT8RANGE
+
+class NUMRANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql NUMRANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'NUMRANGE'
+
+ischema_names['numrange'] = NUMRANGE
+
+class DATERANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql DATERANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'DATERANGE'
+
+ischema_names['daterange'] = DATERANGE
+
+class TSRANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql TSRANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'TSRANGE'
+
+ischema_names['tsrange'] = TSRANGE
+
+class TSTZRANGE(RangeOperators, sqltypes.TypeEngine):
+ """Represent the Postgresql TSTZRANGE type.
+
+ .. versionadded:: 0.8.2
+
+ """
+
+ __visit_name__ = 'TSTZRANGE'
+
+ischema_names['tstzrange'] = TSTZRANGE
diff --git a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py b/libs/sqlalchemy/dialects/postgresql/zxjdbc.py
index 4aea9c9b..67e7d53e 100644
--- a/libs/sqlalchemy/dialects/postgresql/zxjdbc.py
+++ b/libs/sqlalchemy/dialects/postgresql/zxjdbc.py
@@ -1,19 +1,21 @@
# postgresql/zxjdbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the PostgreSQL database via the zxjdbc JDBC connector.
+"""
+.. dialect:: postgresql+zxjdbc
+ :name: zxJDBC for Jython
+ :dbapi: zxjdbc
+ :connectstring: postgresql+zxjdbc://scott:tiger@localhost/db
+ :driverurl: http://jdbc.postgresql.org/
-JDBC Driver
------------
-
-The official Postgresql JDBC driver is at http://jdbc.postgresql.org/.
"""
-from sqlalchemy.connectors.zxJDBC import ZxJDBCConnector
-from sqlalchemy.dialects.postgresql.base import PGDialect, PGExecutionContext
+from ...connectors.zxJDBC import ZxJDBCConnector
+from .base import PGDialect, PGExecutionContext
+
class PGExecutionContext_zxjdbc(PGExecutionContext):
@@ -37,6 +39,7 @@ class PGDialect_zxjdbc(ZxJDBCConnector, PGDialect):
self.DataHandler = PostgresqlDataHandler
def _get_server_version_info(self, connection):
- return tuple(int(x) for x in connection.connection.dbversion.split('.'))
+ parts = connection.connection.dbversion.split('.')
+ return tuple(int(x) for x in parts)
dialect = PGDialect_zxjdbc
diff --git a/libs/sqlalchemy/dialects/sqlite/__init__.py b/libs/sqlalchemy/dialects/sqlite/__init__.py
index c1157b63..a9b23575 100644
--- a/libs/sqlalchemy/dialects/sqlite/__init__.py
+++ b/libs/sqlalchemy/dialects/sqlite/__init__.py
@@ -1,5 +1,5 @@
# sqlite/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
@@ -15,6 +15,7 @@ from sqlalchemy.dialects.sqlite.base import \
NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR, dialect
__all__ = (
- 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT', 'INTEGER',
- 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR', 'dialect', 'REAL'
-)
\ No newline at end of file
+ 'BLOB', 'BOOLEAN', 'CHAR', 'DATE', 'DATETIME', 'DECIMAL', 'FLOAT',
+ 'INTEGER', 'NUMERIC', 'SMALLINT', 'TEXT', 'TIME', 'TIMESTAMP', 'VARCHAR',
+ 'REAL', 'dialect'
+)
diff --git a/libs/sqlalchemy/dialects/sqlite/base.py b/libs/sqlalchemy/dialects/sqlite/base.py
index 9118ace2..ac644f8d 100644
--- a/libs/sqlalchemy/dialects/sqlite/base.py
+++ b/libs/sqlalchemy/dialects/sqlite/base.py
@@ -1,25 +1,26 @@
# sqlite/base.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the SQLite database.
+"""
+.. dialect:: sqlite
+ :name: SQLite
-For information on connecting using a specific driver, see the documentation
-section regarding that driver.
Date and Time Types
-------------------
-SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite does not provide
-out of the box functionality for translating values between Python `datetime` objects
-and a SQLite-supported format. SQLAlchemy's own :class:`~sqlalchemy.types.DateTime`
-and related types provide date formatting and parsing functionality when SQlite is used.
-The implementation classes are :class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`.
-These types represent dates and times as ISO formatted strings, which also nicely
-support ordering. There's no reliance on typical "libc" internals for these functions
-so historical dates are fully supported.
+SQLite does not have built-in DATE, TIME, or DATETIME types, and pysqlite
+does not provide out of the box functionality for translating values between
+Python `datetime` objects and a SQLite-supported format. SQLAlchemy's own
+:class:`~sqlalchemy.types.DateTime` and related types provide date formatting
+and parsing functionality when SQlite is used. The implementation classes are
+:class:`~.sqlite.DATETIME`, :class:`~.sqlite.DATE` and :class:`~.sqlite.TIME`.
+These types represent dates and times as ISO formatted strings, which also
+nicely support ordering. There's no reliance on typical "libc" internals
+for these functions so historical dates are fully supported.
Auto Incrementing Behavior
--------------------------
@@ -46,44 +47,47 @@ to the Table construct::
Transaction Isolation Level
---------------------------
-:func:`.create_engine` accepts an ``isolation_level`` parameter which results in
-the command ``PRAGMA read_uncommitted `` being invoked for every new
-connection. Valid values for this parameter are ``SERIALIZABLE`` and
-``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively.
+:func:`.create_engine` accepts an ``isolation_level`` parameter which
+results in the command ``PRAGMA read_uncommitted `` being invoked for
+every new connection. Valid values for this parameter are ``SERIALIZABLE``
+and ``READ UNCOMMITTED`` corresponding to a value of 0 and 1, respectively.
See the section :ref:`pysqlite_serializable` for an important workaround
when using serializable isolation with Pysqlite.
Database Locking Behavior / Concurrency
---------------------------------------
-Note that SQLite is not designed for a high level of concurrency. The database
-itself, being a file, is locked completely during write operations and within
-transactions, meaning exactly one connection has exclusive access to the database
-during this period - all other connections will be blocked during this time.
+Note that SQLite is not designed for a high level of concurrency. The
+database itself, being a file, is locked completely during write operations
+and within transactions, meaning exactly one connection has exclusive access
+to the database during this period - all other connections will be blocked
+during this time.
The Python DBAPI specification also calls for a connection model that is always
-in a transaction; there is no BEGIN method, only commit and rollback. This implies
-that a SQLite DBAPI driver would technically allow only serialized access to a
-particular database file at all times. The pysqlite driver attempts to ameliorate this by
-deferring the actual BEGIN statement until the first DML (INSERT, UPDATE, or
-DELETE) is received within a transaction. While this breaks serializable isolation,
-it at least delays the exclusive locking inherent in SQLite's design.
+in a transaction; there is no BEGIN method, only commit and rollback. This
+implies that a SQLite DBAPI driver would technically allow only serialized
+access to a particular database file at all times. The pysqlite driver
+attempts to ameliorate this by deferring the actual BEGIN statement until
+the first DML (INSERT, UPDATE, or DELETE) is received within a
+transaction. While this breaks serializable isolation, it at least delays
+the exclusive locking inherent in SQLite's design.
SQLAlchemy's default mode of usage with the ORM is known
-as "autocommit=False", which means the moment the :class:`.Session` begins to be
-used, a transaction is begun. As the :class:`.Session` is used, the autoflush
-feature, also on by default, will flush out pending changes to the database
-before each query. The effect of this is that a :class:`.Session` used in its
-default mode will often emit DML early on, long before the transaction is actually
-committed. This again will have the effect of serializing access to the SQLite
-database. If highly concurrent reads are desired against the SQLite database,
-it is advised that the autoflush feature be disabled, and potentially even
-that autocommit be re-enabled, which has the effect of each SQL statement and
-flush committing changes immediately.
+as "autocommit=False", which means the moment the :class:`.Session` begins to
+be used, a transaction is begun. As the :class:`.Session` is used, the
+autoflush feature, also on by default, will flush out pending changes to the
+database before each query. The effect of this is that a :class:`.Session`
+used in its default mode will often emit DML early on, long before the
+transaction is actually committed. This again will have the effect of
+serializing access to the SQLite database. If highly concurrent reads are
+desired against the SQLite database, it is advised that the autoflush feature
+be disabled, and potentially even that autocommit be re-enabled, which has
+the effect of each SQL statement and flush committing changes immediately.
For more information on SQLite's lack of concurrency by design, please
-see `Situations Where Another RDBMS May Work Better - High Concurrency `_
-near the bottom of the page.
+see `Situations Where Another RDBMS May Work Better - High
+Concurrency `_ near the bottom of
+the page.
.. _sqlite_foreign_keys:
@@ -123,7 +127,8 @@ for new connections through the usage of events::
"""
-import datetime, re
+import datetime
+import re
from sqlalchemy import sql, exc
from sqlalchemy.engine import default, base, reflection
@@ -132,8 +137,10 @@ from sqlalchemy import util
from sqlalchemy.sql import compiler
from sqlalchemy import processors
-from sqlalchemy.types import BLOB, BOOLEAN, CHAR, DATE, DATETIME, DECIMAL,\
- FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT, TIME, TIMESTAMP, VARCHAR
+from sqlalchemy.types import BIGINT, BLOB, BOOLEAN, CHAR,\
+ DECIMAL, FLOAT, REAL, INTEGER, NUMERIC, SMALLINT, TEXT,\
+ TIMESTAMP, VARCHAR
+
class _DateTimeMixin(object):
_reg = None
@@ -146,15 +153,26 @@ class _DateTimeMixin(object):
if storage_format is not None:
self._storage_format = storage_format
+ def adapt(self, cls, **kw):
+ if self._storage_format:
+ kw["storage_format"] = self._storage_format
+ if self._reg:
+ kw["regexp"] = self._reg
+ return util.constructor_copy(self, cls, **kw)
+
+ def literal_processor(self, dialect):
+ bp = self.bind_processor(dialect)
+ def process(value):
+ return "'%s'" % bp(value)
+ return process
+
+
class DATETIME(_DateTimeMixin, sqltypes.DateTime):
"""Represent a Python datetime object in SQLite using a string.
The default string storage format is::
- "%04d-%02d-%02d %02d:%02d:%02d.%06d" % (value.year,
- value.month, value.day,
- value.hour, value.minute,
- value.second, value.microsecond)
+ "%(year)04d-%(month)02d-%(day)02d %(hour)02d:%(min)02d:%(second)02d.%(microsecond)06d"
e.g.::
@@ -167,37 +185,68 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
from sqlalchemy.dialects.sqlite import DATETIME
dt = DATETIME(
- storage_format="%04d/%02d/%02d %02d-%02d-%02d-%06d",
- regexp=re.compile("(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)(?:-(\d+))?")
- )
+ storage_format="%(year)04d/%(month)02d/%(day)02d %(hour)02d:%(min)02d:%(second)02d",
+ regexp=r"(\d+)/(\d+)/(\d+) (\d+)-(\d+)-(\d+)"
+ )
:param storage_format: format string which will be applied to the
- tuple ``(value.year, value.month, value.day, value.hour,
- value.minute, value.second, value.microsecond)``, given a
- Python datetime.datetime() object.
+ dict with keys year, month, day, hour, minute, second, and microsecond.
:param regexp: regular expression which will be applied to
- incoming result rows. The resulting match object is applied to
- the Python datetime() constructor via ``*map(int,
- match_obj.groups(0))``.
+ incoming result rows. If the regexp contains named groups, the
+ resulting match dict is applied to the Python datetime() constructor
+ as keyword arguments. Otherwise, if positional groups are used, the
+ the datetime() constructor is called with positional arguments via
+ ``*map(int, match_obj.groups(0))``.
"""
- _storage_format = "%04d-%02d-%02d %02d:%02d:%02d.%06d"
+ _storage_format = (
+ "%(year)04d-%(month)02d-%(day)02d "
+ "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d"
+ )
+
+ def __init__(self, *args, **kwargs):
+ truncate_microseconds = kwargs.pop('truncate_microseconds', False)
+ super(DATETIME, self).__init__(*args, **kwargs)
+ if truncate_microseconds:
+ assert 'storage_format' not in kwargs, "You can specify only "\
+ "one of truncate_microseconds or storage_format."
+ assert 'regexp' not in kwargs, "You can specify only one of "\
+ "truncate_microseconds or regexp."
+ self._storage_format = (
+ "%(year)04d-%(month)02d-%(day)02d "
+ "%(hour)02d:%(minute)02d:%(second)02d"
+ )
+
def bind_processor(self, dialect):
datetime_datetime = datetime.datetime
datetime_date = datetime.date
format = self._storage_format
+
def process(value):
if value is None:
return None
elif isinstance(value, datetime_datetime):
- return format % (value.year, value.month, value.day,
- value.hour, value.minute, value.second,
- value.microsecond)
+ return format % {
+ 'year': value.year,
+ 'month': value.month,
+ 'day': value.day,
+ 'hour': value.hour,
+ 'minute': value.minute,
+ 'second': value.second,
+ 'microsecond': value.microsecond,
+ }
elif isinstance(value, datetime_date):
- return format % (value.year, value.month, value.day,
- 0, 0, 0, 0)
+ return format % {
+ 'year': value.year,
+ 'month': value.month,
+ 'day': value.day,
+ 'hour': 0,
+ 'minute': 0,
+ 'second': 0,
+ 'microsecond': 0,
+ }
else:
raise TypeError("SQLite DateTime type only accepts Python "
"datetime and date objects as input.")
@@ -210,12 +259,13 @@ class DATETIME(_DateTimeMixin, sqltypes.DateTime):
else:
return processors.str_to_datetime
+
class DATE(_DateTimeMixin, sqltypes.Date):
"""Represent a Python date object in SQLite using a string.
The default string storage format is::
- "%04d-%02d-%02d" % (value.year, value.month, value.day)
+ "%(year)04d-%(month)02d-%(day)02d"
e.g.::
@@ -228,31 +278,36 @@ class DATE(_DateTimeMixin, sqltypes.Date):
from sqlalchemy.dialects.sqlite import DATE
d = DATE(
- storage_format="%02d/%02d/%02d",
- regexp=re.compile("(\d+)/(\d+)/(\d+)")
+ storage_format="%(month)02d/%(day)02d/%(year)04d",
+ regexp=re.compile("(?P\d+)/(?P\d+)/(?P\d+)")
)
:param storage_format: format string which will be applied to the
- tuple ``(value.year, value.month, value.day)``,
- given a Python datetime.date() object.
+ dict with keys year, month, and day.
:param regexp: regular expression which will be applied to
- incoming result rows. The resulting match object is applied to
- the Python date() constructor via ``*map(int,
- match_obj.groups(0))``.
-
+ incoming result rows. If the regexp contains named groups, the
+ resulting match dict is applied to the Python date() constructor
+ as keyword arguments. Otherwise, if positional groups are used, the
+ the date() constructor is called with positional arguments via
+ ``*map(int, match_obj.groups(0))``.
"""
- _storage_format = "%04d-%02d-%02d"
+ _storage_format = "%(year)04d-%(month)02d-%(day)02d"
def bind_processor(self, dialect):
datetime_date = datetime.date
format = self._storage_format
+
def process(value):
if value is None:
return None
elif isinstance(value, datetime_date):
- return format % (value.year, value.month, value.day)
+ return format % {
+ 'year': value.year,
+ 'month': value.month,
+ 'day': value.day,
+ }
else:
raise TypeError("SQLite Date type only accepts Python "
"date objects as input.")
@@ -265,14 +320,13 @@ class DATE(_DateTimeMixin, sqltypes.Date):
else:
return processors.str_to_date
+
class TIME(_DateTimeMixin, sqltypes.Time):
"""Represent a Python time object in SQLite using a string.
The default string storage format is::
- "%02d:%02d:%02d.%06d" % (value.hour, value.minute,
- value.second,
- value.microsecond)
+ "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d"
e.g.::
@@ -285,32 +339,47 @@ class TIME(_DateTimeMixin, sqltypes.Time):
from sqlalchemy.dialects.sqlite import TIME
t = TIME(
- storage_format="%02d-%02d-%02d-%06d",
- regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?")
- )
+ storage_format="%(hour)02d-%(minute)02d-%(second)02d-%(microsecond)06d",
+ regexp=re.compile("(\d+)-(\d+)-(\d+)-(?:-(\d+))?")
+ )
- :param storage_format: format string which will be applied
- to the tuple ``(value.hour, value.minute, value.second,
- value.microsecond)``, given a Python datetime.time() object.
+ :param storage_format: format string which will be applied to the
+ dict with keys hour, minute, second, and microsecond.
:param regexp: regular expression which will be applied to
- incoming result rows. The resulting match object is applied to
- the Python time() constructor via ``*map(int,
- match_obj.groups(0))``.
-
+ incoming result rows. If the regexp contains named groups, the
+ resulting match dict is applied to the Python time() constructor
+ as keyword arguments. Otherwise, if positional groups are used, the
+ the time() constructor is called with positional arguments via
+ ``*map(int, match_obj.groups(0))``.
"""
- _storage_format = "%02d:%02d:%02d.%06d"
+ _storage_format = "%(hour)02d:%(minute)02d:%(second)02d.%(microsecond)06d"
+
+ def __init__(self, *args, **kwargs):
+ truncate_microseconds = kwargs.pop('truncate_microseconds', False)
+ super(TIME, self).__init__(*args, **kwargs)
+ if truncate_microseconds:
+ assert 'storage_format' not in kwargs, "You can specify only "\
+ "one of truncate_microseconds or storage_format."
+ assert 'regexp' not in kwargs, "You can specify only one of "\
+ "truncate_microseconds or regexp."
+ self._storage_format = "%(hour)02d:%(minute)02d:%(second)02d"
def bind_processor(self, dialect):
datetime_time = datetime.time
format = self._storage_format
+
def process(value):
if value is None:
return None
elif isinstance(value, datetime_time):
- return format % (value.hour, value.minute, value.second,
- value.microsecond)
+ return format % {
+ 'hour': value.hour,
+ 'minute': value.minute,
+ 'second': value.second,
+ 'microsecond': value.microsecond,
+ }
else:
raise TypeError("SQLite Time type only accepts Python "
"time objects as input.")
@@ -330,6 +399,7 @@ colspecs = {
}
ischema_names = {
+ 'BIGINT': sqltypes.BIGINT,
'BLOB': sqltypes.BLOB,
'BOOL': sqltypes.BOOLEAN,
'BOOLEAN': sqltypes.BOOLEAN,
@@ -347,10 +417,11 @@ ischema_names = {
'TIME': sqltypes.TIME,
'TIMESTAMP': sqltypes.TIMESTAMP,
'VARCHAR': sqltypes.VARCHAR,
+ 'NVARCHAR': sqltypes.NVARCHAR,
+ 'NCHAR': sqltypes.NCHAR,
}
-
class SQLiteCompiler(compiler.SQLCompiler):
extract_map = util.update_copy(
compiler.SQLCompiler.extract_map,
@@ -391,7 +462,9 @@ class SQLiteCompiler(compiler.SQLCompiler):
def visit_extract(self, extract, **kw):
try:
return "CAST(STRFTIME('%s', %s) AS INTEGER)" % (
- self.extract_map[extract.field], self.process(extract.expr, **kw))
+ self.extract_map[extract.field],
+ self.process(extract.expr, **kw)
+ )
except KeyError:
raise exc.CompileError(
"%s is not a valid extract argument." % extract.field)
@@ -399,7 +472,7 @@ class SQLiteCompiler(compiler.SQLCompiler):
def limit_clause(self, select):
text = ""
if select._limit is not None:
- text += "\n LIMIT " + self.process(sql.literal(select._limit))
+ text += "\n LIMIT " + self.process(sql.literal(select._limit))
if select._offset is not None:
if select._limit is None:
text += "\n LIMIT " + self.process(sql.literal(-1))
@@ -416,7 +489,8 @@ class SQLiteCompiler(compiler.SQLCompiler):
class SQLiteDDLCompiler(compiler.DDLCompiler):
def get_column_specification(self, column, **kwargs):
- colspec = self.preparer.format_column(column) + " " + self.dialect.type_compiler.process(column.type)
+ coltype = self.dialect.type_compiler.process(column.type)
+ colspec = self.preparer.format_column(column) + " " + coltype
default = self.get_column_default_string(column)
if default is not None:
colspec += " DEFAULT " + default
@@ -424,12 +498,12 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
if not column.nullable:
colspec += " NOT NULL"
- if column.primary_key and \
- column.table.kwargs.get('sqlite_autoincrement', False) and \
- len(column.table.primary_key.columns) == 1 and \
- issubclass(column.type._type_affinity, sqltypes.Integer) and \
- not column.foreign_keys:
- colspec += " PRIMARY KEY AUTOINCREMENT"
+ if (column.primary_key and
+ column.table.kwargs.get('sqlite_autoincrement', False) and
+ len(column.table.primary_key.columns) == 1 and
+ issubclass(column.type._type_affinity, sqltypes.Integer) and
+ not column.foreign_keys):
+ colspec += " PRIMARY KEY AUTOINCREMENT"
return colspec
@@ -450,7 +524,7 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
def visit_foreign_key_constraint(self, constraint):
- local_table = constraint._elements.values()[0].parent.table
+ local_table = list(constraint._elements.values())[0].parent.table
remote_table = list(constraint._elements.values())[0].column.table
if local_table.schema != remote_table.schema:
@@ -464,23 +538,15 @@ class SQLiteDDLCompiler(compiler.DDLCompiler):
return preparer.format_table(table, use_schema=False)
def visit_create_index(self, create):
- index = create.element
- preparer = self.preparer
- text = "CREATE "
- if index.unique:
- text += "UNIQUE "
- text += "INDEX %s ON %s (%s)" \
- % (preparer.format_index(index,
- name=self._index_identifier(index.name)),
- preparer.format_table(index.table, use_schema=False),
- ', '.join(preparer.quote(c.name, c.quote)
- for c in index.columns))
- return text
+ return super(SQLiteDDLCompiler, self).\
+ visit_create_index(create, include_table_schema=False)
+
class SQLiteTypeCompiler(compiler.GenericTypeCompiler):
def visit_large_binary(self, type_):
return self.visit_BLOB(type_)
+
class SQLiteIdentifierPreparer(compiler.IdentifierPreparer):
reserved_words = set([
'add', 'after', 'all', 'alter', 'analyze', 'and', 'as', 'asc',
@@ -492,14 +558,15 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer):
'drop', 'each', 'else', 'end', 'escape', 'except', 'exclusive',
'explain', 'false', 'fail', 'for', 'foreign', 'from', 'full', 'glob',
'group', 'having', 'if', 'ignore', 'immediate', 'in', 'index',
- 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect', 'into', 'is',
- 'isnull', 'join', 'key', 'left', 'like', 'limit', 'match', 'natural',
- 'not', 'notnull', 'null', 'of', 'offset', 'on', 'or', 'order', 'outer',
- 'plan', 'pragma', 'primary', 'query', 'raise', 'references',
- 'reindex', 'rename', 'replace', 'restrict', 'right', 'rollback',
- 'row', 'select', 'set', 'table', 'temp', 'temporary', 'then', 'to',
- 'transaction', 'trigger', 'true', 'union', 'unique', 'update', 'using',
- 'vacuum', 'values', 'view', 'virtual', 'when', 'where',
+ 'indexed', 'initially', 'inner', 'insert', 'instead', 'intersect',
+ 'into', 'is', 'isnull', 'join', 'key', 'left', 'like', 'limit',
+ 'match', 'natural', 'not', 'notnull', 'null', 'of', 'offset', 'on',
+ 'or', 'order', 'outer', 'plan', 'pragma', 'primary', 'query',
+ 'raise', 'references', 'reindex', 'rename', 'replace', 'restrict',
+ 'right', 'rollback', 'row', 'select', 'set', 'table', 'temp',
+ 'temporary', 'then', 'to', 'transaction', 'trigger', 'true', 'union',
+ 'unique', 'update', 'using', 'vacuum', 'values', 'view', 'virtual',
+ 'when', 'where',
])
def format_index(self, index, use_schema=True, name=None):
@@ -508,10 +575,14 @@ class SQLiteIdentifierPreparer(compiler.IdentifierPreparer):
if name is None:
name = index.name
result = self.quote(name, index.quote)
- if not self.omit_schema and use_schema and getattr(index.table, "schema", None):
- result = self.quote_schema(index.table.schema, index.table.quote_schema) + "." + result
+ if (not self.omit_schema and
+ use_schema and
+ getattr(index.table, "schema", None)):
+ result = self.quote_schema(
+ index.table.schema, index.table.quote_schema) + "." + result
return result
+
class SQLiteExecutionContext(default.DefaultExecutionContext):
@util.memoized_property
def _preserve_raw_colnames(self):
@@ -536,6 +607,8 @@ class SQLiteDialect(default.DefaultDialect):
supports_default_values = True
supports_empty_insert = False
supports_cast = True
+ supports_multivalues_insert = True
+ supports_right_nested_joins = False
default_paramstyle = 'qmark'
execution_ctx_cls = SQLiteExecutionContext
@@ -567,6 +640,9 @@ class SQLiteDialect(default.DefaultDialect):
self.dbapi.sqlite_version_info >= (3, 3, 8)
self.supports_cast = \
self.dbapi.sqlite_version_info >= (3, 2, 3)
+ self.supports_multivalues_insert = \
+ self.dbapi.sqlite_version_info >= (3, 7, 11)
+ # http://www.sqlite.org/releaselog/3_7_11.html
# see http://www.sqlalchemy.org/trac/ticket/2568
# as well as http://www.sqlite.org/src/info/600482d161
@@ -575,9 +651,10 @@ class SQLiteDialect(default.DefaultDialect):
_isolation_lookup = {
- 'READ UNCOMMITTED':1,
- 'SERIALIZABLE':0
+ 'READ UNCOMMITTED': 1,
+ 'SERIALIZABLE': 0
}
+
def set_isolation_level(self, connection, level):
try:
isolation_level = self._isolation_lookup[level.replace('_', ' ')]
@@ -650,7 +727,8 @@ class SQLiteDialect(default.DefaultDialect):
else:
pragma = "PRAGMA "
qtable = quote(table_name)
- cursor = _pragma_cursor(connection.execute("%stable_info(%s)" % (pragma, qtable)))
+ statement = "%stable_info(%s)" % (pragma, qtable)
+ cursor = _pragma_cursor(connection.execute(statement))
row = cursor.fetchone()
# consume remaining rows, to work around
@@ -716,9 +794,8 @@ class SQLiteDialect(default.DefaultDialect):
else:
pragma = "PRAGMA "
qtable = quote(table_name)
- c = _pragma_cursor(
- connection.execute("%stable_info(%s)" %
- (pragma, qtable)))
+ statement = "%stable_info(%s)" % (pragma, qtable)
+ c = _pragma_cursor(connection.execute(statement))
rows = c.fetchall()
columns = []
@@ -752,7 +829,7 @@ class SQLiteDialect(default.DefaultDialect):
coltype = sqltypes.NullType()
if default is not None:
- default = unicode(default)
+ default = util.text_type(default)
return {
'name': name,
@@ -764,13 +841,13 @@ class SQLiteDialect(default.DefaultDialect):
}
@reflection.cache
- def get_primary_keys(self, connection, table_name, schema=None, **kw):
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
cols = self.get_columns(connection, table_name, schema, **kw)
pkeys = []
for col in cols:
if col['primary_key']:
pkeys.append(col['name'])
- return pkeys
+ return {'constrained_columns': pkeys, 'name': None}
@reflection.cache
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
@@ -830,7 +907,8 @@ class SQLiteDialect(default.DefaultDialect):
pragma = "PRAGMA "
include_auto_indexes = kw.pop('include_auto_indexes', False)
qtable = quote(table_name)
- c = _pragma_cursor(connection.execute("%sindex_list(%s)" % (pragma, qtable)))
+ statement = "%sindex_list(%s)" % (pragma, qtable)
+ c = _pragma_cursor(connection.execute(statement))
indexes = []
while True:
row = c.fetchone()
@@ -838,13 +916,15 @@ class SQLiteDialect(default.DefaultDialect):
break
# ignore implicit primary key index.
# http://www.mail-archive.com/sqlite-users@sqlite.org/msg30517.html
- elif not include_auto_indexes and row[1].startswith('sqlite_autoindex'):
+ elif (not include_auto_indexes and
+ row[1].startswith('sqlite_autoindex')):
continue
indexes.append(dict(name=row[1], column_names=[], unique=row[2]))
# loop thru unique indexes to get the column names.
for idx in indexes:
- c = connection.execute("%sindex_info(%s)" % (pragma, quote(idx['name'])))
+ statement = "%sindex_info(%s)" % (pragma, quote(idx['name']))
+ c = connection.execute(statement)
cols = idx['column_names']
while True:
row = c.fetchone()
@@ -853,6 +933,27 @@ class SQLiteDialect(default.DefaultDialect):
cols.append(row[2])
return indexes
+ @reflection.cache
+ def get_unique_constraints(self, connection, table_name,
+ schema=None, **kw):
+ UNIQUE_SQL = """
+ SELECT sql
+ FROM
+ sqlite_master
+ WHERE
+ type='table' AND
+ name=:table_name
+ """
+ c = connection.execute(UNIQUE_SQL, table_name=table_name)
+ table_data = c.fetchone()[0]
+
+ UNIQUE_PATTERN = 'CONSTRAINT (\w+) UNIQUE \(([^\)]+)\)'
+ return [
+ {'name': name,
+ 'column_names': [col.strip(' "') for col in cols.split(',')]}
+ for name, cols in re.findall(UNIQUE_PATTERN, table_data)
+ ]
+
def _pragma_cursor(cursor):
"""work around SQLite issue whereby cursor.description
diff --git a/libs/sqlalchemy/dialects/sqlite/pysqlite.py b/libs/sqlalchemy/dialects/sqlite/pysqlite.py
index 826eefd8..b53f4d4a 100644
--- a/libs/sqlalchemy/dialects/sqlite/pysqlite.py
+++ b/libs/sqlalchemy/dialects/sqlite/pysqlite.py
@@ -1,13 +1,18 @@
# sqlite/pysqlite.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for the SQLite database via pysqlite.
+"""
+.. dialect:: sqlite+pysqlite
+ :name: pysqlite
+ :dbapi: sqlite3
+ :connectstring: sqlite+pysqlite:///file_path
+ :url: http://docs.python.org/library/sqlite3.html
-Note that pysqlite is the same driver as the ``sqlite3``
-module included with the Python distribution.
+ Note that ``pysqlite`` is the same driver as the ``sqlite3``
+ module included with the Python distribution.
Driver
------
@@ -26,37 +31,36 @@ this explicitly::
from sqlite3 import dbapi2 as sqlite
e = create_engine('sqlite+pysqlite:///file.db', module=sqlite)
-Full documentation on pysqlite is available at:
-``_
Connect Strings
---------------
-The file specification for the SQLite database is taken as the "database" portion of
-the URL. Note that the format of a url is::
+The file specification for the SQLite database is taken as the "database"
+portion of the URL. Note that the format of a SQLAlchemy url is::
driver://user:pass@host/database
-This means that the actual filename to be used starts with the characters to the
-**right** of the third slash. So connecting to a relative filepath looks like::
+This means that the actual filename to be used starts with the characters to
+the **right** of the third slash. So connecting to a relative filepath
+looks like::
# relative path
e = create_engine('sqlite:///path/to/database.db')
-An absolute path, which is denoted by starting with a slash, means you need **four**
-slashes::
+An absolute path, which is denoted by starting with a slash, means you
+need **four** slashes::
# absolute path
e = create_engine('sqlite:////path/to/database.db')
-To use a Windows path, regular drive specifications and backslashes can be used.
-Double backslashes are probably needed::
+To use a Windows path, regular drive specifications and backslashes can be
+used. Double backslashes are probably needed::
# absolute path on Windows
e = create_engine('sqlite:///C:\\\\path\\\\to\\\\database.db')
-The sqlite ``:memory:`` identifier is the default if no filepath is present. Specify
-``sqlite://`` and nothing else::
+The sqlite ``:memory:`` identifier is the default if no filepath is
+present. Specify ``sqlite://`` and nothing else::
# in-memory database
e = create_engine('sqlite://')
@@ -83,22 +87,24 @@ nor should be necessary, for use with SQLAlchemy, usage of PARSE_DECLTYPES
can be forced if one configures "native_datetime=True" on create_engine()::
engine = create_engine('sqlite://',
- connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES},
- native_datetime=True
- )
+ connect_args={'detect_types': sqlite3.PARSE_DECLTYPES|sqlite3.PARSE_COLNAMES},
+ native_datetime=True
+ )
-With this flag enabled, the DATE and TIMESTAMP types (but note - not the DATETIME
-or TIME types...confused yet ?) will not perform any bind parameter or result
-processing. Execution of "func.current_date()" will return a string.
+With this flag enabled, the DATE and TIMESTAMP types (but note - not the
+DATETIME or TIME types...confused yet ?) will not perform any bind parameter
+or result processing. Execution of "func.current_date()" will return a string.
"func.current_timestamp()" is registered as returning a DATETIME type in
SQLAlchemy, so this function still receives SQLAlchemy-level result processing.
+.. _pysqlite_threading_pooling:
+
Threading/Pooling Behavior
---------------------------
Pysqlite's default behavior is to prohibit the usage of a single connection
-in more than one thread. This is originally intended to work with older versions
-of SQLite that did not support multithreaded operation under
+in more than one thread. This is originally intended to work with older
+versions of SQLite that did not support multithreaded operation under
various circumstances. In particular, older SQLite versions
did not allow a ``:memory:`` database to be used in multiple threads
under any circumstances.
@@ -114,17 +120,17 @@ thread-safety to make this usage worth it.
SQLAlchemy sets up pooling to work with Pysqlite's default behavior:
-* When a ``:memory:`` SQLite database is specified, the dialect by default will use
- :class:`.SingletonThreadPool`. This pool maintains a single connection per
- thread, so that all access to the engine within the current thread use the
- same ``:memory:`` database - other threads would access a different
- ``:memory:`` database.
-* When a file-based database is specified, the dialect will use :class:`.NullPool`
- as the source of connections. This pool closes and discards connections
- which are returned to the pool immediately. SQLite file-based connections
- have extremely low overhead, so pooling is not necessary. The scheme also
- prevents a connection from being used again in a different thread and works
- best with SQLite's coarse-grained file locking.
+* When a ``:memory:`` SQLite database is specified, the dialect by default
+ will use :class:`.SingletonThreadPool`. This pool maintains a single
+ connection per thread, so that all access to the engine within the current
+ thread use the same ``:memory:`` database - other threads would access a
+ different ``:memory:`` database.
+* When a file-based database is specified, the dialect will use
+ :class:`.NullPool` as the source of connections. This pool closes and
+ discards connections which are returned to the pool immediately. SQLite
+ file-based connections have extremely low overhead, so pooling is not
+ necessary. The scheme also prevents a connection from being used again in
+ a different thread and works best with SQLite's coarse-grained file locking.
.. versionchanged:: 0.7
Default selection of :class:`.NullPool` for SQLite file-based databases.
@@ -137,9 +143,10 @@ Using a Memory Database in Multiple Threads
To use a ``:memory:`` database in a multithreaded scenario, the same connection
object must be shared among threads, since the database exists
-only within the scope of that connection. The :class:`.StaticPool` implementation
-will maintain a single connection globally, and the ``check_same_thread`` flag
-can be passed to Pysqlite as ``False``::
+only within the scope of that connection. The
+:class:`.StaticPool` implementation will maintain a single connection
+globally, and the ``check_same_thread`` flag can be passed to Pysqlite
+as ``False``::
from sqlalchemy.pool import StaticPool
engine = create_engine('sqlite://',
@@ -152,13 +159,14 @@ version of SQLite.
Using Temporary Tables with SQLite
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-Due to the way SQLite deals with temporary tables, if you wish to use a temporary table
-in a file-based SQLite database across multiple checkouts from the connection pool, such
-as when using an ORM :class:`.Session` where the temporary table should continue to remain
-after :meth:`.commit` or :meth:`.rollback` is called,
-a pool which maintains a single connection must be used. Use :class:`.SingletonThreadPool`
-if the scope is only needed within the current thread, or :class:`.StaticPool` is scope is
-needed within multiple threads for this case::
+Due to the way SQLite deals with temporary tables, if you wish to use a
+temporary table in a file-based SQLite database across multiple checkouts
+from the connection pool, such as when using an ORM :class:`.Session` where
+the temporary table should continue to remain after :meth:`.Session.commit` or
+:meth:`.Session.rollback` is called, a pool which maintains a single connection must
+be used. Use :class:`.SingletonThreadPool` if the scope is only needed
+within the current thread, or :class:`.StaticPool` is scope is needed within
+multiple threads for this case::
# maintain the same connection per thread
from sqlalchemy.pool import SingletonThreadPool
@@ -171,17 +179,17 @@ needed within multiple threads for this case::
engine = create_engine('sqlite:///mydb.db',
poolclass=StaticPool)
-Note that :class:`.SingletonThreadPool` should be configured for the number of threads
-that are to be used; beyond that number, connections will be closed out in a non deterministic
-way.
+Note that :class:`.SingletonThreadPool` should be configured for the number
+of threads that are to be used; beyond that number, connections will be
+closed out in a non deterministic way.
Unicode
-------
-The pysqlite driver only returns Python ``unicode`` objects in result sets, never
-plain strings, and accommodates ``unicode`` objects within bound parameter
-values in all cases. Regardless of the SQLAlchemy string type in use,
-string-based result values will by Python ``unicode`` in Python 2.
+The pysqlite driver only returns Python ``unicode`` objects in result sets,
+never plain strings, and accommodates ``unicode`` objects within bound
+parameter values in all cases. Regardless of the SQLAlchemy string type in
+use, string-based result values will by Python ``unicode`` in Python 2.
The :class:`.Unicode` type should still be used to indicate those columns that
require unicode, however, so that non-``unicode`` values passed inadvertently
will emit a warning. Pysqlite will emit an error if a non-``unicode`` string
@@ -221,6 +229,7 @@ from sqlalchemy import util
import os
+
class _SQLite_pysqliteTimeStamp(DATETIME):
def bind_processor(self, dialect):
if dialect.native_datetime:
@@ -234,6 +243,7 @@ class _SQLite_pysqliteTimeStamp(DATETIME):
else:
return DATETIME.result_processor(self, dialect, coltype)
+
class _SQLite_pysqliteDate(DATE):
def bind_processor(self, dialect):
if dialect.native_datetime:
@@ -247,19 +257,20 @@ class _SQLite_pysqliteDate(DATE):
else:
return DATE.result_processor(self, dialect, coltype)
+
class SQLiteDialect_pysqlite(SQLiteDialect):
default_paramstyle = 'qmark'
colspecs = util.update_copy(
SQLiteDialect.colspecs,
{
- sqltypes.Date:_SQLite_pysqliteDate,
- sqltypes.TIMESTAMP:_SQLite_pysqliteTimeStamp,
+ sqltypes.Date: _SQLite_pysqliteDate,
+ sqltypes.TIMESTAMP: _SQLite_pysqliteTimeStamp,
}
)
- # Py3K
- #description_encoding = None
+ if not util.py2k:
+ description_encoding = None
driver = 'pysqlite'
@@ -279,9 +290,9 @@ class SQLiteDialect_pysqlite(SQLiteDialect):
def dbapi(cls):
try:
from pysqlite2 import dbapi2 as sqlite
- except ImportError, e:
+ except ImportError as e:
try:
- from sqlite3 import dbapi2 as sqlite #try the 2.5+ stdlib name.
+ from sqlite3 import dbapi2 as sqlite # try 2.5+ stdlib name.
except ImportError:
raise e
return sqlite
diff --git a/libs/sqlalchemy/dialects/sybase/__init__.py b/libs/sqlalchemy/dialects/sybase/__init__.py
index 528ebf23..85f9dd9c 100644
--- a/libs/sqlalchemy/dialects/sybase/__init__.py
+++ b/libs/sqlalchemy/dialects/sybase/__init__.py
@@ -1,26 +1,27 @@
# sybase/__init__.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
from sqlalchemy.dialects.sybase import base, pysybase, pyodbc
-
-from base import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\
- TEXT,DATE,DATETIME, FLOAT, NUMERIC,\
- BIGINT,INT, INTEGER, SMALLINT, BINARY,\
- VARBINARY,UNITEXT,UNICHAR,UNIVARCHAR,\
- IMAGE,BIT,MONEY,SMALLMONEY,TINYINT
-
# default dialect
base.dialect = pyodbc.dialect
+from .base import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\
+ TEXT, DATE, DATETIME, FLOAT, NUMERIC,\
+ BIGINT, INT, INTEGER, SMALLINT, BINARY,\
+ VARBINARY, UNITEXT, UNICHAR, UNIVARCHAR,\
+ IMAGE, BIT, MONEY, SMALLMONEY, TINYINT,\
+ dialect
+
+
__all__ = (
- 'CHAR', 'VARCHAR', 'TIME', 'NCHAR', 'NVARCHAR',
- 'TEXT','DATE','DATETIME', 'FLOAT', 'NUMERIC',
- 'BIGINT','INT', 'INTEGER', 'SMALLINT', 'BINARY',
- 'VARBINARY','UNITEXT','UNICHAR','UNIVARCHAR',
- 'IMAGE','BIT','MONEY','SMALLMONEY','TINYINT',
- 'dialect'
+ 'CHAR', 'VARCHAR', 'TIME', 'NCHAR', 'NVARCHAR',
+ 'TEXT', 'DATE', 'DATETIME', 'FLOAT', 'NUMERIC',
+ 'BIGINT', 'INT', 'INTEGER', 'SMALLINT', 'BINARY',
+ 'VARBINARY', 'UNITEXT', 'UNICHAR', 'UNIVARCHAR',
+ 'IMAGE', 'BIT', 'MONEY', 'SMALLMONEY', 'TINYINT',
+ 'dialect'
)
diff --git a/libs/sqlalchemy/dialects/sybase/base.py b/libs/sqlalchemy/dialects/sybase/base.py
index f551bff9..2f58aed9 100644
--- a/libs/sqlalchemy/dialects/sybase/base.py
+++ b/libs/sqlalchemy/dialects/sybase/base.py
@@ -1,5 +1,5 @@
# sybase/base.py
-# Copyright (C) 2010-2011 the SQLAlchemy authors and contributors
+# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors
# get_select_precolumns(), limit_clause() implementation
# copyright (C) 2007 Fisch Asset Management
# AG http://www.fam.ch, with coding by Alexander Houben
@@ -8,18 +8,21 @@
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-"""Support for Sybase Adaptive Server Enterprise (ASE).
+"""
+
+.. dialect:: sybase
+ :name: Sybase
.. note::
The Sybase dialect functions on current SQLAlchemy versions
but is not regularly tested, and may have many issues and
- caveats not currently handled. In particular, the table
- and database reflection features are not implemented.
+ caveats not currently handled.
"""
-
import operator
+import re
+
from sqlalchemy.sql import compiler, expression, text, bindparam
from sqlalchemy.engine import default, base, reflection
from sqlalchemy import types as sqltypes
@@ -28,10 +31,10 @@ from sqlalchemy import schema as sa_schema
from sqlalchemy import util, sql, exc
from sqlalchemy.types import CHAR, VARCHAR, TIME, NCHAR, NVARCHAR,\
- TEXT,DATE,DATETIME, FLOAT, NUMERIC,\
- BIGINT,INT, INTEGER, SMALLINT, BINARY,\
+ TEXT, DATE, DATETIME, FLOAT, NUMERIC,\
+ BIGINT, INT, INTEGER, SMALLINT, BINARY,\
VARBINARY, DECIMAL, TIMESTAMP, Unicode,\
- UnicodeText
+ UnicodeText, REAL
RESERVED_WORDS = set([
"add", "all", "alter", "and",
@@ -99,35 +102,44 @@ class _SybaseUnitypeMixin(object):
def result_processor(self, dialect, coltype):
def process(value):
if value is not None:
- return str(value) #.decode("ucs-2")
+ return str(value) # decode("ucs-2")
else:
return None
return process
+
class UNICHAR(_SybaseUnitypeMixin, sqltypes.Unicode):
__visit_name__ = 'UNICHAR'
+
class UNIVARCHAR(_SybaseUnitypeMixin, sqltypes.Unicode):
__visit_name__ = 'UNIVARCHAR'
+
class UNITEXT(_SybaseUnitypeMixin, sqltypes.UnicodeText):
__visit_name__ = 'UNITEXT'
+
class TINYINT(sqltypes.Integer):
__visit_name__ = 'TINYINT'
+
class BIT(sqltypes.TypeEngine):
__visit_name__ = 'BIT'
+
class MONEY(sqltypes.TypeEngine):
__visit_name__ = "MONEY"
+
class SMALLMONEY(sqltypes.TypeEngine):
__visit_name__ = "SMALLMONEY"
+
class UNIQUEIDENTIFIER(sqltypes.TypeEngine):
__visit_name__ = "UNIQUEIDENTIFIER"
+
class IMAGE(sqltypes.LargeBinary):
__visit_name__ = 'IMAGE'
@@ -170,32 +182,68 @@ class SybaseTypeCompiler(compiler.GenericTypeCompiler):
return "UNIQUEIDENTIFIER"
ischema_names = {
- 'integer' : INTEGER,
- 'unsigned int' : INTEGER, # TODO: unsigned flags
- 'unsigned smallint' : SMALLINT, # TODO: unsigned flags
- 'unsigned bigint' : BIGINT, # TODO: unsigned flags
'bigint': BIGINT,
- 'smallint' : SMALLINT,
- 'tinyint' : TINYINT,
- 'varchar' : VARCHAR,
- 'long varchar' : TEXT, # TODO
- 'char' : CHAR,
- 'decimal' : DECIMAL,
- 'numeric' : NUMERIC,
- 'float' : FLOAT,
- 'double' : NUMERIC, # TODO
- 'binary' : BINARY,
- 'varbinary' : VARBINARY,
- 'bit': BIT,
- 'image' : IMAGE,
- 'timestamp': TIMESTAMP,
+ 'int': INTEGER,
+ 'integer': INTEGER,
+ 'smallint': SMALLINT,
+ 'tinyint': TINYINT,
+ 'unsigned bigint': BIGINT, # TODO: unsigned flags
+ 'unsigned int': INTEGER, # TODO: unsigned flags
+ 'unsigned smallint': SMALLINT, # TODO: unsigned flags
+ 'numeric': NUMERIC,
+ 'decimal': DECIMAL,
+ 'dec': DECIMAL,
+ 'float': FLOAT,
+ 'double': NUMERIC, # TODO
+ 'double precision': NUMERIC, # TODO
+ 'real': REAL,
+ 'smallmoney': SMALLMONEY,
'money': MONEY,
- 'smallmoney': MONEY,
+ 'smalldatetime': DATETIME,
+ 'datetime': DATETIME,
+ 'date': DATE,
+ 'time': TIME,
+ 'char': CHAR,
+ 'character': CHAR,
+ 'varchar': VARCHAR,
+ 'character varying': VARCHAR,
+ 'char varying': VARCHAR,
+ 'unichar': UNICHAR,
+ 'unicode character': UNIVARCHAR,
+ 'nchar': NCHAR,
+ 'national char': NCHAR,
+ 'national character': NCHAR,
+ 'nvarchar': NVARCHAR,
+ 'nchar varying': NVARCHAR,
+ 'national char varying': NVARCHAR,
+ 'national character varying': NVARCHAR,
+ 'text': TEXT,
+ 'unitext': UNITEXT,
+ 'binary': BINARY,
+ 'varbinary': VARBINARY,
+ 'image': IMAGE,
+ 'bit': BIT,
+
+# not in documentation for ASE 15.7
+ 'long varchar': TEXT, # TODO
+ 'timestamp': TIMESTAMP,
'uniqueidentifier': UNIQUEIDENTIFIER,
}
+class SybaseInspector(reflection.Inspector):
+
+ def __init__(self, conn):
+ reflection.Inspector.__init__(self, conn)
+
+ def get_table_id(self, table_name, schema=None):
+ """Return the table id from `table_name` and `schema`."""
+
+ return self.dialect.get_table_id(self.bind, table_name, schema,
+ info_cache=self.info_cache)
+
+
class SybaseExecutionContext(default.DefaultExecutionContext):
_enable_identity_insert = False
@@ -243,12 +291,11 @@ class SybaseExecutionContext(default.DefaultExecutionContext):
self.root_connection.connection.connection,
True)
-
def post_exec(self):
- if self.isddl:
+ if self.isddl:
self.set_ddl_autocommit(self.root_connection, False)
- if self._enable_identity_insert:
+ if self._enable_identity_insert:
self.cursor.execute(
"SET IDENTITY_INSERT %s OFF" %
self.dialect.identifier_preparer.
@@ -262,6 +309,7 @@ class SybaseExecutionContext(default.DefaultExecutionContext):
cursor.close()
return lastrowid
+
class SybaseSQLCompiler(compiler.SQLCompiler):
ansi_bind_rules = True
@@ -288,7 +336,7 @@ class SybaseSQLCompiler(compiler.SQLCompiler):
# FIXME: sybase doesn't allow an offset without a limit
# so use a huge value for TOP here
s += "TOP 1000000 "
- s += "START AT %s " % (select._offset+1,)
+ s += "START AT %s " % (select._offset + 1,)
return s
def get_from_hint_text(self, table, text):
@@ -303,6 +351,9 @@ class SybaseSQLCompiler(compiler.SQLCompiler):
return 'DATEPART("%s", %s)' % (
field, self.process(extract.expr, **kw))
+ def visit_now_func(self, fn, **kw):
+ return "GETDATE()"
+
def for_update_clause(self, select):
# "FOR UPDATE" is only allowed on "DECLARE CURSOR"
# which SQLAlchemy doesn't use
@@ -345,29 +396,31 @@ class SybaseDDLCompiler(compiler.DDLCompiler):
# TODO: need correct syntax for this
colspec += " IDENTITY(%s,%s)" % (start, increment)
else:
+ default = self.get_column_default_string(column)
+ if default is not None:
+ colspec += " DEFAULT " + default
+
if column.nullable is not None:
if not column.nullable or column.primary_key:
colspec += " NOT NULL"
else:
colspec += " NULL"
- default = self.get_column_default_string(column)
- if default is not None:
- colspec += " DEFAULT " + default
-
return colspec
def visit_drop_index(self, drop):
index = drop.element
return "\nDROP INDEX %s.%s" % (
self.preparer.quote_identifier(index.table.name),
- self.preparer.quote(
- self._index_identifier(index.name), index.quote)
+ self._prepared_index_name(drop.element,
+ include_schema=False)
)
+
class SybaseIdentifierPreparer(compiler.IdentifierPreparer):
reserved_words = RESERVED_WORDS
+
class SybaseDialect(default.DefaultDialect):
name = 'sybase'
supports_unicode_statements = False
@@ -385,11 +438,12 @@ class SybaseDialect(default.DefaultDialect):
statement_compiler = SybaseSQLCompiler
ddl_compiler = SybaseDDLCompiler
preparer = SybaseIdentifierPreparer
+ inspector = SybaseInspector
def _get_default_schema_name(self, connection):
return connection.scalar(
text("SELECT user_name() as user_name",
- typemap={'user_name':Unicode})
+ typemap={'user_name': Unicode})
)
def initialize(self, connection):
@@ -400,39 +454,361 @@ class SybaseDialect(default.DefaultDialect):
else:
self.max_identifier_length = 255
+ def get_table_id(self, connection, table_name, schema=None, **kw):
+ """Fetch the id for schema.table_name.
+
+ Several reflection methods require the table id. The idea for using
+ this method is that it can be fetched one time and cached for
+ subsequent calls.
+
+ """
+
+ table_id = None
+ if schema is None:
+ schema = self.default_schema_name
+
+ TABLEID_SQL = text("""
+ SELECT o.id AS id
+ FROM sysobjects o JOIN sysusers u ON o.uid=u.uid
+ WHERE u.name = :schema_name
+ AND o.name = :table_name
+ AND o.type in ('U', 'V')
+ """)
+
+ if util.py2k:
+ if isinstance(schema, unicode):
+ schema = schema.encode("ascii")
+ if isinstance(table_name, unicode):
+ table_name = table_name.encode("ascii")
+ result = connection.execute(TABLEID_SQL,
+ schema_name=schema,
+ table_name=table_name)
+ table_id = result.scalar()
+ if table_id is None:
+ raise exc.NoSuchTableError(table_name)
+ return table_id
+
+ @reflection.cache
+ def get_columns(self, connection, table_name, schema=None, **kw):
+ table_id = self.get_table_id(connection, table_name, schema,
+ info_cache=kw.get("info_cache"))
+
+ COLUMN_SQL = text("""
+ SELECT col.name AS name,
+ t.name AS type,
+ (col.status & 8) AS nullable,
+ (col.status & 128) AS autoincrement,
+ com.text AS 'default',
+ col.prec AS precision,
+ col.scale AS scale,
+ col.length AS length
+ FROM systypes t, syscolumns col LEFT OUTER JOIN syscomments com ON
+ col.cdefault = com.id
+ WHERE col.usertype = t.usertype
+ AND col.id = :table_id
+ ORDER BY col.colid
+ """)
+
+ results = connection.execute(COLUMN_SQL, table_id=table_id)
+
+ columns = []
+ for (name, type_, nullable, autoincrement, default, precision, scale,
+ length) in results:
+ col_info = self._get_column_info(name, type_, bool(nullable),
+ bool(autoincrement), default, precision, scale,
+ length)
+ columns.append(col_info)
+
+ return columns
+
+ def _get_column_info(self, name, type_, nullable, autoincrement, default,
+ precision, scale, length):
+
+ coltype = self.ischema_names.get(type_, None)
+
+ kwargs = {}
+
+ if coltype in (NUMERIC, DECIMAL):
+ args = (precision, scale)
+ elif coltype == FLOAT:
+ args = (precision,)
+ elif coltype in (CHAR, VARCHAR, UNICHAR, UNIVARCHAR, NCHAR, NVARCHAR):
+ args = (length,)
+ else:
+ args = ()
+
+ if coltype:
+ coltype = coltype(*args, **kwargs)
+ #is this necessary
+ #if is_array:
+ # coltype = ARRAY(coltype)
+ else:
+ util.warn("Did not recognize type '%s' of column '%s'" %
+ (type_, name))
+ coltype = sqltypes.NULLTYPE
+
+ if default:
+ default = re.sub("DEFAULT", "", default).strip()
+ default = re.sub("^'(.*)'$", lambda m: m.group(1), default)
+ else:
+ default = None
+
+ column_info = dict(name=name, type=coltype, nullable=nullable,
+ default=default, autoincrement=autoincrement)
+ return column_info
+
+ @reflection.cache
+ def get_foreign_keys(self, connection, table_name, schema=None, **kw):
+
+ table_id = self.get_table_id(connection, table_name, schema,
+ info_cache=kw.get("info_cache"))
+
+ table_cache = {}
+ column_cache = {}
+ foreign_keys = []
+
+ table_cache[table_id] = {"name": table_name, "schema": schema}
+
+ COLUMN_SQL = text("""
+ SELECT c.colid AS id, c.name AS name
+ FROM syscolumns c
+ WHERE c.id = :table_id
+ """)
+
+ results = connection.execute(COLUMN_SQL, table_id=table_id)
+ columns = {}
+ for col in results:
+ columns[col["id"]] = col["name"]
+ column_cache[table_id] = columns
+
+ REFCONSTRAINT_SQL = text("""
+ SELECT o.name AS name, r.reftabid AS reftable_id,
+ r.keycnt AS 'count',
+ r.fokey1 AS fokey1, r.fokey2 AS fokey2, r.fokey3 AS fokey3,
+ r.fokey4 AS fokey4, r.fokey5 AS fokey5, r.fokey6 AS fokey6,
+ r.fokey7 AS fokey7, r.fokey1 AS fokey8, r.fokey9 AS fokey9,
+ r.fokey10 AS fokey10, r.fokey11 AS fokey11, r.fokey12 AS fokey12,
+ r.fokey13 AS fokey13, r.fokey14 AS fokey14, r.fokey15 AS fokey15,
+ r.fokey16 AS fokey16,
+ r.refkey1 AS refkey1, r.refkey2 AS refkey2, r.refkey3 AS refkey3,
+ r.refkey4 AS refkey4, r.refkey5 AS refkey5, r.refkey6 AS refkey6,
+ r.refkey7 AS refkey7, r.refkey1 AS refkey8, r.refkey9 AS refkey9,
+ r.refkey10 AS refkey10, r.refkey11 AS refkey11,
+ r.refkey12 AS refkey12, r.refkey13 AS refkey13,
+ r.refkey14 AS refkey14, r.refkey15 AS refkey15,
+ r.refkey16 AS refkey16
+ FROM sysreferences r JOIN sysobjects o on r.tableid = o.id
+ WHERE r.tableid = :table_id
+ """)
+ referential_constraints = connection.execute(REFCONSTRAINT_SQL,
+ table_id=table_id)
+
+ REFTABLE_SQL = text("""
+ SELECT o.name AS name, u.name AS 'schema'
+ FROM sysobjects o JOIN sysusers u ON o.uid = u.uid
+ WHERE o.id = :table_id
+ """)
+
+ for r in referential_constraints:
+ reftable_id = r["reftable_id"]
+
+ if reftable_id not in table_cache:
+ c = connection.execute(REFTABLE_SQL, table_id=reftable_id)
+ reftable = c.fetchone()
+ c.close()
+ table_info = {"name": reftable["name"], "schema": None}
+ if (schema is not None or
+ reftable["schema"] != self.default_schema_name):
+ table_info["schema"] = reftable["schema"]
+
+ table_cache[reftable_id] = table_info
+ results = connection.execute(COLUMN_SQL, table_id=reftable_id)
+ reftable_columns = {}
+ for col in results:
+ reftable_columns[col["id"]] = col["name"]
+ column_cache[reftable_id] = reftable_columns
+
+ reftable = table_cache[reftable_id]
+ reftable_columns = column_cache[reftable_id]
+
+ constrained_columns = []
+ referred_columns = []
+ for i in range(1, r["count"] + 1):
+ constrained_columns.append(columns[r["fokey%i" % i]])
+ referred_columns.append(reftable_columns[r["refkey%i" % i]])
+
+ fk_info = {
+ "constrained_columns": constrained_columns,
+ "referred_schema": reftable["schema"],
+ "referred_table": reftable["name"],
+ "referred_columns": referred_columns,
+ "name": r["name"]
+ }
+
+ foreign_keys.append(fk_info)
+
+ return foreign_keys
+
+ @reflection.cache
+ def get_indexes(self, connection, table_name, schema=None, **kw):
+ table_id = self.get_table_id(connection, table_name, schema,
+ info_cache=kw.get("info_cache"))
+
+ INDEX_SQL = text("""
+ SELECT object_name(i.id) AS table_name,
+ i.keycnt AS 'count',
+ i.name AS name,
+ (i.status & 0x2) AS 'unique',
+ index_col(object_name(i.id), i.indid, 1) AS col_1,
+ index_col(object_name(i.id), i.indid, 2) AS col_2,
+ index_col(object_name(i.id), i.indid, 3) AS col_3,
+ index_col(object_name(i.id), i.indid, 4) AS col_4,
+ index_col(object_name(i.id), i.indid, 5) AS col_5,
+ index_col(object_name(i.id), i.indid, 6) AS col_6,
+ index_col(object_name(i.id), i.indid, 7) AS col_7,
+ index_col(object_name(i.id), i.indid, 8) AS col_8,
+ index_col(object_name(i.id), i.indid, 9) AS col_9,
+ index_col(object_name(i.id), i.indid, 10) AS col_10,
+ index_col(object_name(i.id), i.indid, 11) AS col_11,
+ index_col(object_name(i.id), i.indid, 12) AS col_12,
+ index_col(object_name(i.id), i.indid, 13) AS col_13,
+ index_col(object_name(i.id), i.indid, 14) AS col_14,
+ index_col(object_name(i.id), i.indid, 15) AS col_15,
+ index_col(object_name(i.id), i.indid, 16) AS col_16
+ FROM sysindexes i, sysobjects o
+ WHERE o.id = i.id
+ AND o.id = :table_id
+ AND (i.status & 2048) = 0
+ AND i.indid BETWEEN 1 AND 254
+ """)
+
+ results = connection.execute(INDEX_SQL, table_id=table_id)
+ indexes = []
+ for r in results:
+ column_names = []
+ for i in range(1, r["count"]):
+ column_names.append(r["col_%i" % (i,)])
+ index_info = {"name": r["name"],
+ "unique": bool(r["unique"]),
+ "column_names": column_names}
+ indexes.append(index_info)
+
+ return indexes
+
+ @reflection.cache
+ def get_pk_constraint(self, connection, table_name, schema=None, **kw):
+ table_id = self.get_table_id(connection, table_name, schema,
+ info_cache=kw.get("info_cache"))
+
+ PK_SQL = text("""
+ SELECT object_name(i.id) AS table_name,
+ i.keycnt AS 'count',
+ i.name AS name,
+ index_col(object_name(i.id), i.indid, 1) AS pk_1,
+ index_col(object_name(i.id), i.indid, 2) AS pk_2,
+ index_col(object_name(i.id), i.indid, 3) AS pk_3,
+ index_col(object_name(i.id), i.indid, 4) AS pk_4,
+ index_col(object_name(i.id), i.indid, 5) AS pk_5,
+ index_col(object_name(i.id), i.indid, 6) AS pk_6,
+ index_col(object_name(i.id), i.indid, 7) AS pk_7,
+ index_col(object_name(i.id), i.indid, 8) AS pk_8,
+ index_col(object_name(i.id), i.indid, 9) AS pk_9,
+ index_col(object_name(i.id), i.indid, 10) AS pk_10,
+ index_col(object_name(i.id), i.indid, 11) AS pk_11,
+ index_col(object_name(i.id), i.indid, 12) AS pk_12,
+ index_col(object_name(i.id), i.indid, 13) AS pk_13,
+ index_col(object_name(i.id), i.indid, 14) AS pk_14,
+ index_col(object_name(i.id), i.indid, 15) AS pk_15,
+ index_col(object_name(i.id), i.indid, 16) AS pk_16
+ FROM sysindexes i, sysobjects o
+ WHERE o.id = i.id
+ AND o.id = :table_id
+ AND (i.status & 2048) = 2048
+ AND i.indid BETWEEN 1 AND 254
+ """)
+
+ results = connection.execute(PK_SQL, table_id=table_id)
+ pks = results.fetchone()
+ results.close()
+
+ constrained_columns = []
+ for i in range(1, pks["count"] + 1):
+ constrained_columns.append(pks["pk_%i" % (i,)])
+ return {"constrained_columns": constrained_columns,
+ "name": pks["name"]}
+
+ @reflection.cache
+ def get_schema_names(self, connection, **kw):
+
+ SCHEMA_SQL = text("SELECT u.name AS name FROM sysusers u")
+
+ schemas = connection.execute(SCHEMA_SQL)
+
+ return [s["name"] for s in schemas]
+
@reflection.cache
def get_table_names(self, connection, schema=None, **kw):
if schema is None:
schema = self.default_schema_name
- result = connection.execute(
- text("select sysobjects.name from sysobjects, sysusers "
- "where sysobjects.uid=sysusers.uid and "
- "sysusers.name=:schemaname and "
- "sysobjects.type='U'",
- bindparams=[
- bindparam('schemaname', schema)
- ])
- )
- return [r[0] for r in result]
+ TABLE_SQL = text("""
+ SELECT o.name AS name
+ FROM sysobjects o JOIN sysusers u ON o.uid = u.uid
+ WHERE u.name = :schema_name
+ AND o.type = 'U'
+ """)
- def has_table(self, connection, tablename, schema=None):
+ if util.py2k:
+ if isinstance(schema, unicode):
+ schema = schema.encode("ascii")
+
+ tables = connection.execute(TABLE_SQL, schema_name=schema)
+
+ return [t["name"] for t in tables]
+
+ @reflection.cache
+ def get_view_definition(self, connection, view_name, schema=None, **kw):
if schema is None:
schema = self.default_schema_name
- result = connection.execute(
- text("select sysobjects.name from sysobjects, sysusers "
- "where sysobjects.uid=sysusers.uid and "
- "sysobjects.name=:tablename and "
- "sysusers.name=:schemaname and "
- "sysobjects.type='U'",
- bindparams=[
- bindparam('tablename', tablename),
- bindparam('schemaname', schema)
- ])
- )
- return result.scalar() is not None
+ VIEW_DEF_SQL = text("""
+ SELECT c.text
+ FROM syscomments c JOIN sysobjects o ON c.id = o.id
+ WHERE o.name = :view_name
+ AND o.type = 'V'
+ """)
- def reflecttable(self, connection, table, include_columns):
- raise NotImplementedError()
+ if util.py2k:
+ if isinstance(view_name, unicode):
+ view_name = view_name.encode("ascii")
+ view = connection.execute(VIEW_DEF_SQL, view_name=view_name)
+
+ return view.scalar()
+
+ @reflection.cache
+ def get_view_names(self, connection, schema=None, **kw):
+ if schema is None:
+ schema = self.default_schema_name
+
+ VIEW_SQL = text("""
+ SELECT o.name AS name
+ FROM sysobjects o JOIN sysusers u ON o.uid = u.uid
+ WHERE u.name = :schema_name
+ AND o.type = 'V'
+ """)
+
+ if util.py2k:
+ if isinstance(schema, unicode):
+ schema = schema.encode("ascii")
+ views = connection.execute(VIEW_SQL, schema_name=schema)
+
+ return [v["name"] for v in views]
+
+ def has_table(self, connection, table_name, schema=None):
+ try:
+ self.get_table_id(connection, table_name, schema)
+ except exc.NoSuchTableError:
+ return False
+ else:
+ return True
diff --git a/libs/sqlalchemy/dialects/sybase/mxodbc.py b/libs/sqlalchemy/dialects/sybase/mxodbc.py
index db60b9b2..f14d1c42 100644
--- a/libs/sqlalchemy/dialects/sybase/mxodbc.py
+++ b/libs/sqlalchemy/dialects/sybase/mxodbc.py
@@ -1,22 +1,31 @@
# sybase/mxodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
-
"""
-Support for Sybase via mxodbc.
-This dialect is a stub only and is likely non functional at this time.
+.. dialect:: sybase+mxodbc
+ :name: mxODBC
+ :dbapi: mxodbc
+ :connectstring: sybase+mxodbc://:@
+ :url: http://www.egenix.com/
+
+.. note::
+
+ This dialect is a stub only and is likely non functional at this time.
"""
-from sqlalchemy.dialects.sybase.base import SybaseDialect, SybaseExecutionContext
+from sqlalchemy.dialects.sybase.base import SybaseDialect
+from sqlalchemy.dialects.sybase.base import SybaseExecutionContext
from sqlalchemy.connectors.mxodbc import MxODBCConnector
+
class SybaseExecutionContext_mxodbc(SybaseExecutionContext):
pass
+
class SybaseDialect_mxodbc(MxODBCConnector, SybaseDialect):
execution_ctx_cls = SybaseExecutionContext_mxodbc
diff --git a/libs/sqlalchemy/dialects/sybase/pyodbc.py b/libs/sqlalchemy/dialects/sybase/pyodbc.py
index 8e3729b3..f773e5a6 100644
--- a/libs/sqlalchemy/dialects/sybase/pyodbc.py
+++ b/libs/sqlalchemy/dialects/sybase/pyodbc.py
@@ -1,18 +1,16 @@
# sybase/pyodbc.py
-# Copyright (C) 2005-2013 the SQLAlchemy authors and contributors
+# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for Sybase via pyodbc.
+.. dialect:: sybase+pyodbc
+ :name: PyODBC
+ :dbapi: pyodbc
+ :connectstring: sybase+pyodbc://:@[/]
+ :url: http://pypi.python.org/pypi/pyodbc/
-http://pypi.python.org/pypi/pyodbc/
-
-Connect strings are of the form::
-
- sybase+pyodbc://:@/
- sybase+pyodbc://:@/
Unicode Support
---------------
@@ -37,8 +35,9 @@ Currently *not* supported are::
from sqlalchemy.dialects.sybase.base import SybaseDialect,\
SybaseExecutionContext
from sqlalchemy.connectors.pyodbc import PyODBCConnector
-from sqlalchemy import types as sqltypes, util, processors
-from sqlalchemy.util.compat import decimal
+from sqlalchemy import types as sqltypes, processors
+import decimal
+
class _SybNumeric_pyodbc(sqltypes.Numeric):
"""Turns Decimals with adjusted() < -6 into floats.
@@ -50,7 +49,7 @@ class _SybNumeric_pyodbc(sqltypes.Numeric):
"""
def bind_processor(self, dialect):
- super_process = super(_SybNumeric_pyodbc,self).\
+ super_process = super(_SybNumeric_pyodbc, self).\
bind_processor(dialect)
def process(value):
@@ -66,6 +65,7 @@ class _SybNumeric_pyodbc(sqltypes.Numeric):
return value
return process
+
class SybaseExecutionContext_pyodbc(SybaseExecutionContext):
def set_ddl_autocommit(self, connection, value):
if value:
@@ -73,11 +73,12 @@ class SybaseExecutionContext_pyodbc(SybaseExecutionContext):
else:
connection.autocommit = False
+
class SybaseDialect_pyodbc(PyODBCConnector, SybaseDialect):
execution_ctx_cls = SybaseExecutionContext_pyodbc
colspecs = {
- sqltypes.Numeric:_SybNumeric_pyodbc,
+ sqltypes.Numeric: _SybNumeric_pyodbc,
}
dialect = SybaseDialect_pyodbc
diff --git a/libs/sqlalchemy/dialects/sybase/pysybase.py b/libs/sqlalchemy/dialects/sybase/pysybase.py
index bf8c2096..664bd9ac 100644
--- a/libs/sqlalchemy/dialects/sybase/pysybase.py
+++ b/libs/sqlalchemy/dialects/sybase/pysybase.py
@@ -1,17 +1,15 @@
# sybase/pysybase.py
-# Copyright (C) 2010-2011 the SQLAlchemy authors and contributors
+# Copyright (C) 2010-2014 the SQLAlchemy authors and contributors
#
# This module is part of SQLAlchemy and is released under
# the MIT License: http://www.opensource.org/licenses/mit-license.php
"""
-Support for Sybase via the python-sybase driver.
-
-http://python-sybase.sourceforge.net/
-
-Connect strings are of the form::
-
- sybase+pysybase://