001package gudusoft.gsqlparser.dlineage.routine;
002
003import gudusoft.gsqlparser.catalog.runtime.CatalogLookupName;
004import gudusoft.gsqlparser.catalog.runtime.CatalogObjectKind;
005import gudusoft.gsqlparser.sqlenv.CanonKey;
006
007import java.util.ArrayList;
008import java.util.EnumSet;
009import java.util.HashMap;
010import java.util.LinkedHashSet;
011import java.util.List;
012import java.util.Map;
013import java.util.Set;
014
015/**
016 * The routines declared in one analysis, by identity, with ordered alias indexes for lookups.
017 *
018 * <p>A declaration with the identity of an earlier one replaces it in place ({@code CREATE OR REPLACE}, a
019 * package body after its specification). Lookups compare written names level by level
020 * ({@link RoutineName}): the terminal through a per-kind index, the containers by their keys and states,
021 * then the argument alias: a name-only lookup matches declarations keyed by name only, a lookup with
022 * {@code n} arguments matches a declaration's total or input count. The result says how it matched and
023 * whether it is unique ({@link RoutineLookupResult}); it never picks the first of several.</p>
024 */
025public final class RoutineRegistry {
026
027    private final List<RoutineDeclaration> declarations = new ArrayList<RoutineDeclaration>();
028    private final Map<RoutineDeclaration.Identity, Integer> byIdentity = new HashMap<RoutineDeclaration.Identity, Integer>();
029    /** Declarations by the kind they were declared as, then by their terminal key under that kind. */
030    private final Map<CatalogObjectKind, Map<CanonKey, List<Integer>>> byTerminal =
031            new HashMap<CatalogObjectKind, Map<CanonKey, List<Integer>>>();
032
033    /**
034     * Adds a declaration, or replaces the current occurrence of its identity; returns the declaration it
035     * replaced, or null.
036     */
037    public RoutineDeclaration declare(RoutineDeclaration declaration) {
038        Integer existing = byIdentity.get(declaration.identity());
039        if (existing != null) {
040            RoutineDeclaration replaced = declarations.get(existing);
041            declaration.setRepresentative(replaced.getRepresentative());
042            declarations.set(existing, declaration);
043            return replaced;
044        }
045        int index = declarations.size();
046        declarations.add(declaration);
047        byIdentity.put(declaration.identity(), index);
048        CatalogObjectKind kind = declaration.getName().getKind();
049        Map<CanonKey, List<Integer>> terminals = byTerminal.get(kind);
050        if (terminals == null) {
051            terminals = new HashMap<CanonKey, List<Integer>>();
052            byTerminal.put(kind, terminals);
053        }
054        CanonKey terminal = declaration.getName().terminalKey(kind);
055        List<Integer> entries = terminals.get(terminal);
056        if (entries == null) {
057            entries = new ArrayList<Integer>(1);
058            terminals.put(terminal, entries);
059        }
060        entries.add(index);
061        return null;
062    }
063
064    /** The current occurrence of an identity, or null. */
065    public RoutineDeclaration find(RoutineDeclaration.Identity identity) {
066        Integer index = byIdentity.get(identity);
067        return index == null ? null : declarations.get(index);
068    }
069
070    public List<RoutineDeclaration> getDeclarations() {
071        return java.util.Collections.unmodifiableList(declarations);
072    }
073
074    /**
075     * Looks a routine up by the interpretations of its written name and, when the call site keys it with
076     * one, the argument count ({@code null} for a name-only lookup).
077     */
078    public RoutineLookupResult lookup(List<RoutineName> interpretations, Integer argumentCount) {
079        return lookup(interpretations, argumentCount, CatalogObjectKind.ROUTINE, null);
080    }
081
082    /**
083     * A lookup for a routine of {@code wanted} kind (ROUTINE when the call site does not say), seeing the local
084     * routines declared in {@code visibleScopes} (the routines enclosing the call); triggers are not callable.
085     */
086    public RoutineLookupResult lookup(List<RoutineName> interpretations, Integer argumentCount, CatalogObjectKind wanted,
087            java.util.Collection<Object> visibleScopes) {
088        Set<RoutineDeclaration> found = new LinkedHashSet<RoutineDeclaration>();
089        RoutineLookupResult.Branch branch = RoutineLookupResult.Branch.NONE;
090        for (RoutineName name : interpretations) {
091            RoutineLookupResult.Branch nameBranch = RoutineLookupResult.Branch.NONE;
092            List<RoutineDeclaration> nameFound = new ArrayList<RoutineDeclaration>();
093            for (RoutineLookupResult.Branch branchWanted : EnumSet.range(RoutineLookupResult.Branch.EXACT,
094                    RoutineLookupResult.Branch.PARTIAL)) {
095                nameFound = candidates(name, argumentCount, branchWanted, wanted, visibleScopes);
096                if (!nameFound.isEmpty()) {
097                    nameBranch = branchWanted;
098                    break;
099                }
100            }
101            if (nameFound.isEmpty()) {
102                continue;
103            }
104            if (branch == RoutineLookupResult.Branch.NONE || nameBranch.compareTo(branch) < 0) {
105                branch = nameBranch;
106            }
107            found.addAll(nameFound);
108        }
109        if (found.isEmpty()) {
110            return RoutineLookupResult.NOT_FOUND;
111        }
112        List<RoutineDeclaration> ordered = new ArrayList<RoutineDeclaration>(found);
113        java.util.Collections.sort(ordered, (a, b) -> Integer.compare(indexOf(a), indexOf(b)));
114        RoutineLookupResult.Status status;
115        if (ordered.size() > 1) {
116            status = RoutineLookupResult.Status.AMBIGUOUS;
117        } else if (branch == RoutineLookupResult.Branch.EXACT || branch == RoutineLookupResult.Branch.NAME_FALLBACK) {
118            status = RoutineLookupResult.Status.RESOLVED;
119        } else {
120            status = RoutineLookupResult.Status.REQUIRES_CONTEXT;
121        }
122        return new RoutineLookupResult(status, branch, ordered);
123    }
124
125    private int indexOf(RoutineDeclaration declaration) {
126        Integer index = byIdentity.get(declaration.identity());
127        return index == null ? Integer.MAX_VALUE : index;
128    }
129
130    private List<RoutineDeclaration> candidates(RoutineName name, Integer argumentCount,
131            RoutineLookupResult.Branch wanted, CatalogObjectKind wantedKind, java.util.Collection<Object> visibleScopes) {
132        List<Integer> indexes = new ArrayList<Integer>();
133        for (Map.Entry<CatalogObjectKind, Map<CanonKey, List<Integer>>> kind : byTerminal.entrySet()) {
134            if (kind.getKey() == CatalogObjectKind.TRIGGER) {
135                continue;
136            }
137            List<Integer> entries = kind.getValue().get(name.terminalKey(kind.getKey()));
138            if (entries != null) {
139                indexes.addAll(entries);
140            }
141        }
142        java.util.Collections.sort(indexes);
143        List<RoutineDeclaration> out = new ArrayList<RoutineDeclaration>();
144        for (Integer index : indexes) {
145            RoutineDeclaration declaration = declarations.get(index);
146            CatalogObjectKind namespace = declaration.getNamespace();
147            if (namespace != CatalogObjectKind.ROUTINE && wantedKind != CatalogObjectKind.ROUTINE && namespace != wantedKind) {
148                // The vendor keeps functions and procedures apart (DB2): a CALL does not find a function.
149                continue;
150            }
151            if (declaration.scope() != null && (visibleScopes == null || !containsIdentical(visibleScopes, declaration.scope()))) {
152                // A routine local to another is visible only inside it.
153                continue;
154            }
155            RoutineLookupResult.Branch containers = branchOf(name, declaration.getName());
156            boolean aliasMatches;
157            if (wanted == RoutineLookupResult.Branch.NAME_FALLBACK) {
158                // A name-only lookup reaches declarations of that very name keyed with a signature (the
159                // legacy prefix match `name(`).
160                aliasMatches = containers == RoutineLookupResult.Branch.EXACT && argumentCount == null
161                        && declaration.hasSignatureAliases();
162            } else if (containers != wanted) {
163                aliasMatches = false;
164            } else if (argumentCount != null) {
165                aliasMatches = declaration.acceptsCount(argumentCount);
166            } else if (wanted == RoutineLookupResult.Branch.EXACT) {
167                aliasMatches = !declaration.hasSignatureAliases();
168            } else {
169                // A name-only leaf match reaches only a declaration keyed with a signature, as the legacy
170                // `leaf.startsWith(name + "(")` did.
171                aliasMatches = declaration.hasSignatureAliases();
172            }
173            if (aliasMatches) {
174                out.add(declaration);
175            }
176        }
177        return out;
178    }
179
180    private static boolean containsIdentical(java.util.Collection<Object> scopes, Object scope) {
181        for (Object candidate : scopes) {
182            if (candidate == scope) {
183                return true;
184            }
185        }
186        return false;
187    }
188
189    /** How a lookup's containers relate to a declaration's (the terminals already equal). */
190    static RoutineLookupResult.Branch branchOf(RoutineName lookup, RoutineName declared) {
191        List<RoutineName.Level> a = lookup.getContainers();
192        List<RoutineName.Level> b = declared.getContainers();
193        if (lookup.isPackageMember() != declared.isPackageMember() || a.size() != b.size()) {
194            return RoutineLookupResult.Branch.NONE;
195        }
196        boolean lookupOmits = false;
197        boolean declarationOmits = false;
198        for (int i = 0; i < a.size(); i++) {
199            RoutineName.Level x = a.get(i);
200            RoutineName.Level y = b.get(i);
201            if (x.getRole() != y.getRole()) {
202                return RoutineLookupResult.Branch.NONE;
203            }
204            boolean xNamed = x.getState() == CatalogLookupName.SlotState.PRESENT;
205            boolean yNamed = y.getState() == CatalogLookupName.SlotState.PRESENT;
206            if (xNamed && yNamed) {
207                if (!x.getKey().equals(y.getKey())) {
208                    return RoutineLookupResult.Branch.NONE;
209                }
210            } else if (yNamed) {
211                lookupOmits = true;
212            } else if (xNamed) {
213                declarationOmits = true;
214            }
215        }
216        if (lookupOmits && declarationOmits) {
217            return RoutineLookupResult.Branch.PARTIAL;
218        }
219        if (lookupOmits) {
220            return RoutineLookupResult.Branch.LEAF_QUALIFIED;
221        }
222        if (declarationOmits) {
223            return RoutineLookupResult.Branch.LEAF_UNQUALIFIED;
224        }
225        return RoutineLookupResult.Branch.EXACT;
226    }
227}