Skip to content

Commit b586345

Browse files
committed
Add integration tests
1 parent ddcf11d commit b586345

File tree

4 files changed

+290
-24
lines changed

4 files changed

+290
-24
lines changed

src/firebolt/async_db/connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,7 +203,8 @@ async def cancel_async_query(self, token: str) -> None:
203203
async def _execute_query_impl(self, request: Request) -> Response:
204204
self._add_transaction_params(request)
205205
response = await self._client.send(request, stream=True)
206-
self._handle_transaction_updates(response.headers)
206+
if not self.autocommit:
207+
self._handle_transaction_updates(response.headers)
207208
return response
208209

209210
async def _begin_nolock(self, request: Request) -> None:

src/firebolt/db/connection.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -283,7 +283,8 @@ def close(self) -> None:
283283
def _execute_query_impl(self, request: Request) -> Response:
284284
self._add_transaction_params(request)
285285
response = self._client.send(request, stream=True)
286-
self._handle_transaction_updates(response.headers)
286+
if not self.autocommit:
287+
self._handle_transaction_updates(response.headers)
287288
return response
288289

289290
def _begin_nolock(self, request: Request) -> None:

tests/integration/dbapi/async/V2/test_queries_async.py

Lines changed: 144 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -823,7 +823,7 @@ async def test_transaction_rollback(
823823
async def test_transaction_cursor_isolation(
824824
connection: Connection, create_drop_test_table_setup_teardown_async: Callable
825825
) -> None:
826-
"""Test that one cursor can't see another's data until it commits."""
826+
"""Test that cursors share the same transaction state - no isolation between cursors."""
827827
table_name = create_drop_test_table_setup_teardown_async
828828
cursor1 = connection.cursor()
829829
cursor2 = connection.cursor()
@@ -832,25 +832,158 @@ async def test_transaction_cursor_isolation(
832832
result = await cursor1.execute("BEGIN TRANSACTION")
833833
assert result == 0, "BEGIN TRANSACTION should return 0 rows"
834834

835-
await cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'isolated_data')")
835+
await cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'shared_data')")
836836

837837
# Verify cursor1 can see its own uncommitted data
838838
await cursor1.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
839839
data1 = await cursor1.fetchall()
840840
assert len(data1) == 1, "Cursor1 should see its own uncommitted data"
841-
assert data1[0] == [1, "isolated_data"], "Cursor1 data should match inserted values"
841+
assert data1[0] == [1, "shared_data"], "Cursor1 data should match inserted values"
842842

843-
# Verify cursor2 cannot see cursor1's uncommitted data
843+
# Verify cursor2 CAN see cursor1's uncommitted data (no isolation between cursors)
844844
await cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
845845
data2 = await cursor2.fetchall()
846-
assert len(data2) == 0, "Cursor2 should not see cursor1's uncommitted data"
846+
assert (
847+
len(data2) == 1
848+
), "Cursor2 should see cursor1's uncommitted data (no isolation)"
849+
assert data2[0] == [1, "shared_data"], "Cursor2 should see the same data as cursor1"
847850

848-
# Commit the transaction in cursor1
849-
result = await cursor1.execute("COMMIT TRANSACTION")
851+
# Commit the transaction in cursor2 (affects both cursors)
852+
result = await cursor2.execute("COMMIT TRANSACTION")
850853
assert result == 0, "COMMIT TRANSACTION should return 0 rows"
851854

852-
# Now cursor2 should be able to see the committed data
855+
# Both cursors should still see the committed data
856+
await cursor1.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
857+
data1_after = await cursor1.fetchall()
858+
assert len(data1_after) == 1, "Cursor1 should see committed data"
859+
assert data1_after[0] == [1, "shared_data"], "Cursor1 should see the committed data"
860+
861+
862+
@mark.parametrize("autocommit_mode", ["implicit", "explicit"])
863+
async def test_autocommit_immediate_visibility(
864+
connection: Connection,
865+
autocommit_mode: str,
866+
create_drop_test_table_setup_teardown_async: Callable,
867+
) -> None:
868+
"""Test that statements are visible immediately with autocommit enabled (uses existing connection fixture)."""
869+
table_name = create_drop_test_table_setup_teardown_async
870+
cursor1 = connection.cursor()
871+
cursor2 = connection.cursor()
872+
873+
# Insert data with cursor1
874+
await cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'autocommit_data')")
875+
876+
# Immediately verify cursor2 can see the data (autocommit makes it visible)
853877
await cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
854-
data2 = await cursor2.fetchall()
855-
assert len(data2) == 1, "Cursor2 should see committed data after commit"
856-
assert data2[0] == [1, "isolated_data"], "Cursor2 should see the committed data"
878+
data = await cursor2.fetchall()
879+
assert (
880+
len(data) == 1
881+
), f"Data should be immediately visible with {autocommit_mode} autocommit"
882+
assert data[0] == [1, "autocommit_data"], "Data should match inserted values"
883+
884+
# Insert more data with cursor2
885+
await cursor2.execute(f"INSERT INTO \"{table_name}\" VALUES (2, 'more_data')")
886+
887+
# Verify cursor1 can immediately see cursor2's data
888+
await cursor1.execute(f'SELECT * FROM "{table_name}" ORDER BY id')
889+
all_data = await cursor1.fetchall()
890+
assert len(all_data) == 2, "All data should be immediately visible"
891+
assert all_data[0] == [1, "autocommit_data"], "First row should match"
892+
assert all_data[1] == [2, "more_data"], "Second row should match"
893+
894+
895+
# Not compatible with core
896+
@mark.parametrize("connection", ["remote"], indirect=True)
897+
async def test_begin_with_autocommit_on(
898+
connection: Connection, create_drop_test_table_setup_teardown_async: Callable
899+
) -> None:
900+
"""Test that BEGIN does not start a transaction when autocommit is enabled."""
901+
table_name = create_drop_test_table_setup_teardown_async
902+
903+
cursor = connection.cursor()
904+
# Test that data is immediately visible without explicit transaction (autocommit)
905+
await cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'autocommit_test')")
906+
907+
# Create a second cursor to verify data is visible immediately
908+
cursor2 = connection.cursor()
909+
await cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
910+
data = await cursor2.fetchall()
911+
assert len(data) == 1, "Data should be visible immediately with autocommit"
912+
assert data[0] == [1, "autocommit_test"], "Data should match inserted values"
913+
914+
# Now test with explicit BEGIN - this should be a no-op when autocommit is enabled
915+
result = await cursor.execute("BEGIN TRANSACTION")
916+
assert result == 0, "BEGIN TRANSACTION should return 0 rows"
917+
assert (
918+
not connection.in_transaction
919+
), "Transaction should not be started when autocommit is enabled"
920+
921+
await cursor.execute(
922+
f"INSERT INTO \"{table_name}\" VALUES (2, 'no_transaction_test')"
923+
)
924+
925+
# ROLLBACK should fail since no transaction was started
926+
with raises(Exception):
927+
await cursor.execute("ROLLBACK")
928+
929+
# The second insert should not be rolled back since it was committed immediately
930+
await cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 2')
931+
data = await cursor.fetchall()
932+
assert (
933+
len(data) == 1
934+
), "Data should remain committed since no transaction was started"
935+
assert data[0] == [2, "no_transaction_test"], "Data should match inserted values"
936+
937+
# Verify data is visible from another cursor (confirming it was committed)
938+
cursor2 = connection.cursor()
939+
await cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 2')
940+
data = await cursor2.fetchall()
941+
assert len(data) == 1, "Data should be visible from other cursors"
942+
assert data[0] == [2, "no_transaction_test"], "Data should match inserted values"
943+
944+
945+
async def test_connection_commit(
946+
connection: Connection, create_drop_test_table_setup_teardown_async: Callable
947+
) -> None:
948+
"""Test that connection.commit() works correctly."""
949+
table_name = create_drop_test_table_setup_teardown_async
950+
951+
cursor = connection.cursor()
952+
# Start a transaction
953+
await cursor.execute("BEGIN TRANSACTION")
954+
await cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'commit_test')")
955+
956+
# Call commit on connection level
957+
await connection.commit()
958+
959+
# Verify data is now visible in a new cursor
960+
cursor2 = connection.cursor()
961+
await cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
962+
data = await cursor2.fetchall()
963+
assert len(data) == 1, "Data should be visible after connection.commit()"
964+
assert data[0] == [1, "commit_test"], "Data should match inserted values"
965+
966+
967+
async def test_connection_rollback(
968+
connection: Connection, create_drop_test_table_setup_teardown_async: Callable
969+
) -> None:
970+
"""Test that connection.rollback() works correctly."""
971+
table_name = create_drop_test_table_setup_teardown_async
972+
973+
cursor = connection.cursor()
974+
# Start a transaction
975+
await cursor.execute("BEGIN TRANSACTION")
976+
await cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'rollback_test')")
977+
978+
# Verify data is visible within the transaction
979+
await cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
980+
data = await cursor.fetchall()
981+
assert len(data) == 1, "Data should be visible within transaction"
982+
983+
# Call rollback on connection level
984+
await connection.rollback()
985+
986+
# Verify data is no longer visible
987+
await cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
988+
data = await cursor.fetchall()
989+
assert len(data) == 0, "Data should be rolled back after connection.rollback()"

tests/integration/dbapi/sync/V2/test_queries.py

Lines changed: 142 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -824,7 +824,7 @@ def test_transaction_rollback(
824824
def test_transaction_cursor_isolation(
825825
connection: Connection, create_drop_test_table_setup_teardown: Callable
826826
) -> None:
827-
"""Test that one cursor can't see another's data until it commits."""
827+
"""Test that cursors share the same transaction state - no isolation between cursors."""
828828
table_name = create_drop_test_table_setup_teardown
829829
cursor1 = connection.cursor()
830830
cursor2 = connection.cursor()
@@ -833,25 +833,156 @@ def test_transaction_cursor_isolation(
833833
result = cursor1.execute("BEGIN TRANSACTION")
834834
assert result == 0, "BEGIN TRANSACTION should return 0 rows"
835835

836-
cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'isolated_data')")
836+
cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'shared_data')")
837837

838838
# Verify cursor1 can see its own uncommitted data
839839
cursor1.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
840840
data1 = cursor1.fetchall()
841841
assert len(data1) == 1, "Cursor1 should see its own uncommitted data"
842-
assert data1[0] == [1, "isolated_data"], "Cursor1 data should match inserted values"
842+
assert data1[0] == [1, "shared_data"], "Cursor1 data should match inserted values"
843843

844-
# Verify cursor2 cannot see cursor1's uncommitted data
844+
# Verify cursor2 CAN see cursor1's uncommitted data (no isolation between cursors)
845845
cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
846846
data2 = cursor2.fetchall()
847-
assert len(data2) == 0, "Cursor2 should not see cursor1's uncommitted data"
847+
assert (
848+
len(data2) == 1
849+
), "Cursor2 should see cursor1's uncommitted data (no isolation)"
850+
assert data2[0] == [1, "shared_data"], "Cursor2 should see the same data as cursor1"
848851

849-
# Commit the transaction in cursor1
850-
result = cursor1.execute("COMMIT TRANSACTION")
852+
# Commit the transaction in cursor2 (affects both cursors)
853+
result = cursor2.execute("COMMIT TRANSACTION")
851854
assert result == 0, "COMMIT TRANSACTION should return 0 rows"
852855

853-
# Now cursor2 should be able to see the committed data
856+
# Both cursors should still see the committed data
857+
cursor1.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
858+
data1_after = cursor1.fetchall()
859+
assert len(data1_after) == 1, "Cursor1 should see committed data"
860+
assert data1_after[0] == [1, "shared_data"], "Cursor1 should see the committed data"
861+
862+
863+
@mark.parametrize("autocommit_mode", ["implicit", "explicit"])
864+
def test_autocommit_immediate_visibility(
865+
connection: Connection,
866+
autocommit_mode: str,
867+
create_drop_test_table_setup_teardown: Callable,
868+
) -> None:
869+
"""Test that statements are visible immediately with autocommit enabled (uses existing connection fixture)."""
870+
table_name = create_drop_test_table_setup_teardown
871+
cursor1 = connection.cursor()
872+
cursor2 = connection.cursor()
873+
874+
# Insert data with cursor1
875+
cursor1.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'autocommit_data')")
876+
877+
# Immediately verify cursor2 can see the data (autocommit makes it visible)
854878
cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
855-
data2 = cursor2.fetchall()
856-
assert len(data2) == 1, "Cursor2 should see committed data after commit"
857-
assert data2[0] == [1, "isolated_data"], "Cursor2 should see the committed data"
879+
data = cursor2.fetchall()
880+
assert (
881+
len(data) == 1
882+
), f"Data should be immediately visible with {autocommit_mode} autocommit"
883+
assert data[0] == [1, "autocommit_data"], "Data should match inserted values"
884+
885+
# Insert more data with cursor2
886+
cursor2.execute(f"INSERT INTO \"{table_name}\" VALUES (2, 'more_data')")
887+
888+
# Verify cursor1 can immediately see cursor2's data
889+
cursor1.execute(f'SELECT * FROM "{table_name}" ORDER BY id')
890+
all_data = cursor1.fetchall()
891+
assert len(all_data) == 2, "All data should be immediately visible"
892+
assert all_data[0] == [1, "autocommit_data"], "First row should match"
893+
assert all_data[1] == [2, "more_data"], "Second row should match"
894+
895+
896+
# Not compatible with core
897+
@mark.parametrize("connection", ["remote"], indirect=True)
898+
def test_begin_with_autocommit_on(
899+
connection: Connection, create_drop_test_table_setup_teardown: Callable
900+
) -> None:
901+
"""Test that BEGIN does not start a transaction when autocommit is enabled."""
902+
table_name = create_drop_test_table_setup_teardown
903+
904+
cursor = connection.cursor()
905+
# Test that data is immediately visible without explicit transaction (autocommit)
906+
cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'autocommit_test')")
907+
908+
# Create a second cursor to verify data is visible immediately
909+
cursor2 = connection.cursor()
910+
cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
911+
data = cursor2.fetchall()
912+
assert len(data) == 1, "Data should be visible immediately with autocommit"
913+
assert data[0] == [1, "autocommit_test"], "Data should match inserted values"
914+
915+
# Now test with explicit BEGIN - this should be a no-op when autocommit is enabled
916+
result = cursor.execute("BEGIN TRANSACTION")
917+
assert result == 0, "BEGIN TRANSACTION should return 0 rows"
918+
assert (
919+
not connection.in_transaction
920+
), "Transaction should not be started when autocommit is enabled"
921+
922+
cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (2, 'no_transaction_test')")
923+
924+
# ROLLBACK should fail since no transaction was started
925+
with raises(Exception):
926+
cursor.execute("ROLLBACK")
927+
928+
# The second insert should not be rolled back since it was committed immediately
929+
cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 2')
930+
data = cursor.fetchall()
931+
assert (
932+
len(data) == 1
933+
), "Data should remain committed since no transaction was started"
934+
assert data[0] == [2, "no_transaction_test"], "Data should match inserted values"
935+
936+
# Verify data is visible from another cursor (confirming it was committed)
937+
cursor2 = connection.cursor()
938+
cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 2')
939+
data = cursor2.fetchall()
940+
assert len(data) == 1, "Data should be visible from other cursors"
941+
assert data[0] == [2, "no_transaction_test"], "Data should match inserted values"
942+
943+
944+
def test_connection_commit(
945+
connection: Connection, create_drop_test_table_setup_teardown: Callable
946+
) -> None:
947+
"""Test that connection.commit() works correctly."""
948+
table_name = create_drop_test_table_setup_teardown
949+
950+
cursor = connection.cursor()
951+
# Start a transaction
952+
cursor.execute("BEGIN TRANSACTION")
953+
cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'commit_test')")
954+
955+
# Call commit on connection level
956+
connection.commit()
957+
958+
# Verify data is now visible in a new cursor
959+
cursor2 = connection.cursor()
960+
cursor2.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
961+
data = cursor2.fetchall()
962+
assert len(data) == 1, "Data should be visible after connection.commit()"
963+
assert data[0] == [1, "commit_test"], "Data should match inserted values"
964+
965+
966+
def test_connection_rollback(
967+
connection: Connection, create_drop_test_table_setup_teardown: Callable
968+
) -> None:
969+
"""Test that connection.rollback() works correctly."""
970+
table_name = create_drop_test_table_setup_teardown
971+
972+
cursor = connection.cursor()
973+
# Start a transaction
974+
cursor.execute("BEGIN TRANSACTION")
975+
cursor.execute(f"INSERT INTO \"{table_name}\" VALUES (1, 'rollback_test')")
976+
977+
# Verify data is visible within the transaction
978+
cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
979+
data = cursor.fetchall()
980+
assert len(data) == 1, "Data should be visible within transaction"
981+
982+
# Call rollback on connection level
983+
connection.rollback()
984+
985+
# Verify data is no longer visible
986+
cursor.execute(f'SELECT * FROM "{table_name}" WHERE id = 1')
987+
data = cursor.fetchall()
988+
assert len(data) == 0, "Data should be rolled back after connection.rollback()"

0 commit comments

Comments
 (0)