Skip to content

Commit 0c8715f

Browse files
Add transaction support to RedshiftClient
1 parent 2b70e38 commit 0c8715f

4 files changed

Lines changed: 71 additions & 1 deletion

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
# Changelog
22

3+
## v1.0.1 - 4/3/23
4+
- Add transaction support to RedshiftClient
5+
36
## v1.0.0 - 3/22/23
47
- Improve Oauth2ApiClient token refresh and method responses
58
- Create separate PostgreSQLClient and PostgreSQLPoolClient classes

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "nypl_py_utils"
7-
version = "1.0.0"
7+
version = "1.0.1"
88
authors = [
99
{ name="Aaron Friedman", email="aaronfriedman@nypl.org" },
1010
]

src/nypl_py_utils/classes/redshift_client.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,38 @@ def execute_query(self, query, dataframe=False):
7272
finally:
7373
cursor.close()
7474

75+
def execute_transaction(self, queries):
76+
"""
77+
Executes a series of queries within a single transaction against the
78+
given database connection. Assumes each of these queries is a write
79+
query and so does not return anything.
80+
81+
Parameters
82+
----------
83+
queries: list<str>
84+
A list of the queries to execute in order
85+
"""
86+
self.logger.info('Executing transaction against {} database'.format(
87+
self.database))
88+
try:
89+
cursor = self.conn.cursor()
90+
cursor.execute('BEGIN TRANSACTION;')
91+
for query in queries:
92+
self.logger.debug('Executing query {}'.format(query))
93+
cursor.execute(query)
94+
cursor.execute('END TRANSACTION;')
95+
self.conn.commit()
96+
except Exception as e:
97+
self.conn.rollback()
98+
self.logger.error(
99+
('Error executing {name} database transaction: {error}')
100+
.format(name=self.database, error=e))
101+
raise RedshiftClientError(
102+
('Error executing {name} database transaction: {error}')
103+
.format(name=self.database, error=e)) from None
104+
finally:
105+
cursor.close()
106+
75107
def close_connection(self):
76108
"""Closes the database connection"""
77109
self.logger.debug('Closing {} database connection'.format(

tests/test_redshift_client.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,41 @@ def test_execute_query_with_exception(
6262
test_instance.conn.rollback.assert_called_once()
6363
mock_cursor.close.assert_called_once()
6464

65+
def test_execute_transaction(self, mock_redshift_conn, test_instance,
66+
mocker):
67+
test_instance.connect()
68+
69+
mock_cursor = mocker.MagicMock()
70+
test_instance.conn.cursor.return_value = mock_cursor
71+
72+
test_instance.execute_transaction(['query 1', 'query 2'])
73+
mock_cursor.execute.assert_has_calls([
74+
mocker.call('BEGIN TRANSACTION;'),
75+
mocker.call('query 1'),
76+
mocker.call('query 2'),
77+
mocker.call('END TRANSACTION;')])
78+
test_instance.conn.commit.assert_called_once()
79+
mock_cursor.close.assert_called_once()
80+
81+
def test_execute_transaction_with_exception(
82+
self, mock_redshift_conn, test_instance, mocker):
83+
test_instance.connect()
84+
85+
mock_cursor = mocker.MagicMock()
86+
mock_cursor.execute.side_effect = [None, None, Exception()]
87+
test_instance.conn.cursor.return_value = mock_cursor
88+
89+
with pytest.raises(RedshiftClientError):
90+
test_instance.execute_transaction(['query 1', 'query 2'])
91+
92+
mock_cursor.execute.assert_has_calls([
93+
mocker.call('BEGIN TRANSACTION;'),
94+
mocker.call('query 1'),
95+
mocker.call('query 2')])
96+
test_instance.conn.commit.assert_not_called()
97+
test_instance.conn.rollback.assert_called_once()
98+
mock_cursor.close.assert_called_once()
99+
65100
def test_close_connection(self, mock_redshift_conn, test_instance):
66101
test_instance.connect()
67102
test_instance.close_connection()

0 commit comments

Comments
 (0)