зеркало из https://github.com/mozilla/PyHive.git
Add SQLAlchemy's tests and fill in some features
This commit is contained in:
Родитель
d770514f85
Коммит
ce75d3e23f
|
@ -3,5 +3,5 @@ pytest
|
||||||
pytest-cov
|
pytest-cov
|
||||||
requests>=1.0.0
|
requests>=1.0.0
|
||||||
sasl>=0.1.3
|
sasl>=0.1.3
|
||||||
sqlalchemy>=0.5.0
|
sqlalchemy>=0.9.4
|
||||||
thrift>=0.8.0
|
thrift>=0.8.0
|
||||||
|
|
|
@ -123,6 +123,9 @@ class Connection(object):
|
||||||
def sessionHandle(self):
|
def sessionHandle(self):
|
||||||
return self._sessionHandle
|
return self._sessionHandle
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
raise NotSupportedError("Hive does not have transactions")
|
||||||
|
|
||||||
|
|
||||||
class Cursor(common.DBAPICursor):
|
class Cursor(common.DBAPICursor):
|
||||||
"""These objects represent a database cursor, which is used to manage the context of a fetch
|
"""These objects represent a database cursor, which is used to manage the context of a fetch
|
||||||
|
|
|
@ -58,6 +58,9 @@ class Connection(object):
|
||||||
"""Return a new :py:class:`Cursor` object using the connection."""
|
"""Return a new :py:class:`Cursor` object using the connection."""
|
||||||
return Cursor(*self._args, **self._kwargs)
|
return Cursor(*self._args, **self._kwargs)
|
||||||
|
|
||||||
|
def rollback(self):
|
||||||
|
raise NotSupportedError("Presto does not have transactions")
|
||||||
|
|
||||||
|
|
||||||
class Cursor(common.DBAPICursor):
|
class Cursor(common.DBAPICursor):
|
||||||
"""These objects represent a database cursor, which is used to manage the context of a fetch
|
"""These objects represent a database cursor, which is used to manage the context of a fetch
|
||||||
|
|
|
@ -0,0 +1,631 @@
|
||||||
|
# Taken from SQLAlchemy with lots of stuff stripped out.
|
||||||
|
|
||||||
|
# sqlalchemy/processors.py
|
||||||
|
# Copyright (C) 2010-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||||
|
# Copyright (C) 2010 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
|
||||||
|
|
||||||
|
"""defines generic type conversion functions, as used in bind and result
|
||||||
|
processors.
|
||||||
|
|
||||||
|
They all share one common characteristic: None is passed through unchanged.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import datetime
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def str_to_datetime_processor_factory(regexp, type_):
|
||||||
|
rmatch = regexp.match
|
||||||
|
# Even on python2.6 datetime.strptime is both slower than this code
|
||||||
|
# and it does not support microseconds.
|
||||||
|
has_named_groups = bool(regexp.groupindex)
|
||||||
|
|
||||||
|
def process(value):
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
m = rmatch(value)
|
||||||
|
except TypeError:
|
||||||
|
raise ValueError("Couldn't parse %s string '%r' "
|
||||||
|
"- value is not a string." %
|
||||||
|
(type_.__name__, value))
|
||||||
|
if m is None:
|
||||||
|
raise ValueError("Couldn't parse %s string: "
|
||||||
|
"'%s'" % (type_.__name__, value))
|
||||||
|
if has_named_groups:
|
||||||
|
groups = m.groupdict(0)
|
||||||
|
return type_(**dict(zip(groups.iterkeys(),
|
||||||
|
map(int, groups.itervalues()))))
|
||||||
|
else:
|
||||||
|
return type_(*map(int, m.groups(0)))
|
||||||
|
return process
|
||||||
|
|
||||||
|
|
||||||
|
DATETIME_RE = re.compile("(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)(?:\.(\d+))?")
|
||||||
|
|
||||||
|
str_to_datetime = str_to_datetime_processor_factory(DATETIME_RE,
|
||||||
|
datetime.datetime)
|
||||||
|
|
||||||
|
# engine/reflection.py
|
||||||
|
# Copyright (C) 2005-2014 the SQLAlchemy authors and contributors <see AUTHORS file>
|
||||||
|
#
|
||||||
|
# This module is part of SQLAlchemy and is released under
|
||||||
|
# the MIT License: http://www.opensource.org/licenses/mit-license.php
|
||||||
|
|
||||||
|
"""Provides an abstraction for obtaining database schema information.
|
||||||
|
|
||||||
|
Usage Notes:
|
||||||
|
|
||||||
|
Here are some general conventions when accessing the low level inspector
|
||||||
|
methods such as get_table_names, get_columns, etc.
|
||||||
|
|
||||||
|
1. Inspector methods return lists of dicts in most cases for the following
|
||||||
|
reasons:
|
||||||
|
|
||||||
|
* They're both standard types that can be serialized.
|
||||||
|
* Using a dict instead of a tuple allows easy expansion of attributes.
|
||||||
|
* Using a list for the outer structure maintains order and is easy to work
|
||||||
|
with (e.g. list comprehension [d['name'] for d in cols]).
|
||||||
|
|
||||||
|
2. Records that contain a name, such as the column name in a column record
|
||||||
|
use the key 'name'. So for most return values, each record will have a
|
||||||
|
'name' attribute..
|
||||||
|
"""
|
||||||
|
|
||||||
|
from sqlalchemy import exc, sql
|
||||||
|
from sqlalchemy import schema as sa_schema
|
||||||
|
from sqlalchemy import util
|
||||||
|
from sqlalchemy.types import TypeEngine
|
||||||
|
from sqlalchemy.util import deprecated
|
||||||
|
|
||||||
|
|
||||||
|
@util.decorator
|
||||||
|
def cache(fn, self, con, *args, **kw):
|
||||||
|
info_cache = kw.get('info_cache', None)
|
||||||
|
if info_cache is None:
|
||||||
|
return fn(self, con, *args, **kw)
|
||||||
|
key = (
|
||||||
|
fn.__name__,
|
||||||
|
tuple(a for a in args if isinstance(a, util.string_types)),
|
||||||
|
tuple((k, v) for k, v in kw.items() if
|
||||||
|
isinstance(v,
|
||||||
|
util.string_types + util.int_types + (float, )
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
ret = info_cache.get(key)
|
||||||
|
if ret is None:
|
||||||
|
ret = fn(self, con, *args, **kw)
|
||||||
|
info_cache[key] = ret
|
||||||
|
return ret
|
||||||
|
|
||||||
|
|
||||||
|
class Inspector(object):
|
||||||
|
"""Performs database schema inspection.
|
||||||
|
|
||||||
|
The Inspector acts as a proxy to the reflection methods of the
|
||||||
|
:class:`~sqlalchemy.engine.interfaces.Dialect`, providing a
|
||||||
|
consistent interface as well as caching support for previously
|
||||||
|
fetched metadata.
|
||||||
|
|
||||||
|
A :class:`.Inspector` object is usually created via the
|
||||||
|
:func:`.inspect` function::
|
||||||
|
|
||||||
|
from sqlalchemy import inspect, create_engine
|
||||||
|
engine = create_engine('...')
|
||||||
|
insp = inspect(engine)
|
||||||
|
|
||||||
|
The inspection method above is equivalent to using the
|
||||||
|
:meth:`.Inspector.from_engine` method, i.e.::
|
||||||
|
|
||||||
|
engine = create_engine('...')
|
||||||
|
insp = Inspector.from_engine(engine)
|
||||||
|
|
||||||
|
Where above, the :class:`~sqlalchemy.engine.interfaces.Dialect` may opt
|
||||||
|
to return an :class:`.Inspector` subclass that provides additional
|
||||||
|
methods specific to the dialect's target database.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, bind):
|
||||||
|
"""Initialize a new :class:`.Inspector`.
|
||||||
|
|
||||||
|
:param bind: a :class:`~sqlalchemy.engine.Connectable`,
|
||||||
|
which is typically an instance of
|
||||||
|
:class:`~sqlalchemy.engine.Engine` or
|
||||||
|
:class:`~sqlalchemy.engine.Connection`.
|
||||||
|
|
||||||
|
For a dialect-specific instance of :class:`.Inspector`, see
|
||||||
|
:meth:`.Inspector.from_engine`
|
||||||
|
|
||||||
|
"""
|
||||||
|
# this might not be a connection, it could be an engine.
|
||||||
|
self.bind = bind
|
||||||
|
|
||||||
|
# set the engine
|
||||||
|
if hasattr(bind, 'engine'):
|
||||||
|
self.engine = bind.engine
|
||||||
|
else:
|
||||||
|
self.engine = bind
|
||||||
|
|
||||||
|
if self.engine is bind:
|
||||||
|
# if engine, ensure initialized
|
||||||
|
bind.connect().close()
|
||||||
|
|
||||||
|
self.dialect = self.engine.dialect
|
||||||
|
self.info_cache = {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_engine(cls, bind):
|
||||||
|
"""Construct a new dialect-specific Inspector object from the given
|
||||||
|
engine or connection.
|
||||||
|
|
||||||
|
:param bind: a :class:`~sqlalchemy.engine.Connectable`,
|
||||||
|
which is typically an instance of
|
||||||
|
:class:`~sqlalchemy.engine.Engine` or
|
||||||
|
:class:`~sqlalchemy.engine.Connection`.
|
||||||
|
|
||||||
|
This method differs from direct a direct constructor call of
|
||||||
|
:class:`.Inspector` in that the
|
||||||
|
:class:`~sqlalchemy.engine.interfaces.Dialect` is given a chance to
|
||||||
|
provide a dialect-specific :class:`.Inspector` instance, which may
|
||||||
|
provide additional methods.
|
||||||
|
|
||||||
|
See the example at :class:`.Inspector`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if hasattr(bind.dialect, 'inspector'):
|
||||||
|
return bind.dialect.inspector(bind)
|
||||||
|
return Inspector(bind)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def default_schema_name(self):
|
||||||
|
"""Return the default schema name presented by the dialect
|
||||||
|
for the current engine's database user.
|
||||||
|
|
||||||
|
E.g. this is typically ``public`` for Postgresql and ``dbo``
|
||||||
|
for SQL Server.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.dialect.default_schema_name
|
||||||
|
|
||||||
|
def get_schema_names(self):
|
||||||
|
"""Return all schema names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if hasattr(self.dialect, 'get_schema_names'):
|
||||||
|
return self.dialect.get_schema_names(self.bind,
|
||||||
|
info_cache=self.info_cache)
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_table_names(self, schema=None, order_by=None):
|
||||||
|
"""Return all table names in referred to within a particular schema.
|
||||||
|
|
||||||
|
The names are expected to be real tables only, not views.
|
||||||
|
Views are instead returned using the :meth:`.Inspector.get_view_names`
|
||||||
|
method.
|
||||||
|
|
||||||
|
|
||||||
|
:param schema: Schema name. If ``schema`` is left at ``None``, the
|
||||||
|
database's default schema is
|
||||||
|
used, else the named schema is searched. If the database does not
|
||||||
|
support named schemas, behavior is undefined if ``schema`` is not
|
||||||
|
passed as ``None``. For special quoting, use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param order_by: Optional, may be the string "foreign_key" to sort
|
||||||
|
the result on foreign key dependencies.
|
||||||
|
|
||||||
|
.. versionchanged:: 0.8 the "foreign_key" sorting sorts tables
|
||||||
|
in order of dependee to dependent; that is, in creation
|
||||||
|
order, rather than in drop order. This is to maintain
|
||||||
|
consistency with similar features such as
|
||||||
|
:attr:`.MetaData.sorted_tables` and :func:`.util.sort_tables`.
|
||||||
|
|
||||||
|
.. seealso::
|
||||||
|
|
||||||
|
:attr:`.MetaData.sorted_tables`
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
if hasattr(self.dialect, 'get_table_names'):
|
||||||
|
tnames = self.dialect.get_table_names(self.bind,
|
||||||
|
schema, info_cache=self.info_cache)
|
||||||
|
else:
|
||||||
|
tnames = self.engine.table_names(schema)
|
||||||
|
if order_by == 'foreign_key':
|
||||||
|
raise NotImplementedError("taken out during backport")
|
||||||
|
return tnames
|
||||||
|
|
||||||
|
def get_table_options(self, table_name, schema=None, **kw):
|
||||||
|
"""Return a dictionary of options specified when the table of the
|
||||||
|
given name was created.
|
||||||
|
|
||||||
|
This currently includes some options that apply to MySQL tables.
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if hasattr(self.dialect, 'get_table_options'):
|
||||||
|
return self.dialect.get_table_options(
|
||||||
|
self.bind, table_name, schema,
|
||||||
|
info_cache=self.info_cache, **kw)
|
||||||
|
return {}
|
||||||
|
|
||||||
|
def get_view_names(self, schema=None):
|
||||||
|
"""Return all view names in `schema`.
|
||||||
|
|
||||||
|
:param schema: Optional, retrieve names from a non-default schema.
|
||||||
|
For special quoting, use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_view_names(self.bind, schema,
|
||||||
|
info_cache=self.info_cache)
|
||||||
|
|
||||||
|
def get_view_definition(self, view_name, schema=None):
|
||||||
|
"""Return definition for `view_name`.
|
||||||
|
|
||||||
|
:param schema: Optional, retrieve names from a non-default schema.
|
||||||
|
For special quoting, use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_view_definition(
|
||||||
|
self.bind, view_name, schema, info_cache=self.info_cache)
|
||||||
|
|
||||||
|
def get_columns(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about columns in `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name` and an optional string `schema`, return
|
||||||
|
column information as a list of dicts with these keys:
|
||||||
|
|
||||||
|
name
|
||||||
|
the column's name
|
||||||
|
|
||||||
|
type
|
||||||
|
:class:`~sqlalchemy.types.TypeEngine`
|
||||||
|
|
||||||
|
nullable
|
||||||
|
boolean
|
||||||
|
|
||||||
|
default
|
||||||
|
the column's default value
|
||||||
|
|
||||||
|
attrs
|
||||||
|
dict containing optional column attributes
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
col_defs = self.dialect.get_columns(self.bind, table_name, schema,
|
||||||
|
info_cache=self.info_cache,
|
||||||
|
**kw)
|
||||||
|
for col_def in col_defs:
|
||||||
|
# make this easy and only return instances for coltype
|
||||||
|
coltype = col_def['type']
|
||||||
|
if not isinstance(coltype, TypeEngine):
|
||||||
|
col_def['type'] = coltype()
|
||||||
|
return col_defs
|
||||||
|
|
||||||
|
@deprecated('0.7', 'Call to deprecated method get_primary_keys.'
|
||||||
|
' Use get_pk_constraint instead.')
|
||||||
|
def get_primary_keys(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about primary keys in `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name`, and an optional string `schema`, return
|
||||||
|
primary key information as a list of column names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_pk_constraint(self.bind, table_name, schema,
|
||||||
|
info_cache=self.info_cache,
|
||||||
|
**kw)['constrained_columns']
|
||||||
|
|
||||||
|
def get_pk_constraint(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about primary key constraint on `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name`, and an optional string `schema`, return
|
||||||
|
primary key information as a dictionary with these keys:
|
||||||
|
|
||||||
|
constrained_columns
|
||||||
|
a list of column names that make up the primary key
|
||||||
|
|
||||||
|
name
|
||||||
|
optional name of the primary key constraint.
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.dialect.get_pk_constraint(self.bind, table_name, schema,
|
||||||
|
info_cache=self.info_cache,
|
||||||
|
**kw)
|
||||||
|
|
||||||
|
def get_foreign_keys(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about foreign_keys in `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name`, and an optional string `schema`, return
|
||||||
|
foreign key information as a list of dicts with these keys:
|
||||||
|
|
||||||
|
constrained_columns
|
||||||
|
a list of column names that make up the foreign key
|
||||||
|
|
||||||
|
referred_schema
|
||||||
|
the name of the referred schema
|
||||||
|
|
||||||
|
referred_table
|
||||||
|
the name of the referred table
|
||||||
|
|
||||||
|
referred_columns
|
||||||
|
a list of column names in the referred table that correspond to
|
||||||
|
constrained_columns
|
||||||
|
|
||||||
|
name
|
||||||
|
optional name of the foreign key constraint.
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_foreign_keys(self.bind, table_name, schema,
|
||||||
|
info_cache=self.info_cache,
|
||||||
|
**kw)
|
||||||
|
|
||||||
|
def get_indexes(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about indexes in `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name` and an optional string `schema`, return
|
||||||
|
index information as a list of dicts with these keys:
|
||||||
|
|
||||||
|
name
|
||||||
|
the index's name
|
||||||
|
|
||||||
|
column_names
|
||||||
|
list of column names in order
|
||||||
|
|
||||||
|
unique
|
||||||
|
boolean
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_indexes(self.bind, table_name,
|
||||||
|
schema,
|
||||||
|
info_cache=self.info_cache, **kw)
|
||||||
|
|
||||||
|
def get_unique_constraints(self, table_name, schema=None, **kw):
|
||||||
|
"""Return information about unique constraints in `table_name`.
|
||||||
|
|
||||||
|
Given a string `table_name` and an optional string `schema`, return
|
||||||
|
unique constraint information as a list of dicts with these keys:
|
||||||
|
|
||||||
|
name
|
||||||
|
the unique constraint's name
|
||||||
|
|
||||||
|
column_names
|
||||||
|
list of column names in order
|
||||||
|
|
||||||
|
:param table_name: string name of the table. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
:param schema: string schema name; if omitted, uses the default schema
|
||||||
|
of the database connection. For special quoting,
|
||||||
|
use :class:`.quoted_name`.
|
||||||
|
|
||||||
|
.. versionadded:: 0.8.4
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
return self.dialect.get_unique_constraints(
|
||||||
|
self.bind, table_name, schema, info_cache=self.info_cache, **kw)
|
||||||
|
|
||||||
|
def reflecttable(self, table, include_columns, exclude_columns=()):
|
||||||
|
"""Given a Table object, load its internal constructs based on
|
||||||
|
introspection.
|
||||||
|
|
||||||
|
This is the underlying method used by most dialects to produce
|
||||||
|
table reflection. Direct usage is like::
|
||||||
|
|
||||||
|
from sqlalchemy import create_engine, MetaData, Table
|
||||||
|
from sqlalchemy.engine import reflection
|
||||||
|
|
||||||
|
engine = create_engine('...')
|
||||||
|
meta = MetaData()
|
||||||
|
user_table = Table('user', meta)
|
||||||
|
insp = Inspector.from_engine(engine)
|
||||||
|
insp.reflecttable(user_table, None)
|
||||||
|
|
||||||
|
:param table: a :class:`~sqlalchemy.schema.Table` instance.
|
||||||
|
:param include_columns: a list of string column names to include
|
||||||
|
in the reflection process. If ``None``, all columns are reflected.
|
||||||
|
|
||||||
|
"""
|
||||||
|
dialect = self.bind.dialect
|
||||||
|
|
||||||
|
# table attributes we might need.
|
||||||
|
reflection_options = {}
|
||||||
|
|
||||||
|
schema = table.schema
|
||||||
|
table_name = table.name
|
||||||
|
|
||||||
|
# apply table options
|
||||||
|
tbl_opts = self.get_table_options(table_name, schema, **table.kwargs)
|
||||||
|
if tbl_opts:
|
||||||
|
table.kwargs.update(tbl_opts)
|
||||||
|
|
||||||
|
# table.kwargs will need to be passed to each reflection method. Make
|
||||||
|
# sure keywords are strings.
|
||||||
|
tblkw = table.kwargs.copy()
|
||||||
|
for (k, v) in list(tblkw.items()):
|
||||||
|
del tblkw[k]
|
||||||
|
tblkw[str(k)] = v
|
||||||
|
|
||||||
|
if isinstance(schema, str):
|
||||||
|
schema = schema.decode(dialect.encoding)
|
||||||
|
if isinstance(table_name, str):
|
||||||
|
table_name = table_name.decode(dialect.encoding)
|
||||||
|
|
||||||
|
# columns
|
||||||
|
found_table = False
|
||||||
|
cols_by_orig_name = {}
|
||||||
|
|
||||||
|
for col_d in self.get_columns(table_name, schema, **tblkw):
|
||||||
|
found_table = True
|
||||||
|
orig_name = col_d['name']
|
||||||
|
|
||||||
|
name = col_d['name']
|
||||||
|
if include_columns and name not in include_columns:
|
||||||
|
continue
|
||||||
|
if exclude_columns and name in exclude_columns:
|
||||||
|
continue
|
||||||
|
|
||||||
|
coltype = col_d['type']
|
||||||
|
col_kw = {
|
||||||
|
'nullable': col_d['nullable'],
|
||||||
|
}
|
||||||
|
for k in ('autoincrement', 'quote', 'info', 'key'):
|
||||||
|
if k in col_d:
|
||||||
|
col_kw[k] = col_d[k]
|
||||||
|
|
||||||
|
colargs = []
|
||||||
|
if col_d.get('default') is not None:
|
||||||
|
# the "default" value is assumed to be a literal SQL
|
||||||
|
# expression, so is wrapped in text() so that no quoting
|
||||||
|
# occurs on re-issuance.
|
||||||
|
colargs.append(
|
||||||
|
sa_schema.DefaultClause(
|
||||||
|
sql.text(col_d['default']), _reflected=True
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'sequence' in col_d:
|
||||||
|
# TODO: mssql and sybase are using this.
|
||||||
|
seq = col_d['sequence']
|
||||||
|
sequence = sa_schema.Sequence(seq['name'], 1, 1)
|
||||||
|
if 'start' in seq:
|
||||||
|
sequence.start = seq['start']
|
||||||
|
if 'increment' in seq:
|
||||||
|
sequence.increment = seq['increment']
|
||||||
|
colargs.append(sequence)
|
||||||
|
|
||||||
|
cols_by_orig_name[orig_name] = col = \
|
||||||
|
sa_schema.Column(name, coltype, *colargs, **col_kw)
|
||||||
|
|
||||||
|
table.append_column(col)
|
||||||
|
|
||||||
|
if not found_table:
|
||||||
|
raise exc.NoSuchTableError(table.name)
|
||||||
|
|
||||||
|
# Primary keys
|
||||||
|
pk_cons = self.get_pk_constraint(table_name, schema, **tblkw)
|
||||||
|
if pk_cons:
|
||||||
|
pk_cols = [
|
||||||
|
cols_by_orig_name[pk]
|
||||||
|
for pk in pk_cons['constrained_columns']
|
||||||
|
if pk in cols_by_orig_name and pk not in exclude_columns
|
||||||
|
]
|
||||||
|
pk_cols += [
|
||||||
|
pk
|
||||||
|
for pk in table.primary_key
|
||||||
|
if pk.key in exclude_columns
|
||||||
|
]
|
||||||
|
primary_key_constraint = sa_schema.PrimaryKeyConstraint(
|
||||||
|
name=pk_cons.get('name'),
|
||||||
|
*pk_cols
|
||||||
|
)
|
||||||
|
|
||||||
|
table.append_constraint(primary_key_constraint)
|
||||||
|
|
||||||
|
# Foreign keys
|
||||||
|
fkeys = self.get_foreign_keys(table_name, schema, **tblkw)
|
||||||
|
for fkey_d in fkeys:
|
||||||
|
conname = fkey_d['name']
|
||||||
|
# look for columns by orig name in cols_by_orig_name,
|
||||||
|
# but support columns that are in-Python only as fallback
|
||||||
|
constrained_columns = [
|
||||||
|
cols_by_orig_name[c].key
|
||||||
|
if c in cols_by_orig_name else c
|
||||||
|
for c in fkey_d['constrained_columns']
|
||||||
|
]
|
||||||
|
if exclude_columns and set(constrained_columns).intersection(
|
||||||
|
exclude_columns):
|
||||||
|
continue
|
||||||
|
referred_schema = fkey_d['referred_schema']
|
||||||
|
referred_table = fkey_d['referred_table']
|
||||||
|
referred_columns = fkey_d['referred_columns']
|
||||||
|
refspec = []
|
||||||
|
if referred_schema is not None:
|
||||||
|
sa_schema.Table(referred_table, table.metadata,
|
||||||
|
autoload=True, schema=referred_schema,
|
||||||
|
autoload_with=self.bind,
|
||||||
|
**reflection_options
|
||||||
|
)
|
||||||
|
for column in referred_columns:
|
||||||
|
refspec.append(".".join(
|
||||||
|
[referred_schema, referred_table, column]))
|
||||||
|
else:
|
||||||
|
sa_schema.Table(referred_table, table.metadata, autoload=True,
|
||||||
|
autoload_with=self.bind,
|
||||||
|
**reflection_options
|
||||||
|
)
|
||||||
|
for column in referred_columns:
|
||||||
|
refspec.append(".".join([referred_table, column]))
|
||||||
|
if 'options' in fkey_d:
|
||||||
|
options = fkey_d['options']
|
||||||
|
else:
|
||||||
|
options = {}
|
||||||
|
table.append_constraint(
|
||||||
|
sa_schema.ForeignKeyConstraint(constrained_columns, refspec,
|
||||||
|
conname, link_to_name=True,
|
||||||
|
**options))
|
||||||
|
# Indexes
|
||||||
|
indexes = self.get_indexes(table_name, schema)
|
||||||
|
for index_d in indexes:
|
||||||
|
name = index_d['name']
|
||||||
|
columns = index_d['column_names']
|
||||||
|
unique = index_d['unique']
|
||||||
|
flavor = index_d.get('type', 'unknown type')
|
||||||
|
if include_columns and \
|
||||||
|
not set(columns).issubset(include_columns):
|
||||||
|
util.warn(
|
||||||
|
"Omitting %s KEY for (%s), key covers omitted columns." %
|
||||||
|
(flavor, ', '.join(columns)))
|
||||||
|
continue
|
||||||
|
# look for columns by orig name in cols_by_orig_name,
|
||||||
|
# but support columns that are in-Python only as fallback
|
||||||
|
sa_schema.Index(name, *[
|
||||||
|
cols_by_orig_name[c] if c in cols_by_orig_name
|
||||||
|
else table.c[c]
|
||||||
|
for c in columns
|
||||||
|
],
|
||||||
|
**dict(unique=unique))
|
|
@ -7,21 +7,26 @@ which is released under the MIT license.
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from distutils.version import StrictVersion
|
||||||
from pyhive import hive
|
from pyhive import hive
|
||||||
|
from sqlalchemy.sql import compiler
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from sqlalchemy import schema
|
|
||||||
from sqlalchemy import types
|
from sqlalchemy import types
|
||||||
from sqlalchemy import util
|
from sqlalchemy import util
|
||||||
from sqlalchemy.databases import mysql
|
from sqlalchemy.databases import mysql
|
||||||
from sqlalchemy.engine import default
|
from sqlalchemy.engine import default
|
||||||
from sqlalchemy.sql import compiler
|
|
||||||
import decimal
|
import decimal
|
||||||
import re
|
import re
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from sqlalchemy import processors
|
from sqlalchemy import processors
|
||||||
except ImportError:
|
except ImportError:
|
||||||
from pyhive import sqlalchemy_processors as processors
|
from pyhive import sqlalchemy_backports as processors
|
||||||
|
try:
|
||||||
|
from sqlalchemy.sql.compiler import SQLCompiler
|
||||||
|
except ImportError:
|
||||||
|
from sqlalchemy.sql.compiler import DefaultCompiler as SQLCompiler
|
||||||
|
|
||||||
|
|
||||||
class HiveStringTypeBase(types.TypeDecorator):
|
class HiveStringTypeBase(types.TypeDecorator):
|
||||||
|
@ -338,10 +343,28 @@ _type_map = {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class HiveCompiler(SQLCompiler):
|
||||||
|
def visit_concat_op_binary(self, binary, operator, **kw):
|
||||||
|
return "concat(%s, %s)" % (self.process(binary.left), self.process(binary.right))
|
||||||
|
|
||||||
|
|
||||||
|
if StrictVersion(sqlalchemy.__version__) >= StrictVersion('0.6.0'):
|
||||||
|
class HiveTypeCompiler(compiler.GenericTypeCompiler):
|
||||||
|
def visit_INTEGER(self, type_):
|
||||||
|
return 'INT'
|
||||||
|
|
||||||
|
def visit_CHAR(self, type_):
|
||||||
|
return 'STRING'
|
||||||
|
|
||||||
|
def visit_VARCHAR(self, type_):
|
||||||
|
return 'STRING'
|
||||||
|
|
||||||
|
|
||||||
class HiveDialect(default.DefaultDialect):
|
class HiveDialect(default.DefaultDialect):
|
||||||
name = 'hive'
|
name = b'hive'
|
||||||
driver = 'thrift'
|
driver = b'thrift'
|
||||||
preparer = HiveIdentifierPreparer
|
preparer = HiveIdentifierPreparer
|
||||||
|
statement_compiler = HiveCompiler
|
||||||
supports_alter = True
|
supports_alter = True
|
||||||
supports_pk_autoincrement = False
|
supports_pk_autoincrement = False
|
||||||
supports_default_values = False
|
supports_default_values = False
|
||||||
|
@ -371,53 +394,99 @@ class HiveDialect(default.DefaultDialect):
|
||||||
kwargs.update(url.query)
|
kwargs.update(url.query)
|
||||||
return ([], kwargs)
|
return ([], kwargs)
|
||||||
|
|
||||||
def reflecttable(self, connection, table, include_columns=None, exclude_columns=None):
|
def get_schema_names(self, connection, **kw):
|
||||||
exclude_columns = exclude_columns or []
|
# Equivalent to SHOW DATABASES
|
||||||
|
return [row.database_name for row in connection.execute('SHOW SCHEMAS')]
|
||||||
|
|
||||||
|
def _get_table_columns(self, connection, table_name, schema):
|
||||||
|
full_table = table_name
|
||||||
|
if schema:
|
||||||
|
full_table = schema + '.' + table_name
|
||||||
# TODO using TGetColumnsReq hangs after sending TFetchResultsReq.
|
# TODO using TGetColumnsReq hangs after sending TFetchResultsReq.
|
||||||
# Using DESCRIBE works but is uglier.
|
# Using DESCRIBE works but is uglier.
|
||||||
try:
|
try:
|
||||||
# This needs the table name to be unescaped (no backticks).
|
# This needs the table name to be unescaped (no backticks).
|
||||||
rows = connection.execute('DESCRIBE {}'.format(table)).fetchall()
|
rows = connection.execute('DESCRIBE {}'.format(full_table)).fetchall()
|
||||||
except exc.OperationalError as e:
|
except exc.OperationalError as e:
|
||||||
# Does the table exist?
|
# Does the table exist?
|
||||||
regex_fmt = r'TExecuteStatementResp.*SemanticException.*Table not found {}'
|
regex_fmt = r'TExecuteStatementResp.*SemanticException.*Table not found {}'
|
||||||
regex = regex_fmt.format(re.escape(table.name))
|
regex = regex_fmt.format(re.escape(full_table))
|
||||||
if re.search(regex, e.message):
|
if re.search(regex, e.message):
|
||||||
raise exc.NoSuchTableError(table.name)
|
raise exc.NoSuchTableError(full_table)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
else:
|
else:
|
||||||
# Strip whitespace
|
# Hive is stupid: this is what I get from DESCRIBE some_schema.does_not_exist
|
||||||
rows = [[col.strip() if col else None for col in row] for row in rows]
|
regex = r'Table .* does not exist'
|
||||||
# Filter out empty rows and comment
|
if len(rows) == 1 and re.match(regex, rows[0].col_name):
|
||||||
rows = [row for row in rows if row[0] and row[0] != '# col_name']
|
raise exc.NoSuchTableError(full_table)
|
||||||
for i, (col_name, col_type, _comment) in enumerate(rows):
|
return rows
|
||||||
if col_name == '# Partition Information':
|
|
||||||
break
|
def has_table(self, connection, table_name, schema=None):
|
||||||
# Take out the more detailed type information
|
try:
|
||||||
# e.g. 'map<int,int>' -> 'map'
|
self._get_table_columns(connection, table_name, schema)
|
||||||
col_type = col_type.partition('<')[0]
|
return True
|
||||||
if include_columns is not None and col_name not in include_columns:
|
except exc.NoSuchTableError:
|
||||||
continue
|
return False
|
||||||
if col_name in exclude_columns:
|
|
||||||
continue
|
def get_columns(self, connection, table_name, schema=None, **kw):
|
||||||
try:
|
rows = self._get_table_columns(connection, table_name, schema)
|
||||||
coltype = _type_map[col_type]
|
# Strip whitespace
|
||||||
except KeyError:
|
rows = [[col.strip() if col else None for col in row] for row in rows]
|
||||||
util.warn("Did not recognize type '%s' of column '%s'" % (
|
# Filter out empty rows and comment
|
||||||
col_type, col_name))
|
rows = [row for row in rows if row[0] and row[0] != '# col_name']
|
||||||
coltype = types.NullType
|
result = []
|
||||||
table.append_column(schema.Column(
|
for (col_name, col_type, _comment) in rows:
|
||||||
name=col_name,
|
if col_name == '# Partition Information':
|
||||||
type_=coltype,
|
break
|
||||||
))
|
# Take out the more detailed type information
|
||||||
# Handle partition columns
|
# e.g. 'map<int,int>' -> 'map'
|
||||||
for col_name, col_type, _comment in rows[i + 1:]:
|
col_type = col_type.partition('<')[0]
|
||||||
if include_columns is not None and col_name not in include_columns:
|
try:
|
||||||
continue
|
coltype = _type_map[col_type]
|
||||||
if col_name in exclude_columns:
|
except KeyError:
|
||||||
continue
|
util.warn("Did not recognize type '%s' of column '%s'" % (
|
||||||
getattr(table.c, col_name).index = True
|
col_type, col_name))
|
||||||
|
coltype = types.NullType
|
||||||
|
result.append({
|
||||||
|
'name': col_name,
|
||||||
|
'type': coltype,
|
||||||
|
'nullable': True,
|
||||||
|
'default': None,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
|
||||||
|
# Hive has no support for foreign keys.
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
|
||||||
|
# Hive has no support for primary keys.
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_indexes(self, connection, table_name, schema=None, **kw):
|
||||||
|
rows = self._get_table_columns(connection, table_name, schema)
|
||||||
|
# Strip whitespace
|
||||||
|
rows = [[col.strip() if col else None for col in row] for row in rows]
|
||||||
|
# Filter out empty rows and comment
|
||||||
|
rows = [row for row in rows if row[0] and row[0] != '# col_name']
|
||||||
|
for i, (col_name, _col_type, _comment) in enumerate(rows):
|
||||||
|
if col_name == '# Partition Information':
|
||||||
|
break
|
||||||
|
# Handle partition columns
|
||||||
|
col_names = []
|
||||||
|
for col_name, _col_type, _comment in rows[i + 1:]:
|
||||||
|
col_names.append(col_name)
|
||||||
|
if col_names:
|
||||||
|
return [{'name': 'partition', 'column_names': col_names, 'unique': False}]
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_table_names(self, connection, schema=None, **kw):
|
||||||
|
query = 'SHOW TABLES'
|
||||||
|
if schema:
|
||||||
|
query += ' IN ' + self.identifier_preparer.quote_identifier(schema)
|
||||||
|
return [row.tab_name for row in connection.execute(query)]
|
||||||
|
|
||||||
def do_rollback(self, dbapi_connection):
|
def do_rollback(self, dbapi_connection):
|
||||||
# No transactions for Hive
|
# No transactions for Hive
|
||||||
|
@ -430,3 +499,13 @@ class HiveDialect(default.DefaultDialect):
|
||||||
def _check_unicode_description(self, connection):
|
def _check_unicode_description(self, connection):
|
||||||
# We decode everything as UTF-8
|
# We decode everything as UTF-8
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if StrictVersion(sqlalchemy.__version__) < StrictVersion('0.6.0'):
|
||||||
|
from pyhive import sqlalchemy_backports
|
||||||
|
|
||||||
|
def reflecttable(self, connection, table, include_columns=None, exclude_columns=None):
|
||||||
|
insp = sqlalchemy_backports.Inspector.from_engine(connection)
|
||||||
|
return insp.reflecttable(table, include_columns, exclude_columns)
|
||||||
|
HiveDialect.reflecttable = reflecttable
|
||||||
|
else:
|
||||||
|
HiveDialect.type_compiler = HiveTypeCompiler
|
||||||
|
|
|
@ -7,14 +7,15 @@ which is released under the MIT license.
|
||||||
|
|
||||||
from __future__ import absolute_import
|
from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
|
from distutils.version import StrictVersion
|
||||||
from pyhive import presto
|
from pyhive import presto
|
||||||
from sqlalchemy import exc
|
from sqlalchemy import exc
|
||||||
from sqlalchemy import schema
|
|
||||||
from sqlalchemy import types
|
from sqlalchemy import types
|
||||||
from sqlalchemy import util
|
from sqlalchemy import util
|
||||||
from sqlalchemy.engine import default
|
from sqlalchemy.engine import default
|
||||||
from sqlalchemy.sql import compiler
|
from sqlalchemy.sql import compiler
|
||||||
import re
|
import re
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
|
||||||
class PrestoIdentifierPreparer(compiler.IdentifierPreparer):
|
class PrestoIdentifierPreparer(compiler.IdentifierPreparer):
|
||||||
|
@ -147,10 +148,12 @@ class PrestoDialect(default.DefaultDialect):
|
||||||
raise ValueError("Unexpected database format {}".format(url.database))
|
raise ValueError("Unexpected database format {}".format(url.database))
|
||||||
return ([], kwargs)
|
return ([], kwargs)
|
||||||
|
|
||||||
def reflecttable(self, connection, table, include_columns=None, exclude_columns=None):
|
def _get_table_columns(self, connection, table_name, schema):
|
||||||
exclude_columns = exclude_columns or []
|
full_table = self.identifier_preparer.quote_identifier(table_name)
|
||||||
|
if schema:
|
||||||
|
full_table = self.identifier_preparer.quote_identifier(schema) + '.' + full_table
|
||||||
try:
|
try:
|
||||||
rows = connection.execute('SHOW COLUMNS FROM "{}"'.format(table))
|
return connection.execute('SHOW COLUMNS FROM {}'.format(full_table))
|
||||||
except presto.DatabaseError as e:
|
except presto.DatabaseError as e:
|
||||||
# Normally SQLAlchemy should wrap this exception in sqlalchemy.exc.DatabaseError, which
|
# Normally SQLAlchemy should wrap this exception in sqlalchemy.exc.DatabaseError, which
|
||||||
# it successfully does in the Hive version. The difference with Presto is that this
|
# it successfully does in the Hive version. The difference with Presto is that this
|
||||||
|
@ -159,29 +162,61 @@ class PrestoDialect(default.DefaultDialect):
|
||||||
# presto.DatabaseError here.
|
# presto.DatabaseError here.
|
||||||
# Does the table exist?
|
# Does the table exist?
|
||||||
msg = e.message.get('message') if isinstance(e.message, dict) else None
|
msg = e.message.get('message') if isinstance(e.message, dict) else None
|
||||||
regex = r"^Table\ \'.*{}\'\ does\ not\ exist$".format(re.escape(table.name))
|
regex = r"^Table\ \'.*{}\'\ does\ not\ exist$".format(re.escape(table_name))
|
||||||
if msg and re.match(regex, msg):
|
if msg and re.match(regex, msg):
|
||||||
raise exc.NoSuchTableError(table.name)
|
raise exc.NoSuchTableError(table_name)
|
||||||
else:
|
else:
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def has_table(self, connection, table_name, schema=None):
|
||||||
|
try:
|
||||||
|
self._get_table_columns(connection, table_name, schema)
|
||||||
|
return True
|
||||||
|
except exc.NoSuchTableError:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def get_columns(self, connection, table_name, schema=None, **kw):
|
||||||
|
rows = self._get_table_columns(connection, table_name, None)
|
||||||
|
result = []
|
||||||
|
for row in rows:
|
||||||
|
name, coltype, nullable, _is_partition_key = row
|
||||||
|
try:
|
||||||
|
coltype = _type_map[coltype]
|
||||||
|
except KeyError:
|
||||||
|
util.warn("Did not recognize type '%s' of column '%s'" % (coltype, name))
|
||||||
|
coltype = types.NullType
|
||||||
|
result.append({
|
||||||
|
'name': name,
|
||||||
|
'type': coltype,
|
||||||
|
'nullable': nullable,
|
||||||
|
'default': None,
|
||||||
|
})
|
||||||
|
return result
|
||||||
|
|
||||||
|
def get_foreign_keys(self, connection, table_name, schema=None, **kw):
|
||||||
|
# Hive has no support for foreign keys.
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_pk_constraint(self, connection, table_name, schema=None, **kw):
|
||||||
|
# Hive has no support for primary keys.
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_indexes(self, connection, table_name, schema=None, **kw):
|
||||||
|
rows = self._get_table_columns(connection, table_name, None)
|
||||||
|
col_names = []
|
||||||
|
for row in rows:
|
||||||
|
if row['Partition Key']:
|
||||||
|
col_names.append(row['Column'])
|
||||||
|
if col_names:
|
||||||
|
return [{'name': 'partition', 'column_names': col_names, 'unique': False}]
|
||||||
else:
|
else:
|
||||||
for row in rows:
|
return []
|
||||||
name, coltype, nullable, is_partition_key = row
|
|
||||||
if include_columns is not None and name not in include_columns:
|
def get_table_names(self, connection, schema=None, **kw):
|
||||||
continue
|
query = 'SHOW TABLES'
|
||||||
if name in exclude_columns:
|
if schema:
|
||||||
continue
|
query += ' FROM ' + self.identifier_preparer.quote_identifier(schema)
|
||||||
try:
|
return [row.tab_name for row in connection.execute(query)]
|
||||||
coltype = _type_map[coltype]
|
|
||||||
except KeyError:
|
|
||||||
util.warn("Did not recognize type '%s' of column '%s'" % (coltype, name))
|
|
||||||
coltype = types.NullType
|
|
||||||
table.append_column(schema.Column(
|
|
||||||
name=name,
|
|
||||||
type_=coltype,
|
|
||||||
nullable=nullable,
|
|
||||||
index=is_partition_key, # Translate Hive partitions to indexes
|
|
||||||
))
|
|
||||||
|
|
||||||
def do_rollback(self, dbapi_connection):
|
def do_rollback(self, dbapi_connection):
|
||||||
# No transactions for Presto
|
# No transactions for Presto
|
||||||
|
@ -194,3 +229,11 @@ class PrestoDialect(default.DefaultDialect):
|
||||||
def _check_unicode_description(self, connection):
|
def _check_unicode_description(self, connection):
|
||||||
# requests gives back Unicode strings
|
# requests gives back Unicode strings
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
if StrictVersion(sqlalchemy.__version__) < StrictVersion('0.6.0'):
|
||||||
|
from pyhive import sqlalchemy_backports
|
||||||
|
|
||||||
|
def reflecttable(self, connection, table, include_columns=None, exclude_columns=None):
|
||||||
|
insp = sqlalchemy_backports.Inspector.from_engine(connection)
|
||||||
|
return insp.reflecttable(table, include_columns, exclude_columns)
|
||||||
|
PrestoDialect.reflecttable = reflecttable
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
# Taken from SQLAlchemy with lots of stuff stripped out.
|
|
||||||
|
|
||||||
# sqlalchemy/processors.py
|
|
||||||
# Copyright (C) 2010-2013 the SQLAlchemy authors and contributors <see AUTHORS file>
|
|
||||||
# Copyright (C) 2010 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
|
|
||||||
|
|
||||||
"""defines generic type conversion functions, as used in bind and result
|
|
||||||
processors.
|
|
||||||
|
|
||||||
They all share one common characteristic: None is passed through unchanged.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
import datetime
|
|
||||||
import re
|
|
||||||
|
|
||||||
|
|
||||||
def str_to_datetime_processor_factory(regexp, type_):
|
|
||||||
rmatch = regexp.match
|
|
||||||
# Even on python2.6 datetime.strptime is both slower than this code
|
|
||||||
# and it does not support microseconds.
|
|
||||||
has_named_groups = bool(regexp.groupindex)
|
|
||||||
|
|
||||||
def process(value):
|
|
||||||
if value is None:
|
|
||||||
return None
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
m = rmatch(value)
|
|
||||||
except TypeError:
|
|
||||||
raise ValueError("Couldn't parse %s string '%r' "
|
|
||||||
"- value is not a string." %
|
|
||||||
(type_.__name__, value))
|
|
||||||
if m is None:
|
|
||||||
raise ValueError("Couldn't parse %s string: "
|
|
||||||
"'%s'" % (type_.__name__, value))
|
|
||||||
if has_named_groups:
|
|
||||||
groups = m.groupdict(0)
|
|
||||||
return type_(**dict(zip(groups.iterkeys(),
|
|
||||||
map(int, groups.itervalues()))))
|
|
||||||
else:
|
|
||||||
return type_(*map(int, m.groups(0)))
|
|
||||||
return process
|
|
||||||
|
|
||||||
|
|
||||||
DATETIME_RE = re.compile("(\d+)-(\d+)-(\d+) (\d+):(\d+):(\d+)(?:\.(\d+))?")
|
|
||||||
|
|
||||||
str_to_datetime = str_to_datetime_processor_factory(DATETIME_RE,
|
|
||||||
datetime.datetime)
|
|
|
@ -3,6 +3,7 @@ from __future__ import absolute_import
|
||||||
from __future__ import unicode_literals
|
from __future__ import unicode_literals
|
||||||
from sqlalchemy import select
|
from sqlalchemy import select
|
||||||
from sqlalchemy.exc import NoSuchTableError
|
from sqlalchemy.exc import NoSuchTableError
|
||||||
|
from sqlalchemy.schema import Index
|
||||||
from sqlalchemy.schema import MetaData
|
from sqlalchemy.schema import MetaData
|
||||||
from sqlalchemy.schema import Table
|
from sqlalchemy.schema import Table
|
||||||
from sqlalchemy.sql import expression
|
from sqlalchemy.sql import expression
|
||||||
|
@ -47,7 +48,8 @@ class SqlAlchemyTestCase(object):
|
||||||
def test_reflect_include_columns(self, engine, connection):
|
def test_reflect_include_columns(self, engine, connection):
|
||||||
"""When passed include_columns, reflecttable should filter out other columns"""
|
"""When passed include_columns, reflecttable should filter out other columns"""
|
||||||
one_row_complex = Table('one_row_complex', MetaData(bind=engine))
|
one_row_complex = Table('one_row_complex', MetaData(bind=engine))
|
||||||
engine.dialect.reflecttable(connection, one_row_complex, include_columns=['int'])
|
engine.dialect.reflecttable(
|
||||||
|
connection, one_row_complex, include_columns=['int'], exclude_columns=[])
|
||||||
self.assertEqual(len(one_row_complex.c), 1)
|
self.assertEqual(len(one_row_complex.c), 1)
|
||||||
self.assertIsNotNone(one_row_complex.c.int)
|
self.assertIsNotNone(one_row_complex.c.int)
|
||||||
self.assertRaises(AttributeError, lambda: one_row_complex.c.tinyint)
|
self.assertRaises(AttributeError, lambda: one_row_complex.c.tinyint)
|
||||||
|
@ -57,17 +59,20 @@ class SqlAlchemyTestCase(object):
|
||||||
"""reflecttable should get the partition column as an index"""
|
"""reflecttable should get the partition column as an index"""
|
||||||
many_rows = Table('many_rows', MetaData(bind=engine), autoload=True)
|
many_rows = Table('many_rows', MetaData(bind=engine), autoload=True)
|
||||||
self.assertEqual(len(many_rows.c), 2)
|
self.assertEqual(len(many_rows.c), 2)
|
||||||
self.assertTrue(many_rows.c.b.index)
|
self.assertEqual(repr(many_rows.indexes), repr({Index('partition', many_rows.c.b)}))
|
||||||
|
|
||||||
many_rows = Table('many_rows', MetaData(bind=engine))
|
many_rows = Table('many_rows', MetaData(bind=engine))
|
||||||
engine.dialect.reflecttable(connection, many_rows, include_columns=['a'])
|
engine.dialect.reflecttable(
|
||||||
|
connection, many_rows, include_columns=['a'], exclude_columns=[])
|
||||||
self.assertEqual(len(many_rows.c), 1)
|
self.assertEqual(len(many_rows.c), 1)
|
||||||
self.assertFalse(many_rows.c.a.index)
|
self.assertFalse(many_rows.c.a.index)
|
||||||
|
self.assertFalse(many_rows.indexes)
|
||||||
|
|
||||||
many_rows = Table('many_rows', MetaData(bind=engine))
|
many_rows = Table('many_rows', MetaData(bind=engine))
|
||||||
engine.dialect.reflecttable(connection, many_rows, include_columns=['b'])
|
engine.dialect.reflecttable(
|
||||||
|
connection, many_rows, include_columns=['b'], exclude_columns=[])
|
||||||
self.assertEqual(len(many_rows.c), 1)
|
self.assertEqual(len(many_rows.c), 1)
|
||||||
self.assertTrue(many_rows.c.b.index)
|
self.assertEqual(repr(many_rows.indexes), repr({Index('partition', many_rows.c.b)}))
|
||||||
|
|
||||||
@with_engine_connection
|
@with_engine_connection
|
||||||
def test_unicode(self, engine, connection):
|
def test_unicode(self, engine, connection):
|
||||||
|
|
|
@ -0,0 +1,12 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
import sqlalchemy
|
||||||
|
|
||||||
|
if StrictVersion(sqlalchemy.__version__) >= StrictVersion('0.9.4'):
|
||||||
|
from sqlalchemy.dialects import registry
|
||||||
|
|
||||||
|
registry.register("hive", "pyhive.sqlalchemy_hive", "HiveDialect")
|
||||||
|
registry.register("presto", "pyhive.sqlalchemy_presto", "PrestoDialect")
|
||||||
|
|
||||||
|
from sqlalchemy.testing.plugin.pytestplugin import *
|
|
@ -0,0 +1,48 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from sqlalchemy.testing import exclusions
|
||||||
|
from sqlalchemy.testing.requirements import SuiteRequirements
|
||||||
|
|
||||||
|
|
||||||
|
class Requirements(SuiteRequirements):
|
||||||
|
@property
|
||||||
|
def self_referential_foreign_keys(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def index_reflection(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def view_reflection(self):
|
||||||
|
# Hive supports views, but there's no SHOW VIEWS command, which breaks the tests.
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def foreign_key_constraint_reflection(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def primary_key_constraint_reflection(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_constraint_reflection(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def schemas(self):
|
||||||
|
return exclusions.open()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def date(self):
|
||||||
|
# Added in Hive 0.12
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def implements_get_lastrowid(self):
|
||||||
|
return exclusions.closed()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def views(self):
|
||||||
|
return exclusions.closed()
|
|
@ -0,0 +1,109 @@
|
||||||
|
from __future__ import absolute_import
|
||||||
|
from __future__ import unicode_literals
|
||||||
|
from distutils.version import StrictVersion
|
||||||
|
from sqlalchemy import types as sql_types
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
if StrictVersion(sa.__version__) >= StrictVersion('0.9.4'):
|
||||||
|
from sqlalchemy.testing import suite
|
||||||
|
from sqlalchemy.testing.suite import *
|
||||||
|
|
||||||
|
class ComponentReflectionTest(suite.ComponentReflectionTest):
|
||||||
|
@classmethod
|
||||||
|
def define_reflected_tables(cls, metadata, schema):
|
||||||
|
users = Table('users', metadata,
|
||||||
|
Column('user_id', sa.INT),
|
||||||
|
Column('test1', sa.CHAR(5)),
|
||||||
|
Column('test2', sa.Float(5)),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
Table("dingalings", metadata,
|
||||||
|
Column('dingaling_id', sa.Integer),
|
||||||
|
Column('address_id', sa.Integer),
|
||||||
|
Column('data', sa.String(30)),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
Table('email_addresses', metadata,
|
||||||
|
Column('address_id', sa.Integer),
|
||||||
|
Column('remote_user_id', sa.Integer),
|
||||||
|
Column('email_address', sa.String(20)),
|
||||||
|
schema=schema,
|
||||||
|
)
|
||||||
|
|
||||||
|
if testing.requires.index_reflection.enabled:
|
||||||
|
cls.define_index(metadata, users)
|
||||||
|
if testing.requires.view_reflection.enabled:
|
||||||
|
cls.define_views(metadata, schema)
|
||||||
|
|
||||||
|
def test_nullable_reflection(self):
|
||||||
|
# TODO figure out why pytest treats unittest.skip as a failure
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_numeric_reflection(self):
|
||||||
|
# TODO figure out why pytest treats unittest.skip as a failure
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_varchar_reflection(self):
|
||||||
|
typ = self._type_round_trip(sql_types.String(52))[0]
|
||||||
|
assert isinstance(typ, sql_types.String)
|
||||||
|
|
||||||
|
class HasTableTest(suite.HasTableTest):
|
||||||
|
@classmethod
|
||||||
|
def define_tables(cls, metadata):
|
||||||
|
Table('test_table', metadata,
|
||||||
|
Column('id', Integer),
|
||||||
|
Column('data', String(50))
|
||||||
|
)
|
||||||
|
|
||||||
|
class OrderByLabelTest(suite.OrderByLabelTest):
|
||||||
|
_ran_insert_data = False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def define_tables(cls, metadata):
|
||||||
|
Table("some_table", metadata,
|
||||||
|
Column('id', Integer),
|
||||||
|
Column('x', Integer),
|
||||||
|
Column('y', Integer),
|
||||||
|
Column('q', String(50)),
|
||||||
|
Column('p', String(50))
|
||||||
|
)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def insert_data(cls):
|
||||||
|
if not cls._ran_insert_data: # MapReduce is slow
|
||||||
|
cls._ran_insert_data = True
|
||||||
|
config.db.execute('''
|
||||||
|
INSERT OVERWRITE TABLE some_table SELECT stack(3,
|
||||||
|
1, 1, 2, 'q1', 'p3',
|
||||||
|
2, 2, 3, 'q2', 'p2',
|
||||||
|
3, 3, 4, 'q3', 'p1'
|
||||||
|
) AS (id, x, y, q, p) FROM default.one_row
|
||||||
|
''')
|
||||||
|
|
||||||
|
class TableDDLTest(fixtures.TestBase):
|
||||||
|
def _simple_fixture(self):
|
||||||
|
return Table('test_table', self.metadata,
|
||||||
|
Column('id', Integer),
|
||||||
|
Column('data', String(50))
|
||||||
|
)
|
||||||
|
|
||||||
|
def _simple_roundtrip(self, table):
|
||||||
|
# Inserting data into Hive is hard.
|
||||||
|
pass
|
||||||
|
|
||||||
|
# These test rely on inserting data, which is hard in Hive.
|
||||||
|
# TODO could in theory compile insert statements using insert select from a known one row table.
|
||||||
|
BooleanTest = None
|
||||||
|
DateTimeMicrosecondsTest = None
|
||||||
|
DateTimeTest = None
|
||||||
|
InsertBehaviorTest = None
|
||||||
|
IntegerTest = None
|
||||||
|
NumericTest = None
|
||||||
|
RowFetchTest = None
|
||||||
|
SimpleUpdateDeleteTest = None
|
||||||
|
StringTest = None
|
||||||
|
TextTest = None
|
||||||
|
TimeMicrosecondsTest = None
|
||||||
|
TimeTest = None
|
||||||
|
UnicodeTextTest = None
|
||||||
|
UnicodeVarcharTest = None
|
|
@ -2,6 +2,13 @@
|
||||||
tag_build = dev
|
tag_build = dev
|
||||||
|
|
||||||
[pytest]
|
[pytest]
|
||||||
addopts = --cov pyhive --cov-report html --cov-report term
|
addopts = --tb=short --cov pyhive --cov-report html --cov-report term
|
||||||
norecursedirs = env
|
norecursedirs = env
|
||||||
python_files = test_*.py
|
python_files = test_*.py
|
||||||
|
|
||||||
|
[sqla_testing]
|
||||||
|
requirement_cls=pyhive.tests_sqlalchemy.hive_requirements:Requirements
|
||||||
|
profile_file=.profiles.txt
|
||||||
|
|
||||||
|
[db]
|
||||||
|
default=hive://hadoop@localhost:10000/sqlalchemy_test
|
||||||
|
|
Загрузка…
Ссылка в новой задаче