diff --git a/ACKNOWLEDGMENTS b/ACKNOWLEDGMENTS index 0483991..08c43e2 100644 --- a/ACKNOWLEDGMENTS +++ b/ACKNOWLEDGMENTS @@ -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 diff --git a/README.rst b/README.rst index 796720a..0cd5417 100644 --- a/README.rst +++ b/README.rst @@ -3,8 +3,14 @@ sqlalchemy-vertica Vertica dialect for sqlalchemy. -Forked from the `Vertica dialect for sqlalchemy using vertica-python `_. +Forked from the `sqlalchemy-vertica repository `. +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 diff --git a/setup.py b/setup.py index 9f34f33..83c1a90 100644 --- a/setup.py +++ b/setup.py @@ -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( @@ -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='ben.feinstein@startapp.com', + url='https://github.com/lv10/sqlalchemy-vertica', + download_url='https://github.com/lv10/sqlalchemy-vertica/tarball/%s' % (version,), + author='Luis Villamarin', + author_email='luis@lv10.me', packages=( 'sqlalchemy_vertica', ), @@ -28,7 +28,6 @@ 'pyodbc>=4.0.16', ], 'vertica-python': [ - 'psycopg2>=2.7.1', 'vertica-python>=0.7.3', ], }, diff --git a/sqlalchemy_vertica/base.py b/sqlalchemy_vertica/base.py index b039a86..7fe1605 100644 --- a/sqlalchemy_vertica/base.py +++ b/sqlalchemy_vertica/base.py @@ -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, @@ -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, @@ -42,6 +47,9 @@ 'RAW': BLOB, 'BYTEA': BYTEA, 'BOOLEAN': BOOLEAN, + 'LONG VARBINARY': BLOB, + 'LONG VARCHAR': VARCHAR, + 'GEOMETRY': BLOB, } @@ -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 @@ -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 @@ -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