Skip to content

Commit b19da41

Browse files
author
Samson Gebre
committed
feat: add SQL encoding verification test for batch operations
1 parent b8ef1f4 commit b19da41

1 file changed

Lines changed: 135 additions & 0 deletions

File tree

examples/basic/functional_testing.py

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,137 @@ def test_query_records(client: DataverseClient, table_info: Dict[str, Any]) -> N
311311
print(" This might be expected if the table is very new.")
312312

313313

314+
def test_sql_encoding(
315+
client: DataverseClient,
316+
table_info: Dict[str, Any],
317+
retrieved_record: Dict[str, Any],
318+
) -> None:
319+
"""Verify SQL encoding parity between client.query.sql() and batch.query.sql().
320+
321+
The direct path (client.query.sql) delegates to _build_sql which encodes the
322+
SQL via urllib.parse.quote(safe=''), producing %20 for spaces. The batch path
323+
uses the same _build_sql method, so both should behave identically.
324+
325+
Specifically tests SQL containing:
326+
- Spaces in a WHERE string literal (requires %20 encoding)
327+
- Colons in a WHERE string literal (the HH:MM:SS timestamp in the name)
328+
329+
Both paths are run against the same SQL and their results are compared
330+
to confirm the encoding produces matching Dataverse responses.
331+
"""
332+
print("\n-> SQL Encoding Verification Test")
333+
print("=" * 50)
334+
335+
table_schema_name = table_info.get("table_schema_name")
336+
logical_name = table_info.get("table_logical_name", table_schema_name.lower())
337+
attr_prefix = table_schema_name.split("_", 1)[0] if "_" in table_schema_name else table_schema_name
338+
name_col = f"{attr_prefix}_name"
339+
known_name = retrieved_record.get(name_col, "")
340+
341+
try:
342+
# ------------------------------------------------------------------
343+
# Case 1: Basic SELECT — no special characters in WHERE clause.
344+
# Baseline: confirms the path works before adding complexity.
345+
# ------------------------------------------------------------------
346+
basic_sql = f"SELECT TOP 5 {name_col} FROM {logical_name}"
347+
print(f" [1/3] Basic SELECT (no special chars): {basic_sql}")
348+
349+
direct_rows = client.query.sql(basic_sql)
350+
direct_count = len(direct_rows)
351+
352+
batch = client.batch.new()
353+
batch.query.sql(basic_sql)
354+
result = batch.execute()
355+
batch_count = len(result.responses[0].data.get("value", [])) if result.responses and result.responses[0].is_success and result.responses[0].data else 0
356+
357+
assert direct_count == batch_count, (
358+
f"Row count mismatch: client={direct_count}, batch={batch_count}"
359+
)
360+
print(f" [OK] Both paths returned {direct_count} rows")
361+
362+
# ------------------------------------------------------------------
363+
# Case 2: WHERE clause with spaces and colons in the string literal.
364+
# This is the critical case: the record name is of the form
365+
# "Test Record HH:MM:SS" which contains spaces (-> %20) and
366+
# colons. If encoding differs between direct and batch, only
367+
# one path would find the record.
368+
# ------------------------------------------------------------------
369+
if known_name:
370+
escaped_name = known_name.replace("'", "''")
371+
where_sql = f"SELECT TOP 1 {name_col} FROM {logical_name} WHERE {name_col} = '{escaped_name}'"
372+
print(f" [2/3] WHERE with spaces/colons: ...WHERE {name_col} = '{escaped_name}'")
373+
374+
direct_rows_where = client.query.sql(where_sql)
375+
direct_where_count = len(direct_rows_where)
376+
377+
batch2 = client.batch.new()
378+
batch2.query.sql(where_sql)
379+
result2 = batch2.execute()
380+
batch_where_count = (
381+
len(result2.responses[0].data.get("value", []))
382+
if result2.responses and result2.responses[0].is_success and result2.responses[0].data
383+
else 0
384+
)
385+
386+
assert direct_where_count == batch_where_count, (
387+
f"Row count mismatch on WHERE query: client={direct_where_count}, batch={batch_where_count}"
388+
)
389+
assert direct_where_count == 1, (
390+
f"Expected exactly 1 row for known record name, got {direct_where_count}"
391+
)
392+
direct_name = direct_rows_where[0].get(name_col)
393+
assert direct_name == known_name, (
394+
f"Returned name '{direct_name}' does not match expected '{known_name}'"
395+
)
396+
print(f" [OK] Both paths found the record: '{direct_name}'")
397+
else:
398+
print(" [2/3] Skipped WHERE test — record name not available in retrieved_record")
399+
400+
# ------------------------------------------------------------------
401+
# Case 3: WHERE clause with an equals sign inside the string literal.
402+
# Creates a temporary record whose name contains '=' (which
403+
# must be percent-encoded as %3D in the query string), queries
404+
# it via both paths, then deletes it.
405+
# ------------------------------------------------------------------
406+
print(" [3/3] WHERE with '=' in string literal (tests %3D encoding)")
407+
equals_name = f"SQL=Test {datetime.now().strftime('%H:%M:%S')}"
408+
eq_id = client.records.create(table_schema_name, {name_col: equals_name})
409+
try:
410+
escaped_eq = equals_name.replace("'", "''")
411+
eq_sql = f"SELECT TOP 1 {name_col} FROM {logical_name} WHERE {name_col} = '{escaped_eq}'"
412+
413+
direct_eq_rows = client.query.sql(eq_sql)
414+
direct_eq_count = len(direct_eq_rows)
415+
416+
batch3 = client.batch.new()
417+
batch3.query.sql(eq_sql)
418+
result3 = batch3.execute()
419+
batch_eq_count = (
420+
len(result3.responses[0].data.get("value", []))
421+
if result3.responses and result3.responses[0].is_success and result3.responses[0].data
422+
else 0
423+
)
424+
425+
assert direct_eq_count == batch_eq_count, (
426+
f"Row count mismatch on '=' query: client={direct_eq_count}, batch={batch_eq_count}"
427+
)
428+
assert direct_eq_count == 1, (
429+
f"Expected 1 row for '=' record, got {direct_eq_count}"
430+
)
431+
print(f" [OK] Both paths found record with '=' in name: '{direct_eq_rows[0].get(name_col)}'")
432+
finally:
433+
client.records.delete(table_schema_name, eq_id)
434+
435+
print("[OK] SQL encoding verification passed — %20/%3D encoding is consistent across both paths")
436+
437+
except AssertionError as e:
438+
print(f"[ERR] Encoding parity assertion failed: {e}")
439+
raise
440+
except Exception as e:
441+
print(f"[WARN] SQL encoding test encountered an issue: {e}")
442+
print(" Check that the test table exists and has at least one record.")
443+
444+
314445
def test_batch_all_operations(client: DataverseClient, table_info: Dict[str, Any]) -> None:
315446
"""Test every available batch operation type in a structured sequence.
316447
@@ -657,6 +788,9 @@ def main():
657788
# Test querying
658789
test_query_records(client, table_info)
659790

791+
# Verify SQL encoding parity between direct and batch paths
792+
test_sql_encoding(client, table_info, retrieved_record)
793+
660794
# Test batch operations (all operation types)
661795
test_batch_all_operations(client, table_info)
662796

@@ -668,6 +802,7 @@ def main():
668802
print("[OK] Record Creation: Success")
669803
print("[OK] Record Reading: Success")
670804
print("[OK] Record Querying: Success")
805+
print("[OK] SQL Encoding: Success")
671806
print("[OK] Batch Operations: Success")
672807
print("\nYour PowerPlatform Dataverse Client SDK is fully functional!")
673808

0 commit comments

Comments
 (0)