@@ -440,3 +440,99 @@ def test_incomplete_entry_recovery(self):
440440 # Should have error about incomplete 'Server'
441441 errors = exc_info .value .errors
442442 assert any ("Server" in err and "Incomplete specification" in err for err in errors )
443+
444+
445+ class TestSynonymFirstWins :
446+ """
447+ Verify that _normalize_params uses first-wins for synonym keys.
448+
449+ ODBC Driver 18 behaviour (confirmed via live test against sqlcconn.cpp):
450+ - Same key repeated → first-wins (fFromAttrOrProp guard)
451+ - Addr vs Address → same KEY_ADDR slot, first-wins
452+ - Addr/Address vs Server → separate slots, Addr/Address takes priority
453+
454+ _ConnectionStringParser._parse() rejects exact duplicate keys outright.
455+ These tests cover synonyms that map to the same canonical key during
456+ normalization (e.g. addr/address/server → "Server").
457+ """
458+
459+ @staticmethod
460+ def _normalize (raw : dict ) -> dict :
461+ """Shorthand for calling _normalize_params with warnings suppressed."""
462+ return _ConnectionStringParser ._normalize_params (raw , warn_rejected = False )
463+
464+ # ---- server / addr / address synonyms --------------------------------
465+
466+ def test_server_then_addr_first_wins (self ):
467+ """Server=A;Addr=B → first-wins keeps A."""
468+ result = self ._normalize ({"server" : "hostA" , "addr" : "hostB" })
469+ assert result ["Server" ] == "hostA"
470+
471+ def test_addr_then_server_first_wins (self ):
472+ """Addr=A;Server=B → first-wins keeps A."""
473+ result = self ._normalize ({"addr" : "hostA" , "server" : "hostB" })
474+ assert result ["Server" ] == "hostA"
475+
476+ def test_address_then_server_first_wins (self ):
477+ """Address=A;Server=B → first-wins keeps A."""
478+ result = self ._normalize ({"address" : "hostA" , "server" : "hostB" })
479+ assert result ["Server" ] == "hostA"
480+
481+ def test_addr_then_address_first_wins (self ):
482+ """Addr=A;Address=B → first-wins keeps A."""
483+ result = self ._normalize ({"addr" : "hostA" , "address" : "hostB" })
484+ assert result ["Server" ] == "hostA"
485+
486+ def test_all_three_server_synonyms_first_wins (self ):
487+ """Addr=A;Address=B;Server=C → first-wins keeps A."""
488+ result = self ._normalize ({"addr" : "hostA" , "address" : "hostB" , "server" : "hostC" })
489+ assert result ["Server" ] == "hostA"
490+
491+ def test_server_only_no_synonyms (self ):
492+ """Single key has no conflict."""
493+ result = self ._normalize ({"server" : "hostA" })
494+ assert result ["Server" ] == "hostA"
495+
496+ # ---- trustservercertificate / trust_server_certificate synonyms ------
497+
498+ def test_trustservercertificate_then_snake_case_first_wins (self ):
499+ """trustservercertificate=Yes;trust_server_certificate=No → first-wins keeps Yes."""
500+ result = self ._normalize (
501+ {"trustservercertificate" : "Yes" , "trust_server_certificate" : "No" }
502+ )
503+ assert result ["TrustServerCertificate" ] == "Yes"
504+
505+ def test_snake_case_then_trustservercertificate_first_wins (self ):
506+ """trust_server_certificate=No;trustservercertificate=Yes → first-wins keeps No."""
507+ result = self ._normalize (
508+ {"trust_server_certificate" : "No" , "trustservercertificate" : "Yes" }
509+ )
510+ assert result ["TrustServerCertificate" ] == "No"
511+
512+ # ---- packetsize / "packet size" synonyms -----------------------------
513+
514+ def test_packetsize_then_packet_space_first_wins (self ):
515+ """packetsize=8192;packet size=4096 → first-wins keeps 8192."""
516+ result = self ._normalize ({"packetsize" : "8192" , "packet size" : "4096" })
517+ assert result ["PacketSize" ] == "8192"
518+
519+ def test_packet_space_then_packetsize_first_wins (self ):
520+ """packet size=4096;packetsize=8192 → first-wins keeps 4096."""
521+ result = self ._normalize ({"packet size" : "4096" , "packetsize" : "8192" })
522+ assert result ["PacketSize" ] == "4096"
523+
524+ # ---- non-synonym keys are unaffected ---------------------------------
525+
526+ def test_different_keys_both_kept (self ):
527+ """Non-synonym keys should both be present."""
528+ result = self ._normalize ({"server" : "host" , "database" : "mydb" , "uid" : "sa" })
529+ assert result == {"Server" : "host" , "Database" : "mydb" , "UID" : "sa" }
530+
531+ # ---- reserved keys filtered regardless of order ----------------------
532+
533+ def test_reserved_keys_always_filtered (self ):
534+ """Driver and APP are always stripped, even when first."""
535+ result = self ._normalize ({"driver" : "foo" , "server" : "host" , "app" : "bar" })
536+ assert "Driver" not in result
537+ assert "APP" not in result
538+ assert result ["Server" ] == "host"
0 commit comments