001package gudusoft.gsqlparser.resolver2.scope;
002
003import gudusoft.gsqlparser.ETableEffectType;
004import gudusoft.gsqlparser.nodes.TTable;
005import gudusoft.gsqlparser.resolver2.ColumnLevel;
006import gudusoft.gsqlparser.resolver2.ScopeType;
007import gudusoft.gsqlparser.resolver2.matcher.INameMatcher;
008import gudusoft.gsqlparser.resolver2.model.ResolvePath;
009import gudusoft.gsqlparser.resolver2.model.ScopeChild;
010import gudusoft.gsqlparser.resolver2.namespace.INamespace;
011import gudusoft.gsqlparser.stmt.TDeleteSqlStatement;
012
013import java.util.ArrayList;
014import java.util.Collections;
015import java.util.List;
016
017/**
018 * Scope for DELETE statement.
019 *
020 * Contains:
021 * - Target table (the DELETE table)
022 * - FROM clause scope (optional - for DELETE...FROM syntax in SQL Server)
023 * - WHERE clause scope
024 *
025 * Example (SQL Server):
026 * DELETE FROM Sales.SalesPersonQuotaHistory
027 * FROM Sales.SalesPersonQuotaHistory AS spqh
028 * INNER JOIN Sales.SalesPerson AS sp
029 * ON spqh.SalesPersonID = sp.SalesPersonID
030 * WHERE sp.SalesYTD > 2500000.00;
031 *
032 * Resolution strategy:
033 * - WHERE clause columns resolve from all visible tables (target table + FROM clause tables)
034 * - JOIN ON clause columns resolve from the joined tables
035 */
036public class DeleteScope extends AbstractScope {
037
038    /** FROM clause scope (includes target table and FROM clause tables) */
039    private FromScope fromScope;
040
041    /** WHERE clause scope */
042    private IScope whereScope;
043
044    /** The DELETE statement */
045    private final TDeleteSqlStatement deleteStatement;
046
047    public DeleteScope(IScope parent, TDeleteSqlStatement stmt) {
048        super(parent, stmt, ScopeType.DELETE);
049        this.deleteStatement = stmt;
050    }
051
052    public void setFromScope(FromScope fromScope) {
053        this.fromScope = fromScope;
054    }
055
056    public FromScope getFromScope() {
057        return fromScope;
058    }
059
060    public void setWhereScope(IScope whereScope) {
061        this.whereScope = whereScope;
062    }
063
064    public IScope getWhereScope() {
065        return whereScope;
066    }
067
068    public TDeleteSqlStatement getDeleteStatement() {
069        return deleteStatement;
070    }
071
072    @Override
073    public INamespace resolveTable(String tableName) {
074        // First try FROM scope
075        if (fromScope != null) {
076            INamespace ns = fromScope.resolveTable(tableName);
077            if (ns != null) {
078                return ns;
079            }
080        }
081
082        // Then delegate to parent
083        return super.resolveTable(tableName);
084    }
085
086    @Override
087    public List<INamespace> getVisibleNamespaces() {
088        List<INamespace> visible = new ArrayList<>();
089
090        // The FROM scope's own relations. The FROM scope's parent is this scope, so asking it for ITS visible
091        // namespaces would come straight back here and never terminate (Mantis 4820).
092        addOwnNamespaces(visible, fromScope);
093
094        // Add parent's namespaces
095        addAllDistinct(visible, getLookupParent().getVisibleNamespaces());
096
097        return visible;
098    }
099
100    /**
101     * Resolve a name within this DELETE scope.
102     *
103     * Resolution strategy (same as UpdateScope):
104     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
105     * 2. For unqualified names (col): search all visible namespaces in FROM scope
106     * 3. Delegate to parent for names not found locally
107     */
108    @Override
109    public void resolve(List<String> names,
110                       INameMatcher matcher,
111                       boolean deep,
112                       IResolved resolved) {
113        if (names.isEmpty()) {
114            return;
115        }
116
117        boolean foundLocally = false;
118
119        // First, try to resolve via FROM scope
120        if (fromScope != null) {
121            if (names.size() >= 2) {
122                // Qualified name like "t.col" - let FROM scope handle it
123                String firstName = names.get(0);
124
125                // Check if first name matches any child in FROM scope
126                // Track matched aliases to avoid duplicate matches for same table name
127                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
128                for (ScopeChild child : fromScope.getChildren()) {
129                    String alias = child.getAlias();
130                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
131                        // Found matching table/subquery - only add once per alias
132                        matchedAliases.add(alias);
133                        INamespace namespace = child.getNamespace();
134                        List<String> remaining = names.subList(1, names.size());
135                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
136                        foundLocally = true;
137                    }
138                }
139            } else {
140                // Single name - could be a table alias or unqualified column
141                String name = names.get(0);
142
143                // First check if it's a table alias
144                for (ScopeChild child : fromScope.getChildren()) {
145                    if (matcher.matches(child.getAlias(), name)) {
146                        // It's a table alias - resolve as table
147                        resolved.found(child.getNamespace(), child.isNullable(),
148                                      this, new ResolvePath(), Collections.emptyList());
149                        foundLocally = true;
150                    }
151                }
152
153                // If not a table alias, try to resolve as unqualified column
154                if (!foundLocally) {
155                    // First, determine if we have a "sole implicit derived table" scenario
156                    // According to teradata_implicit_derived_tables_zh.md:
157                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
158                    // If there are NO explicit tables AND exactly 1 implicit derived table,
159                    // unqualified columns should be linked to that implicit derived table
160                    List<INamespace> explicitTables = new ArrayList<>();
161                    List<INamespace> implicitDerivedTables = new ArrayList<>();
162
163                    for (ScopeChild child : fromScope.getChildren()) {
164                        INamespace ns = child.getNamespace();
165                        TTable table = ns.getFinalTable();
166                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
167                            implicitDerivedTables.add(ns);
168                        } else {
169                            explicitTables.add(ns);
170                        }
171                    }
172
173                    // If no explicit tables and exactly 1 implicit derived table,
174                    // include it for unqualified column resolution
175                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
176                                                           implicitDerivedTables.size() == 1;
177
178                    // Search through all visible namespaces.
179                    //
180                    // Mirrors UpdateScope.resolve: physical tables without
181                    // metadata (no catalog supplied) report ColumnLevel.MAYBE
182                    // but do NOT support dynamic inference. They land in
183                    // {@code inferredCandidates} so a single-target DELETE
184                    // without a catalog still resolves its WHERE columns to
185                    // the lone visible base table (EXACT_MATCH downstream).
186                    // Without this branch, the slice-77 / slice-81 path that
187                    // collects column refs via NameBindingProvider.bindColumn
188                    // fails with NOT_FOUND for every DELETE WHERE column
189                    // when no catalog is supplied.
190                    List<INamespace> candidates = new ArrayList<>();
191                    List<INamespace> maybeCandidates = new ArrayList<>();
192                    List<INamespace> inferredCandidates = new ArrayList<>();
193
194                    for (ScopeChild child : fromScope.getChildren()) {
195                        INamespace ns = child.getNamespace();
196
197                        // Skip implicit lateral derived tables (Teradata-specific)
198                        // They should only match qualified references like "employee.employee_id"
199                        // UNLESS this is a "sole implicit derived table" scenario where there
200                        // are no explicit tables and exactly 1 implicit derived table
201                        TTable table = ns.getFinalTable();
202                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
203                            if (!useSoleImplicitDerivedTable) {
204                                continue;
205                            }
206                        }
207
208                        ColumnLevel level = ns.hasColumn(name);
209
210                        if (level == ColumnLevel.EXISTS) {
211                            candidates.add(ns);
212                        } else if (level == ColumnLevel.MAYBE) {
213                            // MAYBE means the column might exist (no metadata to confirm).
214                            // For subqueries/CTEs with star columns add to maybeCandidates;
215                            // for physical tables without metadata add to inferredCandidates
216                            // so we can detect ambiguity when multiple tables could have
217                            // the column AND resolve unambiguously when only one could.
218                            if (ns.supportsDynamicInference()) {
219                                maybeCandidates.add(ns);
220                            } else {
221                                inferredCandidates.add(ns);
222                            }
223                        }
224                    }
225
226                    // If we found exact matches, use them
227                    if (!candidates.isEmpty()) {
228                        for (INamespace ns : candidates) {
229                            resolved.found(ns, false, this, new ResolvePath(), names);
230                        }
231                        foundLocally = true;
232                    }
233                    // If we found MAYBE matches (star columns), only use if exactly one
234                    // Multiple MAYBE candidates means ambiguous - don't resolve
235                    else if (maybeCandidates.size() == 1) {
236                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
237                        foundLocally = true;
238                    }
239                    // If multiple tables without metadata, report all for ambiguity detection.
240                    // This allows NameResolver to detect that the column is ambiguous
241                    // and populate candidateTables with all potential sources.
242                    else if (inferredCandidates.size() > 1) {
243                        for (INamespace ns : inferredCandidates) {
244                            resolved.found(ns, false, this, new ResolvePath(), names);
245                        }
246                        foundLocally = true;
247                    }
248                    // Single inferred candidate with no maybeCandidates: resolve to it.
249                    // For single-target DELETE without a catalog this is the typical
250                    // case — the lone employees table becomes the EXACT_MATCH source.
251                    else if (inferredCandidates.size() == 1 && maybeCandidates.isEmpty()) {
252                        resolved.found(inferredCandidates.get(0), false, this, new ResolvePath(), names);
253                        foundLocally = true;
254                    }
255                    // Multiple maybe candidates means ambiguous with star columns - don't resolve
256                }
257            }
258        }
259
260        // If not found locally, delegate to parent
261        if (!foundLocally) {
262            parent.resolve(names, matcher, deep, resolved);
263        }
264    }
265
266    @Override
267    public String toString() {
268        return String.format("DeleteScope(from=%s)", fromScope != null ? "present" : "null");
269    }
270}