Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fixes mapping views #5

Open
wants to merge 12 commits into
base: master
Choose a base branch
from
12 changes: 7 additions & 5 deletions ACKNOWLEDGMENTS
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
THIS PROJECT IS DERIVED FROM THE FOLLOWING PROJECTS/FORKS:
- https://github.com/LocusEnergy/sqlalchemy-vertica-python
THIS PROJECT IS THE MAINLY DERIVED FROM startappdev repo:
- https://github.com/startappdev/sqlalchemy-vertica

THIS PROJECT WAS ALSO DERIVED FROM THE FOLLOWING PROJECTS:

- https://github.com/zzzeek/sqlalchemy
- https://github.com/bluelabsio/vertica-sqlalchemy
- https://github.com/dennisobrien/sqlalchemy-vertica-python
- https://github.com/Eighty20/sqlalchemy-vertica-python

THANKS TO ALL THE GREAT PEOPLE WHO'VE CONTRIBUTED TO THESE PROJECTS.
- https://github.com/LocusEnergy/sqlalchemy-vertica-python
- https://github.com/dennisobrien/sqlalchemy-vertica-python
10 changes: 8 additions & 2 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,14 @@ sqlalchemy-vertica

Vertica dialect for sqlalchemy.

Forked from the `Vertica dialect for sqlalchemy using vertica-python <https://pypi.python
.org/pypi/sqlalchemy-vertica-python>`_.
Forked from the `sqlalchemy-vertica repository <https://github.com/startappdev/sqlalchemy-vertica>`.
Unfortunately, sqlalchemy-vertica was removed from pypi. As of Sept 28, 2018 this version supports
querying views. This is version is not a state of the art, nor does it follow the principles
outlinedb by SQLAlchemy at:https://github.com/zzzeek/sqlalchemy/blob/master/README.dialects.rst.
However, I will slowly start upgrading the code base to meet standards and submit so that it
becomes an external dialect in SQLAlchemy. Anyone interested in helping is welcome to contribute
and/or submit issues/ideas.


.. code-block:: python

Expand Down
11 changes: 5 additions & 6 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
with open("README.rst", "r") as f:
description = f.read()

version_info = (0, 2, 5)
version_info = (0, 0, 5)
version = '.'.join(map(str, version_info))

setup(
Expand All @@ -12,10 +12,10 @@
description='Vertica dialect for sqlalchemy',
long_description=description,
license='MIT',
url='https://github.com/startappdev/sqlalchemy-vertica',
download_url='https://github.com/startappdev/sqlalchemy-vertica/tarball/%s' % (version,),
author='StartApp Inc.',
author_email='[email protected]',
url='https://github.com/lv10/sqlalchemy-vertica',
download_url='https://github.com/lv10/sqlalchemy-vertica/tarball/%s' % (version,),
author='Luis Villamarin',
author_email='[email protected]',
packages=(
'sqlalchemy_vertica',
),
Expand All @@ -28,7 +28,6 @@
'pyodbc>=4.0.16',
],
'vertica-python': [
'psycopg2>=2.7.1',
'vertica-python>=0.7.3',
],
},
Expand Down
163 changes: 147 additions & 16 deletions sqlalchemy_vertica/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@
import re
from sqlalchemy import exc
from sqlalchemy import sql
from sqlalchemy import util
from textwrap import dedent

from sqlalchemy.dialects.postgresql import BYTEA, DOUBLE_PRECISION
from sqlalchemy.dialects.postgresql import BYTEA, DOUBLE_PRECISION, INTERVAL
from sqlalchemy.dialects.postgresql.base import PGDialect, PGDDLCompiler
from sqlalchemy.engine import reflection
from sqlalchemy.types import INTEGER, BIGINT, SMALLINT, VARCHAR, CHAR, \
NUMERIC, FLOAT, REAL, DATE, DATETIME, BOOLEAN, BLOB, TIMESTAMP, TIME
from sqlalchemy.sql import sqltypes

ischema_names = {
'INT': INTEGER,
Expand All @@ -32,8 +34,11 @@
'DOUBLE': DOUBLE_PRECISION,
'TIMESTAMP': TIMESTAMP,
'TIMESTAMP WITH TIMEZONE': TIMESTAMP,
'TIMESTAMPTZ': TIMESTAMP(timezone=True),
'TIME': TIME,
'TIME WITH TIMEZONE': TIME,
'TIMETZ': TIME(timezone=True),
'INTERVAL': INTERVAL,
'DATE': DATE,
'DATETIME': DATETIME,
'SMALLDATETIME': DATETIME,
Expand All @@ -42,6 +47,9 @@
'RAW': BLOB,
'BYTEA': BYTEA,
'BOOLEAN': BOOLEAN,
'LONG VARBINARY': BLOB,
'LONG VARCHAR': VARCHAR,
'GEOMETRY': BLOB,
}


Expand Down Expand Up @@ -151,20 +159,44 @@ def get_schema_names(self, connection, **kw):
c = connection.execute(get_schemas_sql)
return [row[0] for row in c if not row[0].startswith('v_')]

@reflection.cache
def get_table_comment(self, connection, table_name, schema=None, **kw):
if schema is None:
schema_conditional = ""
else:
schema_conditional = "AND object_schema = '{schema}'".format(schema=schema)
query = """
SELECT
comment
FROM
v_catalog.comments
WHERE
object_type = 'TABLE'
AND
object_name = :table_name
{schema_conditional}
""".format(schema_conditional=schema_conditional)
c = connection.execute(sql.text(query), table_name=table_name)
return {"text": c.scalar()}

@reflection.cache
def get_table_oid(self, connection, table_name, schema=None, **kw):
if schema is None:
schema = self._get_default_schema_name(connection)

get_oid_sql = sql.text(dedent("""
SELECT table_id
FROM v_catalog.tables
WHERE lower(table_name) = '%(table)s'
AND lower(table_schema) = '%(schema)s'
SELECT A.table_id
FROM
(SELECT table_id, table_name, table_schema FROM v_catalog.tables
UNION
SELECT table_id, table_name, table_schema FROM v_catalog.views) AS A
WHERE lower(A.table_name) = '%(table)s'
AND lower(A.table_schema) = '%(schema)s'
""" % {'schema': schema.lower(), 'table': table_name.lower()}))

c = connection.execute(get_oid_sql)
table_oid = c.scalar()

if table_oid is None:
raise exc.NoSuchTableError(table_name)
return table_oid
Expand Down Expand Up @@ -256,21 +288,20 @@ def get_columns(self, connection, table_name, schema=None, **kw):
columns = []
for row in connection.execute(s):
name = row.column_name
dtype = row.data_type.upper()
if '(' in dtype:
dtype = dtype.split('(')[0]
coltype = self.ischema_names[dtype]
dtype = row.data_type.lower()
primary_key = name in pk_columns
default = row.column_default
nullable = row.is_nullable

columns.append({
'name': name,
'type': coltype,
'nullable': nullable,
'default': default,
'primary_key': primary_key
})
column_info = self._get_column_info(
name,
dtype,
default,
nullable,
schema,
)
column_info.update({'primary_key': primary_key})
columns.append(column_info)
return columns

@reflection.cache
Expand Down Expand Up @@ -341,3 +372,103 @@ def get_indexes(self, connection, table_name, schema, **kw):
# noinspection PyUnusedLocal
def visit_create_index(self, create):
return None

def _get_column_info(
self,
name,
format_type,
default,
nullable,
schema,
):

# strip (*) from character varying(5), timestamp(5)
# with time zone, geometry(POLYGON), etc.
attype = re.sub(r"\(.*\)", "", format_type)

charlen = re.search(r"\(([\d,]+)\)", format_type)
if charlen:
charlen = charlen.group(1)
args = re.search(r"\((.*)\)", format_type)
if args and args.group(1):
args = tuple(re.split(r"\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 == "integer":
args = ()
elif attype in ("timestamptz", "timetz"):
kwargs["timezone"] = True
if charlen:
kwargs["precision"] = int(charlen)
args = ()
elif attype in (
"timestamp",
"time",
):
kwargs["timezone"] = False
if charlen:
kwargs["precision"] = int(charlen)
args = ()
elif attype.startswith("interval"):
field_match = re.match(r"interval (.+)", attype, re.I)
if charlen:
kwargs["precision"] = int(charlen)
if field_match:
kwargs["fields"] = field_match.group(1)
attype = "interval"
args = ()
elif charlen:
args = (int(charlen),)

while True:
if attype.upper() in self.ischema_names:
coltype = self.ischema_names[attype.upper()]
break
else:
coltype = None
break

if coltype:
coltype = coltype(*args, **kwargs)
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:
if issubclass(coltype._type_affinity, sqltypes.Integer):
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