Skip to content

Commit b811eeb

Browse files
Apply changes from #2388 to async tests
1 parent 72cc2a7 commit b811eeb

File tree

3 files changed

+134
-52
lines changed

3 files changed

+134
-52
lines changed

test/integ/aio/test_arrow_result_async.py

Lines changed: 38 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -834,35 +834,46 @@ async def test_select_vector(conn_cnx, is_public_test):
834834

835835
@pytest.mark.asyncio
836836
async def test_select_time(conn_cnx):
837-
for scale in range(10):
838-
await select_time_with_scale(conn_cnx, scale)
839-
840-
841-
async def select_time_with_scale(conn_cnx, scale):
837+
# Test key scales and meaningful cases in a single table operation
838+
# Cover: no fractional seconds, milliseconds, microseconds, nanoseconds
839+
scales = [0, 3, 6, 9] # Key precision levels
842840
cases = [
843-
"00:01:23",
844-
"00:01:23.1",
845-
"00:01:23.12",
846-
"00:01:23.123",
847-
"00:01:23.1234",
848-
"00:01:23.12345",
849-
"00:01:23.123456",
850-
"00:01:23.1234567",
851-
"00:01:23.12345678",
852-
"00:01:23.123456789",
841+
"00:01:23", # Basic time
842+
"00:01:23.123456789", # Max precision
843+
"23:59:59.999999999", # Edge case - max time with max precision
844+
"00:00:00.000000001", # Edge case - min time with min precision
853845
]
854-
table = "test_arrow_time"
855-
column = f"(a time({scale}))"
856-
values = (
857-
"(-1, NULL), ("
858-
+ "),(".join([f"{i}, '{c}'" for i, c in enumerate(cases)])
859-
+ f"), ({len(cases)}, NULL)"
860-
)
861-
await init(conn_cnx, table, column, values)
862-
sql_text = f"select a from {table} order by s"
863-
row_count = len(cases) + 2
864-
col_count = 1
865-
await iterate_over_test_chunk("time", conn_cnx, sql_text, row_count, col_count)
846+
847+
table = "test_arrow_time_scales"
848+
849+
# Create columns for selected scales only (init function will add 's number' automatically)
850+
columns = ", ".join([f"a{i} time({i})" for i in scales])
851+
column_def = f"({columns})"
852+
853+
# Create values for selected scales - each case tests all scales simultaneously
854+
value_rows = []
855+
for i, case in enumerate(cases):
856+
# Each row has the same time value for all scale columns
857+
time_values = ", ".join([f"'{case}'" for _ in scales])
858+
value_rows.append(f"({i}, {time_values})")
859+
860+
# Add NULL rows
861+
null_values = ", ".join(["NULL" for _ in scales])
862+
value_rows.append(f"(-1, {null_values})")
863+
value_rows.append(f"({len(cases)}, {null_values})")
864+
865+
values = ", ".join(value_rows)
866+
867+
# Single table creation and test
868+
await init(conn_cnx, table, column_def, values)
869+
870+
# Test each scale column
871+
for scale in scales:
872+
sql_text = f"select a{scale} from {table} order by s"
873+
row_count = len(cases) + 2
874+
col_count = 1
875+
await iterate_over_test_chunk("time", conn_cnx, sql_text, row_count, col_count)
876+
866877
await finish(conn_cnx, table)
867878

868879

test/integ/aio/test_dbapi_async.py

Lines changed: 61 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -714,15 +714,67 @@ async def test_escape(conn_local):
714714
async with conn_local() as con:
715715
cur = con.cursor()
716716
await executeDDL1(cur)
717-
for i in teststrings:
718-
args = {"dbapi_ddl2": i}
719-
await cur.execute("insert into %s values (%%(dbapi_ddl2)s)" % TABLE1, args)
720-
await cur.execute("select * from %s" % TABLE1)
721-
row = await cur.fetchone()
722-
await cur.execute("delete from %s where name=%%s" % TABLE1, i)
723-
assert (
724-
i == row[0]
725-
), f"newline not properly converted, got {row[0]}, should be {i}"
717+
718+
# Test 1: Batch INSERT with dictionary parameters (executemany)
719+
# This tests the same dictionary parameter binding as the original
720+
batch_args = [{"dbapi_ddl2": test_string} for test_string in teststrings]
721+
await cur.executemany(
722+
"insert into %s values (%%(dbapi_ddl2)s)" % TABLE1, batch_args
723+
)
724+
725+
# Test 2: Batch SELECT with no parameters
726+
# This tests the same SELECT functionality as the original
727+
await cur.execute("select name from %s" % TABLE1)
728+
rows = await cur.fetchall()
729+
730+
# Verify each test string was properly escaped/handled
731+
assert len(rows) == len(
732+
teststrings
733+
), f"Expected {len(teststrings)} rows, got {len(rows)}"
734+
735+
# Extract actual strings from result set
736+
actual_strings = {row[0] for row in rows} # Use set to ignore order
737+
expected_strings = set(teststrings)
738+
739+
# Verify all expected strings are present
740+
missing_strings = expected_strings - actual_strings
741+
extra_strings = actual_strings - expected_strings
742+
743+
assert len(missing_strings) == 0, f"Missing strings: {missing_strings}"
744+
assert len(extra_strings) == 0, f"Extra strings: {extra_strings}"
745+
assert actual_strings == expected_strings, "String sets don't match"
746+
747+
# Test 3: DELETE with positional parameters (batched for efficiency)
748+
# This maintains the same DELETE parameter binding test as the original
749+
# We test a representative subset to maintain coverage while being efficient
750+
critical_test_strings = [
751+
teststrings[0], # Basic newline: "abc\ndef"
752+
teststrings[5], # Double quote: 'abc"def'
753+
teststrings[7], # Single quote: "abc'def"
754+
teststrings[13], # Tab: "abc\tdef"
755+
teststrings[16], # Backslash-x: "\\x"
756+
]
757+
758+
# Batch DELETE with positional parameters using executemany
759+
# This tests the same positional parameter binding as the original individual DELETEs
760+
await cur.executemany(
761+
"delete from %s where name=%%s" % TABLE1,
762+
[(test_string,) for test_string in critical_test_strings],
763+
)
764+
765+
# Batch verification: check that all critical strings were deleted
766+
await cur.execute(
767+
"select name from %s where name in (%s)"
768+
% (TABLE1, ",".join(["%s"] * len(critical_test_strings))),
769+
critical_test_strings,
770+
)
771+
remaining_critical = await cur.fetchall()
772+
assert (
773+
len(remaining_critical) == 0
774+
), f"Failed to delete strings: {[row[0] for row in remaining_critical]}"
775+
776+
# Clean up remaining rows
777+
await cur.execute("delete from %s" % TABLE1)
726778

727779

728780
@pytest.mark.skipolddriver

test/integ/aio/test_put_get_async.py

Lines changed: 35 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -232,15 +232,30 @@ async def test_get_multiple_files_with_same_name(tmp_path, aio_connection, caplo
232232
f"PUT 'file://{filename_in_put}' @{stage_name}/data/2/",
233233
)
234234

235+
# Verify files are uploaded before attempting GET
236+
import asyncio
237+
238+
for _ in range(10): # Wait up to 10 seconds for files to be available
239+
file_list = await (await cur.execute(f"LS @{stage_name}")).fetchall()
240+
if len(file_list) >= 2: # Both files should be available
241+
break
242+
await asyncio.sleep(1)
243+
else:
244+
pytest.fail(f"Files not available in stage after 10 seconds: {file_list}")
245+
235246
with caplog.at_level(logging.WARNING):
236247
try:
237248
await cur.execute(
238249
f"GET @{stage_name} file://{tmp_path} PATTERN='.*data.csv.gz'"
239250
)
240251
except OperationalError:
241-
# This is expected flakiness
252+
# This can happen due to cloud storage timing issues
242253
pass
243-
assert "Downloading multiple files with the same name" in caplog.text
254+
255+
# Check for the expected warning message
256+
assert (
257+
"Downloading multiple files with the same name" in caplog.text
258+
), f"Expected warning not found in logs: {caplog.text}"
244259

245260

246261
async def test_transfer_error_message(tmp_path, aio_connection):
@@ -267,24 +282,26 @@ async def test_transfer_error_message(tmp_path, aio_connection):
267282
@pytest.mark.skipolddriver
268283
async def test_put_md5(tmp_path, aio_connection):
269284
"""This test uploads a single and a multi part file and makes sure that md5 is populated."""
270-
# Generate random files and folders
271-
small_folder = tmp_path / "small"
272-
big_folder = tmp_path / "big"
273-
small_folder.mkdir()
274-
big_folder.mkdir()
275-
generate_k_lines_of_n_files(3, 1, tmp_dir=str(small_folder))
276-
# This generates a ~342 MB file to trigger a multipart upload
277-
generate_k_lines_of_n_files(3_000_000, 1, tmp_dir=str(big_folder))
278-
279-
small_test_file = small_folder / "file0"
280-
big_test_file = big_folder / "file0"
285+
# Create files directly without subfolders for efficiency
286+
# Small file for single-part upload test
287+
small_test_file = tmp_path / "small_file.txt"
288+
small_test_file.write_text("test content\n") # Minimal content
289+
290+
# Big file for multi-part upload test - 200MB (well over 64MB threshold)
291+
big_test_file = tmp_path / "big_file.txt"
292+
chunk_size = 1024 * 1024 # 1MB chunks
293+
chunk_data = "A" * chunk_size # 1MB of 'A' characters
294+
with open(big_test_file, "w") as f:
295+
for _ in range(200): # Write 200MB total
296+
f.write(chunk_data)
281297

282298
stage_name = random_string(5, "test_put_md5_")
283299
# Use the async connection for PUT/LS operations
284300
await aio_connection.connect()
285301
async with aio_connection.cursor() as cur:
286302
await cur.execute(f"create temporary stage {stage_name}")
287303

304+
# Upload both files in sequence
288305
small_filename_in_put = str(small_test_file).replace("\\", "/")
289306
big_filename_in_put = str(big_test_file).replace("\\", "/")
290307

@@ -295,6 +312,8 @@ async def test_put_md5(tmp_path, aio_connection):
295312
f"PUT 'file://{big_filename_in_put}' @{stage_name}/big AUTO_COMPRESS = FALSE"
296313
)
297314

298-
res = await cur.execute(f"LS @{stage_name}")
299-
300-
assert all(map(lambda e: e[2] is not None, await res.fetchall()))
315+
# Verify MD5 is populated for both files
316+
file_list = await (await cur.execute(f"LS @{stage_name}")).fetchall()
317+
assert all(
318+
file_info[2] is not None for file_info in file_list
319+
), "MD5 should be populated for all uploaded files"

0 commit comments

Comments
 (0)