Repository: blaze/blaze

Issue:
Ensure that SQL data object and resource function support the use of database names
This is probably related to https://github.com/ContinuumIO/blaze/pull/687



Discussion:

Resolving patch:
diff --git a/blaze/data/sql.py b/blaze/data/sql.py
--- a/blaze/data/sql.py
+++ b/blaze/data/sql.py
@@ -151,6 +151,7 @@ class SQL(DataDescriptor):
     >>> dd.extend([('Alice', 100), ('Bob', 200)])
 
     Select all from table
+
     >>> list(dd) # doctest: +SKIP
     [('Alice', 100), ('Bob', 200)]
 
@@ -168,6 +169,8 @@ class SQL(DataDescriptor):
         or SQLAlchemy engine
     table : string
         The name of the table
+    schema_name : string (optional)
+        The name of the SQL database schema (not to be confused with the dshape)
     schema : string, list of Columns
         The datashape/schema of the database
         Possibly a list of SQLAlchemy columns
@@ -180,17 +183,27 @@ def remote(self):
     def persistent(self):
         return self.engine.url != 'sqlite:///:memory:'
 
-    def __init__(self, engine, tablename, primary_key='', schema=None, **kwargs):
+    def __init__(self, engine, tablename, primary_key='', schema=None,
+            schema_name=None, **kwargs):
         if isinstance(engine, _strtypes):
             engine = sql.create_engine(engine)
         self.engine = engine
-        self.tablename = tablename
+        if not schema_name and '.' in tablename:
+            self.schema_name = tablename.rsplit('.', 1)[0] if ('.' in tablename) else None
+            self.tablename = tablename.split('.')[-1]
+        else:
+            self.schema_name = schema_name
+            self.tablename = tablename
         self.dbtype = engine.url.drivername
         metadata = sql.MetaData()
 
-        if engine.has_table(tablename):
-            metadata.reflect(engine)
-            table = metadata.tables[tablename]
+        if engine.has_table(self.tablename, schema=self.schema_name):
+            metadata.reflect(engine, schema=self.schema_name)
+            if self.schema_name:
+                name = self.schema_name + '.' + self.tablename
+            else:
+                name = self.tablename
+            table = metadata.tables[name]
             engine_schema = discover(table).subshape[0]
             # if schema and dshape(schema) != engine_schema:
                 # raise ValueError("Mismatched schemas:\n"
@@ -203,14 +216,23 @@ def __init__(self, engine, tablename, primary_key='', schema=None, **kwargs):
             for column in columns:
                 if column.name == primary_key:
                     column.primary_key = True
-            table = sql.Table(tablename, metadata, *columns)
+            table = sql.Table(tablename, metadata, *columns, schema=self.schema_name)
         else:
             raise ValueError('Must provide schema or point to valid table. '
                              'Table %s does not exist' % tablename)
 
         self._schema = datashape.dshape(schema)
         self.table = table
-        metadata.create_all(engine)
+
+        try:
+            metadata.create_all(engine)
+        except sqlalchemy.exc.ProgrammingError:  # Maybe db does not exist
+            if not self.schema_name:
+                raise
+            # Create schema
+            statement = sqlalchemy.schema.CreateSchema(self.schema_name)
+            engine.execute(statement)
+            metadata.create_all(engine)
 
     def _iter(self):
         with self.engine.connect() as conn:


Test patch:
diff --git a/blaze/data/tests/test_postgres.py b/blaze/data/tests/test_postgres.py
new file mode 100644
--- /dev/null
+++ b/blaze/data/tests/test_postgres.py
@@ -0,0 +1,87 @@
+from __future__ import absolute_import, division, print_function
+
+import pytest
+
+psycopg2 = pytest.importorskip('psycopg2')
+import subprocess
+ps = subprocess.Popen("ps aux | grep postgres",shell=True, stdout=subprocess.PIPE)
+output = ps.stdout.read()
+num_processes = len(output.splitlines())
+pytestmark = pytest.mark.skipif(num_processes < 6, reason="No Postgres Installation")
+
+from blaze import SQL, into, resource
+import sqlalchemy
+from contextlib import contextmanager
+
+data = [('Alice', 1), ('Bob', 2), ('Charlie', 3)]
+
+url = 'postgresql://localhost/postgres'
+# url = 'postgresql://postgres:postgres@localhost/postgres'
+engine = sqlalchemy.create_engine(url)
+
+
+@contextmanager
+def existing_schema(name):
+    create = sqlalchemy.schema.CreateSchema(name)
+    try:
+        engine.execute(create)
+    except:
+        pass
+
+    try:
+        yield
+    finally:
+        metadata = sqlalchemy.MetaData()
+        metadata.reflect(engine, schema=name)
+        for t in metadata.tables.values():
+            t.drop(bind=engine)
+
+        drop = sqlalchemy.schema.DropSchema(name)
+        engine.execute(drop)
+
+
+@contextmanager
+def non_existing_schema(name):
+    try:
+        yield
+    finally:
+        metadata = sqlalchemy.MetaData()
+        metadata.reflect(engine, schema=name)
+        for t in metadata.tables.values():
+            t.drop(bind=engine)
+
+        try:
+            drop = sqlalchemy.schema.DropSchema(name)
+            engine.execute(drop)
+        except:
+            pass
+
+
+def test_sql_schema_behavior():
+    with existing_schema('myschema'):
+        sql = SQL(url, 'accounts', schema_name='myschema', schema='{name: string, value: int}')
+        into(sql, data)
+        assert engine.has_table('accounts', schema='myschema')
+
+        sql2 = SQL(url, 'accounts', schema_name='myschema')
+        assert list(sql2) == data
+
+        sql3 = SQL(url, 'myschema.accounts')
+        assert list(sql2) == data
+
+
+def test_sql_new_schema():
+    with non_existing_schema('myschema2'):
+        sql = SQL(url, 'accounts', schema_name='myschema2', schema='{name: string, value: int}')
+        into(sql, data)
+        assert engine.has_table('accounts', schema='myschema2')
+
+        sql2 = SQL(url, 'accounts', schema_name='myschema2')
+        assert list(sql2) == data
+
+
+def test_resource_specifying_database_name():
+    with existing_schema('myschema'):
+        sql = resource(url + '::myschema.accounts', schema='{name: string, value: int}')
+        assert isinstance(sql, SQL)
+        assert sql.table.schema == 'myschema'
diff --git a/blaze/data/tests/test_sql.py b/blaze/data/tests/test_sql.py
--- a/blaze/data/tests/test_sql.py
+++ b/blaze/data/tests/test_sql.py
@@ -7,7 +7,7 @@
 import gzip
 
 from blaze.data.sql import SQL, discover, dshape_to_alchemy
-from blaze.utils import raises, filetext
+from blaze.utils import raises, filetext, tmpfile
 from datashape import dshape
 import datashape
 from blaze.compatibility import PY2