11from dataclasses import dataclass , field
22from datetime import datetime
33from enum import Enum
4+ import re
45
56import psycopg
67
@@ -339,9 +340,23 @@ def check_constraints(self):
339340 ORDER BY n.nspname, cl.relname, c.conname
340341 """
341342
343+ # Normalization function for constraint records
344+ def normalize_constraint_record (record_dict , col_names ):
345+ """Normalize constraint definitions in a record."""
346+ normalized = record_dict .copy ()
347+ if "constraint_definition" in normalized and normalized ["constraint_definition" ]:
348+ normalized ["constraint_definition" ] = self .__normalize_constraint_definition (
349+ normalized ["constraint_definition" ]
350+ )
351+ return normalized
352+
342353 # Execute both queries and combine results
343- passed_keys , diffs_keys = self .__check_equals (key_query )
344- passed_checks , diffs_checks = self .__check_equals (check_query )
354+ passed_keys , diffs_keys = self .__check_equals (
355+ key_query , normalize_func = normalize_constraint_record
356+ )
357+ passed_checks , diffs_checks = self .__check_equals (
358+ check_query , normalize_func = normalize_constraint_record
359+ )
345360
346361 return (passed_keys and passed_checks , diffs_keys + diffs_checks )
347362
@@ -527,11 +542,57 @@ def check_rules(self):
527542
528543 return self .__check_equals (query )
529544
530- def __check_equals (self , query ) -> tuple [bool , list [DifferenceItem ]]:
545+ @staticmethod
546+ def __normalize_constraint_definition (definition : str ) -> str :
547+ """Normalize a constraint definition for comparison.
548+
549+ PostgreSQL may represent functionally equivalent constraints differently,
550+ especially after dump/restore operations. This function normalizes common
551+ variations to enable accurate comparison.
552+
553+ Args:
554+ definition: The constraint definition string from pg_get_constraintdef()
555+
556+ Returns:
557+ Normalized constraint definition
558+ """
559+ if not definition :
560+ return definition
561+
562+ # Normalize different ARRAY representations:
563+ # Before: (ARRAY['a'::type, 'b'::type])::type[] OR ARRAY[('a'::type)::text, ...]
564+ # After: Canonical form based on sorted elements
565+
566+ # Strategy: Extract the constraint type and key values, ignoring formatting details
567+ # For ANY/ALL with arrays, extract just the operator and the array values
568+
569+ # Remove extra parentheses around ARRAY expressions
570+ # (ARRAY[...])::type[] -> ARRAY[...]::type[]
571+ definition = re .sub (r"\(\(ARRAY\[(.*?)\]\)::(.*?)\[\]\)" , r"ARRAY[\1]::\2[]" , definition )
572+
573+ # Also remove parentheses without cast: (ARRAY[...]) -> ARRAY[...]
574+ definition = re .sub (r"\(ARRAY\[([^\]]+)\]\)" , r"ARRAY[\1]" , definition )
575+
576+ # Normalize array element casts: ('value'::type1)::type2 -> 'value'::type1
577+ # This handles the case where elements are double-cast
578+ definition = re .sub (r"\('([^']+)'::([^)]+)\)::(\w+)" , r"'\1'::\2" , definition )
579+
580+ # Remove trailing array cast that may be present or absent: ::text[] or ::character varying[]
581+ # This is safe because the type information is already in each array element
582+ definition = re .sub (r"::(?:text|character varying)\[\]" , "" , definition )
583+
584+ # Remove extra whitespace and normalize spacing
585+ definition = re .sub (r"\s+" , " " , definition ).strip ()
586+
587+ return definition
588+
589+ def __check_equals (self , query , normalize_func = None ) -> tuple [bool , list [DifferenceItem ]]:
531590 """Check if the query results on the two databases are equals.
532591
533592 Args:
534593 query: The SQL query to execute on both databases.
594+ normalize_func: Optional function to normalize specific fields in records.
595+ Should accept (dict, col_names) and return normalized dict.
535596
536597 Returns:
537598 tuple: A tuple containing:
@@ -554,6 +615,14 @@ def __check_equals(self, query) -> tuple[bool, list[DifferenceItem]]:
554615 structured1 = [dict (zip (col_names , record )) for record in records1 ]
555616 structured2 = [dict (zip (col_names , record )) for record in records2 ]
556617
618+ # Apply normalization if provided
619+ if normalize_func :
620+ structured1 = [normalize_func (r , col_names ) for r in structured1 ]
621+ structured2 = [normalize_func (r , col_names ) for r in structured2 ]
622+ # Recreate records from normalized structured data
623+ records1 = [tuple (r [col ] for col in col_names ) for r in structured1 ]
624+ records2 = [tuple (r [col ] for col in col_names ) for r in structured2 ]
625+
557626 # Create sets for comparison
558627 set1 = {str (tuple (r )) for r in records1 }
559628 set2 = {str (tuple (r )) for r in records2 }
0 commit comments