4545logger = logging .getLogger (__name__ )
4646
4747
48+ class TableMapping (t .Dict [str , str ]):
49+ """A table name mapping that caches the dialect-normalized form of its keys.
50+
51+ `exp.replace_tables` normalizes every key of the mapping it's given, so resolving a single
52+ table against a mapping of every model in an environment costs O(N). Resolving it against
53+ this mapping costs a dictionary lookup, since each key is normalized once per dialect.
54+ """
55+
56+ def __init__ (self , * args : t .Any , ** kwargs : t .Any ):
57+ super ().__init__ (* args , ** kwargs )
58+ self ._normalized_keys : t .Dict [DialectType , t .Dict [str , str ]] = {}
59+
60+ def normalized_keys (self , dialect : DialectType ) -> t .Dict [str , str ]:
61+ """Returns a mapping from each normalized key to the last key that normalizes to it."""
62+ normalized_keys = self ._normalized_keys .get (dialect )
63+ if normalized_keys is None :
64+ normalized_keys = {exp .normalize_table_name (key , dialect = dialect ): key for key in self }
65+ self ._normalized_keys [dialect ] = normalized_keys
66+ return normalized_keys
67+
68+ def __setitem__ (self , key : str , value : str ) -> None :
69+ self ._normalized_keys .clear ()
70+ super ().__setitem__ (key , value )
71+
72+ def __delitem__ (self , key : str ) -> None :
73+ self ._normalized_keys .clear ()
74+ super ().__delitem__ (key )
75+
76+ def __ior__ (self , other : t .Any ) -> TableMapping : # type: ignore[override,misc]
77+ self ._normalized_keys .clear ()
78+ return super ().__ior__ (other )
79+
80+ def update (self , * args : t .Any , ** kwargs : t .Any ) -> None :
81+ self ._normalized_keys .clear ()
82+ super ().update (* args , ** kwargs )
83+
84+ def setdefault (self , key : str , default : str ) -> str : # type: ignore[override]
85+ self ._normalized_keys .clear ()
86+ return super ().setdefault (key , default )
87+
88+ def pop (self , key : str , * args : t .Any ) -> t .Any :
89+ self ._normalized_keys .clear ()
90+ return super ().pop (key , * args )
91+
92+ def popitem (self ) -> t .Tuple [str , str ]:
93+ self ._normalized_keys .clear ()
94+ return super ().popitem ()
95+
96+ def clear (self ) -> None :
97+ self ._normalized_keys .clear ()
98+ super ().clear ()
99+
100+
101+ def _normalize_keys (mapping : t .Dict [str , str ], dialect : DialectType ) -> t .Dict [str , str ]:
102+ if isinstance (mapping , TableMapping ):
103+ return mapping .normalized_keys (dialect )
104+ return {exp .normalize_table_name (key , dialect = dialect ): key for key in mapping }
105+
106+
48107class BaseExpressionRenderer :
49108 def __init__ (
50109 self ,
@@ -325,20 +384,35 @@ def update_cache(self, expression: t.Optional[exp.Expr]) -> None:
325384
326385 def _resolve_table (
327386 self ,
328- table_name : str | exp . Expr ,
387+ table_name : str ,
329388 snapshots : t .Optional [t .Dict [str , Snapshot ]] = None ,
330389 table_mapping : t .Optional [t .Dict [str , str ]] = None ,
331390 deployability_index : t .Optional [DeployabilityIndex ] = None ,
332391 ) -> exp .Table :
333- table = exp .replace_tables (
334- t .cast (exp .Table , exp .maybe_parse (table_name , into = exp .Table , dialect = self ._dialect )),
335- {
336- ** self ._to_table_mapping ((snapshots or {}).values (), deployability_index ),
337- ** (table_mapping or {}),
338- },
339- dialect = self ._dialect ,
340- copy = False ,
392+ table = t .cast (
393+ exp .Table , exp .maybe_parse (table_name , into = exp .Table , dialect = self ._dialect )
341394 )
395+
396+ mapping : t .Dict [str , str ] = {}
397+ if table_mapping :
398+ # An explicit mapping takes precedence over snapshots, so when one of its keys matches
399+ # the table, that key alone decides the result. Among equivalent keys, the last wins.
400+ key = _normalize_keys (table_mapping , self ._dialect ).get (
401+ exp .normalize_table_name (table , dialect = self ._dialect )
402+ )
403+ if key is not None :
404+ mapping = {key : table_mapping [key ]}
405+
406+ if not mapping and snapshots :
407+ # An exact FQN match avoids scanning unrelated snapshots.
408+ snapshot = snapshots .get (table_name )
409+ # Keys normalized under different dialects may differ in casing or quoting.
410+ # Fall back to the full mapping so exp.replace_tables can reconcile them.
411+ mapping = self ._to_table_mapping (
412+ [snapshot ] if snapshot else snapshots .values (), deployability_index
413+ )
414+
415+ table = exp .replace_tables (table , mapping , dialect = self ._dialect , copy = False )
342416 # We quote the table here to mimic the behavior of _resolve_tables, otherwise we may end
343417 # up normalizing twice, because _to_table_mapping returns the mapped names unquoted.
344418 return (
@@ -363,6 +437,11 @@ def _resolve_tables(
363437
364438 expression = expression .copy ()
365439 with self ._normalize_and_quote (expression ) as expression :
440+ # An expression with no table (e.g. most session or virtual properties) has nothing
441+ # to expand or replace, so skip building the O(N) expand set and mapping.
442+ if not expression .find (exp .Table ):
443+ return expression
444+
366445 snapshots = snapshots or {}
367446 table_mapping = table_mapping or {}
368447 mapping = {
0 commit comments