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        // Add FROM scope's namespaces
091        if (fromScope != null) {
092            visible.addAll(fromScope.getVisibleNamespaces());
093        }
094
095        // Add parent's namespaces
096        visible.addAll(parent.getVisibleNamespaces());
097
098        return visible;
099    }
100
101    /**
102     * Resolve a name within this DELETE scope.
103     *
104     * Resolution strategy (same as UpdateScope):
105     * 1. For qualified names (t.col): find table t in FROM scope, then resolve col
106     * 2. For unqualified names (col): search all visible namespaces in FROM scope
107     * 3. Delegate to parent for names not found locally
108     */
109    @Override
110    public void resolve(List<String> names,
111                       INameMatcher matcher,
112                       boolean deep,
113                       IResolved resolved) {
114        if (names.isEmpty()) {
115            return;
116        }
117
118        boolean foundLocally = false;
119
120        // First, try to resolve via FROM scope
121        if (fromScope != null) {
122            if (names.size() >= 2) {
123                // Qualified name like "t.col" - let FROM scope handle it
124                String firstName = names.get(0);
125
126                // Check if first name matches any child in FROM scope
127                // Track matched aliases to avoid duplicate matches for same table name
128                java.util.Set<String> matchedAliases = new java.util.HashSet<>();
129                for (ScopeChild child : fromScope.getChildren()) {
130                    String alias = child.getAlias();
131                    if (matcher.matches(alias, firstName) && !matchedAliases.contains(alias)) {
132                        // Found matching table/subquery - only add once per alias
133                        matchedAliases.add(alias);
134                        INamespace namespace = child.getNamespace();
135                        List<String> remaining = names.subList(1, names.size());
136                        resolved.found(namespace, child.isNullable(), this, new ResolvePath(), remaining);
137                        foundLocally = true;
138                    }
139                }
140            } else {
141                // Single name - could be a table alias or unqualified column
142                String name = names.get(0);
143
144                // First check if it's a table alias
145                for (ScopeChild child : fromScope.getChildren()) {
146                    if (matcher.matches(child.getAlias(), name)) {
147                        // It's a table alias - resolve as table
148                        resolved.found(child.getNamespace(), child.isNullable(),
149                                      this, new ResolvePath(), Collections.emptyList());
150                        foundLocally = true;
151                    }
152                }
153
154                // If not a table alias, try to resolve as unqualified column
155                if (!foundLocally) {
156                    // First, determine if we have a "sole implicit derived table" scenario
157                    // According to teradata_implicit_derived_tables_zh.md:
158                    // "当前作用域只可见 1 张表:未限定列默认归属该表"
159                    // If there are NO explicit tables AND exactly 1 implicit derived table,
160                    // unqualified columns should be linked to that implicit derived table
161                    List<INamespace> explicitTables = new ArrayList<>();
162                    List<INamespace> implicitDerivedTables = new ArrayList<>();
163
164                    for (ScopeChild child : fromScope.getChildren()) {
165                        INamespace ns = child.getNamespace();
166                        TTable table = ns.getFinalTable();
167                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
168                            implicitDerivedTables.add(ns);
169                        } else {
170                            explicitTables.add(ns);
171                        }
172                    }
173
174                    // If no explicit tables and exactly 1 implicit derived table,
175                    // include it for unqualified column resolution
176                    boolean useSoleImplicitDerivedTable = explicitTables.isEmpty() &&
177                                                           implicitDerivedTables.size() == 1;
178
179                    // Search through all visible namespaces
180                    List<INamespace> candidates = new ArrayList<>();
181                    List<INamespace> maybeCandidates = new ArrayList<>();
182
183                    for (ScopeChild child : fromScope.getChildren()) {
184                        INamespace ns = child.getNamespace();
185
186                        // Skip implicit lateral derived tables (Teradata-specific)
187                        // They should only match qualified references like "employee.employee_id"
188                        // UNLESS this is a "sole implicit derived table" scenario where there
189                        // are no explicit tables and exactly 1 implicit derived table
190                        TTable table = ns.getFinalTable();
191                        if (table != null && table.getEffectType() == ETableEffectType.tetImplicitLateralDerivedTable) {
192                            if (!useSoleImplicitDerivedTable) {
193                                continue;
194                            }
195                        }
196
197                        ColumnLevel level = ns.hasColumn(name);
198
199                        if (level == ColumnLevel.EXISTS) {
200                            candidates.add(ns);
201                        } else if (level == ColumnLevel.MAYBE) {
202                            // Only include MAYBE matches for namespaces that support
203                            // dynamic inference (subqueries/CTEs with star columns)
204                            // Physical tables without metadata should NOT be candidates
205                            if (ns.supportsDynamicInference()) {
206                                maybeCandidates.add(ns);
207                            }
208                        }
209                    }
210
211                    // If we found exact matches, use them
212                    if (!candidates.isEmpty()) {
213                        for (INamespace ns : candidates) {
214                            resolved.found(ns, false, this, new ResolvePath(), names);
215                        }
216                        foundLocally = true;
217                    }
218                    // If we found MAYBE matches (star columns), only use if exactly one
219                    // Multiple MAYBE candidates means ambiguous - don't resolve
220                    else if (maybeCandidates.size() == 1) {
221                        resolved.found(maybeCandidates.get(0), false, this, new ResolvePath(), names);
222                        foundLocally = true;
223                    }
224                    // If multiple MAYBE candidates, leave as unresolved (ambiguous)
225                }
226            }
227        }
228
229        // If not found locally, delegate to parent
230        if (!foundLocally) {
231            parent.resolve(names, matcher, deep, resolved);
232        }
233    }
234
235    @Override
236    public String toString() {
237        return String.format("DeleteScope(from=%s)", fromScope != null ? "present" : "null");
238    }
239}