Skip to content

Commit 5f17e1a

Browse files
Merge pull request #6316 from jorisvandenbossche/sql-postgresql-tests
TEST: add basic postgresql tests
2 parents 2a9e994 + fcad2a5 commit 5f17e1a

File tree

4 files changed

+123
-42
lines changed

4 files changed

+123
-42
lines changed

.travis.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ install:
7373

7474
before_script:
7575
- mysql -e 'create database pandas_nosetest;'
76+
- psql -c 'create database pandas_nosetest;' -U postgres
7677

7778
script:
7879
- echo "script"

ci/requirements-2.7.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ beautifulsoup4==4.2.1
1919
statsmodels==0.5.0
2020
bigquery==2.0.17
2121
sqlalchemy==0.8.1
22+
psycopg2==2.5.2

ci/requirements-3.3.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ scipy==0.12.0
1515
beautifulsoup4==4.2.1
1616
statsmodels==0.4.3
1717
sqlalchemy==0.9.1
18+
psycopg2==2.5.2

pandas/io/tests/test_sql.py

Lines changed: 120 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,19 @@
3636
`PetalLength` DOUBLE,
3737
`PetalWidth` DOUBLE,
3838
`Name` VARCHAR(200)
39+
)""",
40+
'postgresql': """CREATE TABLE iris (
41+
"SepalLength" DOUBLE PRECISION,
42+
"SepalWidth" DOUBLE PRECISION,
43+
"PetalLength" DOUBLE PRECISION,
44+
"PetalWidth" DOUBLE PRECISION,
45+
"Name" VARCHAR(200)
3946
)"""
4047
},
4148
'insert_iris': {
4249
'sqlite': """INSERT INTO iris VALUES(?, ?, ?, ?, ?)""",
43-
'mysql': """INSERT INTO iris VALUES(%s, %s, %s, %s, "%s");"""
50+
'mysql': """INSERT INTO iris VALUES(%s, %s, %s, %s, "%s");""",
51+
'postgresql': """INSERT INTO iris VALUES(%s, %s, %s, %s, %s);"""
4452
},
4553
'create_test_types': {
4654
'sqlite': """CREATE TABLE types_test_data (
@@ -62,6 +70,16 @@
6270
`BoolCol` BOOLEAN,
6371
`IntColWithNull` INTEGER,
6472
`BoolColWithNull` BOOLEAN
73+
)""",
74+
'postgresql': """CREATE TABLE types_test_data (
75+
"TextCol" TEXT,
76+
"DateCol" TIMESTAMP,
77+
"IntDateCol" INTEGER,
78+
"FloatCol" DOUBLE PRECISION,
79+
"IntCol" INTEGER,
80+
"BoolCol" BOOLEAN,
81+
"IntColWithNull" INTEGER,
82+
"BoolColWithNull" BOOLEAN
6583
)"""
6684
},
6785
'insert_test_types': {
@@ -72,6 +90,10 @@
7290
'mysql': """
7391
INSERT INTO types_test_data
7492
VALUES("%s", %s, %s, %s, %s, %s, %s, %s)
93+
""",
94+
'postgresql': """
95+
INSERT INTO types_test_data
96+
VALUES(%s, %s, %s, %s, %s, %s, %s, %s)
7597
"""
7698
}
7799
}
@@ -403,29 +425,12 @@ def test_date_and_index(self):
403425
"IntDateCol loaded with incorrect type")
404426

405427

406-
class TestSQLAlchemy(PandasSQLTest):
407-
408-
'''
409-
Test the sqlalchemy backend against an in-memory sqlite database.
428+
class _TestSQLAlchemy(PandasSQLTest):
429+
"""
430+
Base class for testing the sqlalchemy backend. Subclasses for specific
431+
database types are created below.
410432
Assume that sqlalchemy takes case of the DB specifics
411-
'''
412-
flavor = 'sqlite'
413-
414-
def connect(self):
415-
return sqlalchemy.create_engine('sqlite:///:memory:')
416-
417-
def setUp(self):
418-
# Skip this test if SQLAlchemy not available
419-
if not SQLALCHEMY_INSTALLED:
420-
raise nose.SkipTest('SQLAlchemy not installed')
421-
422-
self.conn = self.connect()
423-
self.pandasSQL = sql.PandasSQLAlchemy(self.conn)
424-
425-
self._load_iris_data()
426-
self._load_raw_sql()
427-
428-
self._load_test1_data()
433+
"""
429434

430435
def test_read_sql(self):
431436
self._read_sql_iris()
@@ -491,32 +496,31 @@ def test_read_table_absent(self):
491496
ValueError, sql.read_table, "this_doesnt_exist", con=self.conn)
492497

493498
def test_default_type_convertion(self):
494-
""" Test default type conversion"""
495499
df = sql.read_table("types_test_data", self.conn)
496-
self.assertTrue(
497-
issubclass(df.FloatCol.dtype.type, np.floating), "FloatCol loaded with incorrect type")
498-
self.assertTrue(
499-
issubclass(df.IntCol.dtype.type, np.integer), "IntCol loaded with incorrect type")
500-
self.assertTrue(
501-
issubclass(df.BoolCol.dtype.type, np.integer), "BoolCol loaded with incorrect type")
500+
501+
self.assertTrue(issubclass(df.FloatCol.dtype.type, np.floating),
502+
"FloatCol loaded with incorrect type")
503+
self.assertTrue(issubclass(df.IntCol.dtype.type, np.integer),
504+
"IntCol loaded with incorrect type")
505+
self.assertTrue(issubclass(df.BoolCol.dtype.type, np.bool_),
506+
"BoolCol loaded with incorrect type")
502507

503508
# Int column with NA values stays as float
504509
self.assertTrue(issubclass(df.IntColWithNull.dtype.type, np.floating),
505510
"IntColWithNull loaded with incorrect type")
506-
# Non-native Bool column with NA values stays as float
507-
self.assertTrue(
508-
issubclass(df.BoolColWithNull.dtype.type, np.floating), "BoolCol loaded with incorrect type")
511+
# Bool column with NA values becomes object
512+
self.assertTrue(issubclass(df.BoolColWithNull.dtype.type, np.object),
513+
"BoolColWithNull loaded with incorrect type")
509514

510515
def test_default_date_load(self):
511516
df = sql.read_table("types_test_data", self.conn)
512517

513518
# IMPORTANT - sqlite has no native date type, so shouldn't parse, but
514519
# MySQL SHOULD be converted.
515-
self.assertFalse(
520+
self.assertTrue(
516521
issubclass(df.DateCol.dtype.type, np.datetime64), "DateCol loaded with incorrect type")
517522

518523
def test_date_parsing(self):
519-
""" Test date parsing """
520524
# No Parsing
521525
df = sql.read_table("types_test_data", self.conn)
522526

@@ -551,6 +555,54 @@ def test_date_parsing(self):
551555
"IntDateCol loaded with incorrect type")
552556

553557

558+
class TestSQLAlchemy(_TestSQLAlchemy):
559+
"""
560+
Test the sqlalchemy backend against an in-memory sqlite database.
561+
"""
562+
flavor = 'sqlite'
563+
564+
def connect(self):
565+
return sqlalchemy.create_engine('sqlite:///:memory:')
566+
567+
def setUp(self):
568+
# Skip this test if SQLAlchemy not available
569+
if not SQLALCHEMY_INSTALLED:
570+
raise nose.SkipTest('SQLAlchemy not installed')
571+
572+
self.conn = self.connect()
573+
self.pandasSQL = sql.PandasSQLAlchemy(self.conn)
574+
575+
self._load_iris_data()
576+
self._load_raw_sql()
577+
578+
self._load_test1_data()
579+
580+
def test_default_type_convertion(self):
581+
df = sql.read_table("types_test_data", self.conn)
582+
583+
self.assertTrue(issubclass(df.FloatCol.dtype.type, np.floating),
584+
"FloatCol loaded with incorrect type")
585+
self.assertTrue(issubclass(df.IntCol.dtype.type, np.integer),
586+
"IntCol loaded with incorrect type")
587+
# sqlite has no boolean type, so integer type is returned
588+
self.assertTrue(issubclass(df.BoolCol.dtype.type, np.integer),
589+
"BoolCol loaded with incorrect type")
590+
591+
# Int column with NA values stays as float
592+
self.assertTrue(issubclass(df.IntColWithNull.dtype.type, np.floating),
593+
"IntColWithNull loaded with incorrect type")
594+
# Non-native Bool column with NA values stays as float
595+
self.assertTrue(issubclass(df.BoolColWithNull.dtype.type, np.floating),
596+
"BoolColWithNull loaded with incorrect type")
597+
598+
def test_default_date_load(self):
599+
df = sql.read_table("types_test_data", self.conn)
600+
601+
# IMPORTANT - sqlite has no native date type, so shouldn't parse, but
602+
self.assertFalse(issubclass(df.DateCol.dtype.type, np.datetime64),
603+
"DateCol loaded with incorrect type")
604+
605+
554606
# --- Test SQLITE fallback
555607
class TestSQLite(PandasSQLTest):
556608

@@ -660,7 +712,7 @@ def tearDown(self):
660712
self.conn.close()
661713

662714

663-
class TestMySQLAlchemy(TestSQLAlchemy):
715+
class TestMySQLAlchemy(_TestSQLAlchemy):
664716
flavor = 'mysql'
665717

666718
def connect(self):
@@ -691,13 +743,39 @@ def tearDown(self):
691743
for table in c.fetchall():
692744
self.conn.execute('DROP TABLE %s' % table[0])
693745

694-
def test_default_date_load(self):
695-
df = sql.read_table("types_test_data", self.conn)
696746

697-
# IMPORTANT - sqlite has no native date type, so shouldn't parse,
698-
# but MySQL SHOULD be converted.
699-
self.assertTrue(
700-
issubclass(df.DateCol.dtype.type, np.datetime64), "DateCol loaded with incorrect type")
747+
class TestPostgreSQLAlchemy(_TestSQLAlchemy):
748+
flavor = 'postgresql'
749+
750+
def connect(self):
751+
return sqlalchemy.create_engine(
752+
'postgresql+{driver}://postgres@localhost/pandas_nosetest'.format(driver=self.driver))
753+
754+
def setUp(self):
755+
if not SQLALCHEMY_INSTALLED:
756+
raise nose.SkipTest('SQLAlchemy not installed')
757+
758+
try:
759+
import psycopg2
760+
self.driver = 'psycopg2'
761+
762+
except ImportError:
763+
raise nose.SkipTest
764+
765+
self.conn = self.connect()
766+
self.pandasSQL = sql.PandasSQLAlchemy(self.conn)
767+
768+
self._load_iris_data()
769+
self._load_raw_sql()
770+
771+
self._load_test1_data()
772+
773+
def tearDown(self):
774+
c = self.conn.execute(
775+
"SELECT table_name FROM information_schema.tables"
776+
" WHERE table_schema = 'public'")
777+
for table in c.fetchall():
778+
self.conn.execute("DROP TABLE %s" % table[0])
701779

702780
if __name__ == '__main__':
703781
nose.runmodule(argv=[__file__, '-vvs', '-x', '--pdb', '--pdb-failure'],

0 commit comments

Comments
 (0)