@@ -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+
314445def 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 ("\n Your PowerPlatform Dataverse Client SDK is fully functional!" )
673808
0 commit comments