Skip to content

Commit 175c5a6

Browse files
author
Robert Engel
committed
More options for UnloadCommand and more resilient test cases
1 parent 1ecf9ed commit 175c5a6

File tree

5 files changed

+51
-30
lines changed

5 files changed

+51
-30
lines changed

redshift_sqlalchemy/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
__version__ = '0.5.0a'
1+
__version__ = '0.5.1a'
22

33
from sqlalchemy.dialects import registry
44

redshift_sqlalchemy/dialect.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,7 @@ def __init__(self, select, unload_location, access_key, secret_key, session_toke
195195
'OFF' the result will write to one (1) file up to 6.2GB before
196196
splitting
197197
add_quotes: Boolean value for ADDQUOTES; defaults to True
198+
null_as: optional string that represents a null value in unload output
198199
delimiter - File delimiter. Defaults to ','
199200
'''
200201
self.select = select
@@ -215,6 +216,7 @@ def visit_unload_from_select(element, compiler, **kw):
215216
CREDENTIALS 'aws_access_key_id=%(access_key)s;aws_secret_access_key=%(secret_key)s%(session_token)s'
216217
DELIMITER '%(delimiter)s'
217218
%(add_quotes)s
219+
%(null_as)s
218220
ALLOWOVERWRITE
219221
PARALLEL %(parallel)s;
220222
""" % \
@@ -224,6 +226,7 @@ def visit_unload_from_select(element, compiler, **kw):
224226
'secret_key': element.secret_key,
225227
'session_token': ';token=%s' % element.session_token if element.session_token else '',
226228
'add_quotes': 'ADDQUOTES' if bool(element.options.get('add_quotes', True)) else '',
229+
'null_as': ("NULL '%s'" % element.options.get('null_as')) if element.options.get('null_as') else '',
227230
'delimiter': element.options.get('delimiter', ','),
228231
'parallel': element.options.get('parallel', 'ON')}
229232

@@ -245,7 +248,7 @@ def __init__(self, schema_name, table_name, data_location, access_key, secret_ke
245248
options - Set of optional parameters to modify the COPY sql
246249
delimiter - File delimiter; defaults to ','
247250
ignore_header - Integer value of number of lines to skip at the start of each file
248-
null - String value denoting what to interpret as a NULL value from the file; defaults to '---'
251+
null - Optional string value denoting what to interpret as a NULL value from the file
249252
empty_as_null - Boolean value denoting whether to load VARCHAR fields with
250253
empty values as NULL instead of empty string; defaults to True
251254
blanks_as_null - Boolean value denoting whether to load VARCHAR fields with
@@ -271,7 +274,7 @@ def visit_copy_command(element, compiler, **kw):
271274
TRUNCATECOLUMNS
272275
DELIMITER '%(delimiter)s'
273276
IGNOREHEADER %(ignore_header)s
274-
NULL '%(null)s'
277+
%(null)s
275278
%(empty_as_null)s
276279
%(blanks_as_null)s;
277280
""" % \
@@ -281,7 +284,7 @@ def visit_copy_command(element, compiler, **kw):
281284
'access_key': element.access_key,
282285
'secret_key': element.secret_key,
283286
'session_token': ';token=%s' % element.session_token if element.session_token else '',
284-
'null': element.options.get('null', '---'),
287+
'null': ("NULL '%s'" % element.options.get('null')) if element.options.get('null') else '',
285288
'delimiter': element.options.get('delimiter', ','),
286289
'ignore_header': element.options.get('ignore_header', 0),
287290
'empty_as_null': 'EMPTYASNULL' if bool(element.options.get('empty_as_null', True)) else '',

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ def run_tests(self):
1010

1111
setup(
1212
name='redshift-sqlalchemy',
13-
version='0.5.0a',
13+
version='0.5.1a',
1414
description='Amazon Redshift Dialect for sqlalchemy',
1515
long_description=open("README.rst").read(),
1616
author='Matt George',

tests/test_copy_command.py

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
11
from unittest import TestCase
22
from redshift_sqlalchemy.dialect import CopyCommand
3+
import re
34

45

56
class TestCopyCommand(TestCase):
7+
68
def setUp(self):
79
pass
810

911
def test_basic_copy_case(self):
10-
''' Tests that the simplest type of CopyCommand works
1112
'''
12-
expected_result = "COPY schema1.t1 FROM 's3://mybucket/data/listing/'\n" \
13-
" CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies'\n" \
14-
" CSV\n TRUNCATECOLUMNS\n DELIMITER ','\n" \
15-
" IGNOREHEADER 0\n NULL '---'\n EMPTYASNULL\n" \
16-
" BLANKSASNULL;"
17-
insert = CopyCommand('schema1', 't1', 's3://mybucket/data/listing/', 'cookies', 'cookies')
18-
self.assertEqual(expected_result, str(insert).strip())
13+
Tests that the simplest type of CopyCommand works
14+
'''
15+
expected_result = re.sub(r'\s+', ' ',
16+
"COPY schema1.t1 FROM 's3://mybucket/data/listing/' "
17+
"CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies' "
18+
"CSV TRUNCATECOLUMNS DELIMITER ',' IGNOREHEADER 0 EMPTYASNULL BLANKSASNULL;").strip()
19+
copy = CopyCommand('schema1', 't1', 's3://mybucket/data/listing/', 'cookies', 'cookies')
20+
21+
copy_str = re.sub(r'\s+', ' ', str(copy)).strip()
22+
23+
self.assertEqual(expected_result, copy_str)

tests/test_unload_from_select.py

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,33 +2,46 @@
22
from sqlalchemy import Table, Column, Integer, String, MetaData
33
from sqlalchemy.sql import select, func
44
from redshift_sqlalchemy.dialect import UnloadFromSelect
5+
import re
56

67

78
class TestUnloadFromSelect(TestCase):
9+
810
def setUp(self):
9-
''' Sets up a table and associate meta data for the test queries to build against
11+
'''
12+
Sets up a table and associate meta data for the test queries to build against
1013
'''
1114
self.metadata = MetaData()
1215
self.t1 = Table('t1', self.metadata, Column('id', Integer, primary_key=True), Column('name', String))
1316

1417
def test_basic_unload_case(self):
15-
''' Tests that the simplest type of UnloadFromSelect works
1618
'''
17-
expected_result = "UNLOAD ('SELECT count(t1.id) AS count_1 \nFROM t1') TO 's3://bucket/key'\n " \
18-
"CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies;token=cookies'\n" \
19-
" DELIMITER ','\n ADDQUOTES\n ALLOWOVERWRITE\n " \
20-
" PARALLEL ON;"
21-
insert = UnloadFromSelect(select([func.count(self.t1.c.id)]), 's3://bucket/key', 'cookies', 'cookies',
19+
Tests that the simplest type of UnloadFromSelect works
20+
'''
21+
expected_result = re.sub(r'\s+', ' ',
22+
"UNLOAD ('SELECT count(t1.id) AS count_1 \nFROM t1') TO 's3://bucket/key' "
23+
"CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies;token=cookies' "
24+
"DELIMITER ',' ADDQUOTES ALLOWOVERWRITE PARALLEL ON;").strip()
25+
26+
unload = UnloadFromSelect(select([func.count(self.t1.c.id)]), 's3://bucket/key', 'cookies', 'cookies',
2227
'cookies')
23-
self.assertEqual(expected_result, str(insert).strip())
2428

25-
def test_parallel_off_unload_case(self):
26-
''' Tests that UnloadFromSelect handles parallel being set to off
29+
unload_str = re.sub(r'\s+', ' ', str(unload)).strip()
30+
31+
self.assertEqual(expected_result, unload_str)
32+
33+
def test_unload_with_options(self):
34+
'''
35+
Tests that UnloadFromSelect handles options correctly
2736
'''
28-
expected_result = "UNLOAD ('SELECT count(t1.id) AS count_1 \nFROM t1') TO 's3://bucket/key'\n " \
29-
"CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies;token=cookies'\n" \
30-
" DELIMITER ','\n ADDQUOTES\n ALLOWOVERWRITE\n " \
31-
" PARALLEL OFF;"
32-
insert = UnloadFromSelect(select([func.count(self.t1.c.id)]), 's3://bucket/key', 'cookies', 'cookies',
33-
'cookies', {'parallel': 'OFF'})
34-
self.assertEqual(expected_result, str(insert).strip())
37+
expected_result = re.sub(r'\s+', ' ',
38+
"UNLOAD ('SELECT count(t1.id) AS count_1 \nFROM t1') TO 's3://bucket/key' "
39+
"CREDENTIALS 'aws_access_key_id=cookies;aws_secret_access_key=cookies;token=cookies' "
40+
"DELIMITER ',' ADDQUOTES NULL '---' ALLOWOVERWRITE PARALLEL OFF;").strip()
41+
42+
unload = UnloadFromSelect(select([func.count(self.t1.c.id)]), 's3://bucket/key', 'cookies', 'cookies',
43+
'cookies', {'parallel': 'OFF', 'null_as': '---'})
44+
45+
unload_str = re.sub(r'\s+', ' ', str(unload)).strip()
46+
47+
self.assertEqual(expected_result, unload_str)

0 commit comments

Comments
 (0)