001package gudusoft.gsqlparser.ir.builder.mssql;
002
003import gudusoft.gsqlparser.*;
004import gudusoft.gsqlparser.analyzer.v2.AnalyzerV2Config;
005import gudusoft.gsqlparser.ir.bound.*;
006import gudusoft.gsqlparser.ir.common.*;
007import gudusoft.gsqlparser.nodes.*;
008import gudusoft.gsqlparser.stmt.*;
009import gudusoft.gsqlparser.stmt.mssql.*;
010
011import java.util.*;
012
013/**
014 * T-SQL AST visitor that collects routine declarations, call references,
015 * variable declarations, and table references from SQL Server scripts.
016 * <p>
017 * Extends {@link TParseTreeVisitor} and uses preVisit/postVisit pairs
018 * for scope-creating nodes and preVisit only for leaf collection.
019 */
020public class TsqlSymbolCollector extends TParseTreeVisitor {
021
022    /** T-SQL XML data type methods and .WRITE() column update method.
023     *  When these appear as something.method(), they are NOT user-defined function calls. */
024    private static final Set<String> DOT_METHOD_NAMES = new HashSet<String>();
025    static {
026        // XML data type methods
027        DOT_METHOD_NAMES.add("NODES");
028        DOT_METHOD_NAMES.add("VALUE");
029        DOT_METHOD_NAMES.add("QUERY");
030        DOT_METHOD_NAMES.add("EXIST");
031        DOT_METHOD_NAMES.add("MODIFY");
032        // .WRITE() partial column update
033        DOT_METHOD_NAMES.add("WRITE");
034    }
035
036    private final BoundProgram program;
037    private final Deque<BoundScope> scopeStack;
038    private final Deque<String> routineIdStack;
039    private final Set<TFunctionCall> visitedFunctionCalls;
040    private final AnalyzerV2Config config;
041    private final String fileId;
042    private int batchCounter = 0;
043
044    public TsqlSymbolCollector(BoundProgram program, BoundScope globalScope,
045                               AnalyzerV2Config config, String fileId) {
046        this.program = program;
047        this.scopeStack = new ArrayDeque<BoundScope>();
048        this.routineIdStack = new ArrayDeque<String>();
049        this.visitedFunctionCalls = new HashSet<TFunctionCall>();
050        this.config = config;
051        this.fileId = fileId != null ? fileId : "";
052
053        scopeStack.push(globalScope);
054    }
055
056    // ---- Scope helpers ----
057
058    private BoundScope currentScope() {
059        return scopeStack.peek();
060    }
061
062    private BoundScope pushScope(EScopeKind kind, SourceAnchor anchor) {
063        BoundScope scope = new BoundScope(kind, currentScope(), anchor);
064        scopeStack.push(scope);
065        program.addScope(scope);
066        return scope;
067    }
068
069    private void popScope() {
070        if (scopeStack.size() > 1) {
071            scopeStack.pop();
072        }
073    }
074
075    private String currentRoutineId() {
076        return routineIdStack.isEmpty() ? null : routineIdStack.peek();
077    }
078
079    private SourceAnchor anchor(TParseTreeNode node) {
080        SourceAnchor base = SourceAnchor.from(node);
081        if (base != null && !fileId.isEmpty()) {
082            return new SourceAnchor(fileId,
083                    base.startOffset, base.endOffset,
084                    base.startLine, base.startCol,
085                    base.endLine, base.endCol,
086                    base.statementKey, base.snippet);
087        }
088        return base;
089    }
090
091    // ==========================================
092    // Routine declaration visitors
093    // ==========================================
094
095    // ---- CREATE PROCEDURE ----
096    // Note: TMssqlCreateProcedure extends TCreateProcedureStmt.
097    // The visitor dispatches to TCreateProcedureStmt, not TMssqlCreateProcedure.
098
099    @Override
100    public void preVisit(TCreateProcedureStmt node) {
101        // Only handle MSSQL procedures
102        if (node.dbvendor != EDbVendor.dbvmssql) return;
103
104        TObjectName procName = node.getStoredProcedureName();
105        if (procName == null) return;
106
107        String rawName = procName.toString().trim();
108        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName);
109
110        String schema = normalized.getSchema();
111        String name = normalized.getObject();
112
113        List<BoundParameterSymbol> params = extractParameters(node.getParameterDeclarations());
114
115        BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node));
116
117        BoundRoutineSymbol routine = new BoundRoutineSymbol(
118                name.toUpperCase(), schema != null ? schema.toUpperCase() : null,
119                routineScope, anchor(node), params, null, ERoutineKind.PROCEDURE);
120        program.registerRoutine(routine);
121        routineIdStack.push(routine.getRoutineId());
122
123        // Register params in scope
124        for (BoundParameterSymbol p : params) {
125            routineScope.declareSymbol(p.getParamName(), p);
126        }
127    }
128
129    @Override
130    public void postVisit(TCreateProcedureStmt node) {
131        if (node.dbvendor != EDbVendor.dbvmssql) return;
132        if (!routineIdStack.isEmpty()) routineIdStack.pop();
133        popScope();
134    }
135
136    // ---- CREATE FUNCTION ----
137
138    @Override
139    public void preVisit(TCreateFunctionStmt node) {
140        if (!(node instanceof TMssqlCreateFunction)) return;
141
142        TObjectName funcName = node.getFunctionName();
143        if (funcName == null) return;
144
145        String rawName = funcName.toString().trim();
146        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName);
147        String schema = normalized.getSchema();
148        String name = normalized.getObject();
149
150        List<BoundParameterSymbol> params = extractParameters(node.getParameterDeclarations());
151
152        ERoutineKind kind;
153        EFunctionReturnsType returnsType = node.getReturnsType();
154        if (returnsType == EFunctionReturnsType.frtInlineTableValue) {
155            kind = ERoutineKind.FUNCTION; // IF
156        } else if (returnsType == EFunctionReturnsType.frtMultiStatementTableValue) {
157            kind = ERoutineKind.FUNCTION; // TF
158        } else {
159            kind = ERoutineKind.FUNCTION; // SF
160        }
161
162        BoundTypeRef returnType = MssqlTypeRefMapper.map(node.getReturnDataType());
163        BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node));
164
165        BoundRoutineSymbol routine = new BoundRoutineSymbol(
166                name.toUpperCase(), schema != null ? schema.toUpperCase() : null,
167                routineScope, anchor(node), params, returnType, kind);
168
169        // Note: function sub-type (SF/IF/TF) not stored on BoundRoutineSymbol
170        // since it lacks a properties map. The kindCode() always returns "F".
171        // Sub-type can be inferred from the source anchor if needed.
172
173        program.registerRoutine(routine);
174        routineIdStack.push(routine.getRoutineId());
175
176        for (BoundParameterSymbol p : params) {
177            routineScope.declareSymbol(p.getParamName(), p);
178        }
179    }
180
181    @Override
182    public void postVisit(TCreateFunctionStmt node) {
183        if (!(node instanceof TMssqlCreateFunction)) return;
184        if (!routineIdStack.isEmpty()) routineIdStack.pop();
185        popScope();
186    }
187
188    // ---- CREATE TRIGGER ----
189    // Note: MSSQL triggers parse as TCreateTriggerStmt, not TMssqlCreateTrigger.
190
191    @Override
192    public void preVisit(TCreateTriggerStmt node) {
193        if (node.dbvendor != EDbVendor.dbvmssql) return;
194
195        TObjectName triggerName = node.getTriggerName();
196        if (triggerName == null) return;
197
198        String rawName = triggerName.toString().trim();
199        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName);
200        String schema = normalized.getSchema();
201        String name = normalized.getObject();
202
203        BoundScope routineScope = pushScope(EScopeKind.ROUTINE, anchor(node));
204
205        BoundRoutineSymbol routine = new BoundRoutineSymbol(
206                name.toUpperCase(), schema != null ? schema.toUpperCase() : null,
207                routineScope, anchor(node),
208                Collections.<BoundParameterSymbol>emptyList(), null, ERoutineKind.TRIGGER);
209
210        // Track trigger table via object ref
211        TTable onTable = node.getOnTable();
212        if (onTable != null) {
213            collectTableRef(onTable, null);
214        }
215
216        program.registerRoutine(routine);
217        routineIdStack.push(routine.getRoutineId());
218
219        // Register inserted/deleted pseudo tables in trigger scope
220        routineScope.declareSymbol("INSERTED",
221                new BoundVariableSymbol("INSERTED", routineScope, null, null, false));
222        routineScope.declareSymbol("DELETED",
223                new BoundVariableSymbol("DELETED", routineScope, null, null, false));
224    }
225
226    @Override
227    public void postVisit(TCreateTriggerStmt node) {
228        if (node.dbvendor != EDbVendor.dbvmssql) return;
229        if (!routineIdStack.isEmpty()) routineIdStack.pop();
230        popScope();
231    }
232
233    // ==========================================
234    // Control flow / scope visitors
235    // ==========================================
236
237    @Override
238    public void preVisit(TMssqlBlock node) {
239        pushScope(EScopeKind.BLOCK, anchor(node));
240    }
241
242    @Override
243    public void postVisit(TMssqlBlock node) {
244        popScope();
245    }
246
247    @Override
248    public void preVisit(TMssqlTryCatch node) {
249        // TRY...CATCH treated as a single scope unit; calls inside
250        // inherit the current routine context (owningRoutineId).
251        // No extra scope push needed — the body statements are visited normally.
252    }
253
254    // ==========================================
255    // Variable declaration
256    // ==========================================
257
258    @Override
259    public void preVisit(TMssqlDeclare node) {
260        if (node.getVariables() != null) {
261            TDeclareVariableList vars = node.getVariables();
262            for (int i = 0; i < vars.size(); i++) {
263                TDeclareVariable var = vars.getDeclareVariable(i);
264                if (var.getVariableName() != null) {
265                    String varName = var.getVariableName().toString().trim();
266                    BoundTypeRef typeRef = var.getDatatype() != null
267                            ? MssqlTypeRefMapper.map(var.getDatatype()) : null;
268                    BoundVariableSymbol varSym = new BoundVariableSymbol(
269                            varName, currentScope(), anchor(node), typeRef, false);
270                    currentScope().declareSymbol(varName.toUpperCase(), varSym);
271                }
272            }
273        }
274        // Cursor declaration
275        if (node.getDeclareType() == EDeclareType.cursor && node.getCursorName() != null) {
276            String cursorName = node.getCursorName().toString().trim();
277            BoundVariableSymbol cursorSym = new BoundVariableSymbol(
278                    cursorName, currentScope(), anchor(node),
279                    new BoundTypeRef("CURSOR", ETypeCategory.REF_CURSOR), false);
280            currentScope().declareSymbol(cursorName.toUpperCase(), cursorSym);
281        }
282    }
283
284    // ==========================================
285    // Call reference collection
286    // ==========================================
287
288    // ---- EXEC / EXECUTE ----
289
290    @Override
291    public void preVisit(TMssqlExecute node) {
292        int execType = node.getExecType();
293
294        if (execType == TBaseType.metExecSp || execType == TBaseType.metNoExecKeyword) {
295            handleExecModule(node);
296        } else if (execType == TBaseType.metExecStringCmd
297                || execType == TBaseType.metExecStringCmdLinkServer) {
298            handleDynamicExecString(node);
299        }
300    }
301
302    private void handleExecModule(TMssqlExecute node) {
303        TObjectName moduleName = node.getModuleName();
304        if (moduleName == null) return;
305
306        String rawName = moduleName.toString().trim();
307        if (rawName.isEmpty()) return;
308
309        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(rawName);
310
311        // Cross-database or remote call → external dependency
312        if (normalized.getPartCount() >= 4) {
313            addExternalRef(rawName, "REMOTE_CALL", node);
314            return;
315        }
316        if (normalized.getPartCount() >= 3 && normalized.getDatabase() != null) {
317            addExternalRef(rawName, "CROSS_DATABASE", node);
318            return;
319        }
320
321        // Check if sp_executesql
322        if (MssqlExternalDepClassifier.isSpExecuteSql(rawName)) {
323            addExternalRef(rawName, "SYSTEM_PROC", node);
324            handleSpExecuteSql(node);
325            return;
326        }
327
328        // System / extended proc
329        if (MssqlExternalDepClassifier.isExternal(rawName)) {
330            String extType = MssqlExternalDepClassifier.classify(rawName);
331            addExternalRef(rawName, extType, node);
332            return;
333        }
334
335        // User procedure call
336        int argCount = node.getParameters() != null ? node.getParameters().size() : 0;
337        addRoutineRef(rawName, argCount, node, Confidence.HIGH, "EXEC module");
338    }
339
340    private void handleSpExecuteSql(TMssqlExecute node) {
341        // Try to extract the SQL literal from the first parameter
342        String sqlText = null;
343        if (node.getParameters() != null && node.getParameters().size() > 0) {
344            TExecParameter firstParam = node.getParameters().getExecParameter(0);
345            if (firstParam != null && firstParam.getParameterValue() != null) {
346                sqlText = extractStringLiteral(firstParam.getParameterValue());
347            }
348        }
349        // Also check node.getSqlText() which the parser may have resolved
350        if (sqlText == null) {
351            sqlText = node.getSqlText();
352        }
353
354        if (sqlText != null && !sqlText.isEmpty()) {
355            parseInnerSql(sqlText);
356        }
357    }
358
359    private void handleDynamicExecString(TMssqlExecute node) {
360        // EXEC('sql string') or EXEC(@variable)
361        String sqlText = null;
362        if (node.getStringValues() != null && node.getStringValues().size() > 0) {
363            TExpression firstExpr = node.getStringValues().getExpression(0);
364            if (firstExpr != null) {
365                sqlText = extractStringLiteral(firstExpr);
366            }
367        }
368        if (sqlText == null) {
369            sqlText = node.getSqlText();
370        }
371
372        if (sqlText != null && !sqlText.isEmpty()) {
373            parseInnerSql(sqlText);
374        }
375
376        // Record dynamic SQL as external dependency
377        BoundRoutineRef ref = createRoutineRef(
378                "EXEC_DYNAMIC_SQL", 0, node, Confidence.LOW, "EXEC string");
379        ref.setProperty("externalDependency", true);
380        ref.setProperty("externalType", "DYNAMIC_SQL");
381        program.addRoutineRef(ref);
382    }
383
384    /**
385     * Attempts to parse inner SQL from dynamic SQL and extract refs.
386     */
387    private void parseInnerSql(String sqlText) {
388        try {
389            TGSqlParser innerParser = new TGSqlParser(EDbVendor.dbvmssql);
390            innerParser.sqltext = sqlText;
391            if (innerParser.parse() == 0) {
392                // Extract table refs from inner SQL
393                for (int i = 0; i < innerParser.sqlstatements.size(); i++) {
394                    TCustomSqlStatement stmt = innerParser.sqlstatements.get(i);
395                    if (stmt != null && stmt.tables != null) {
396                        for (int j = 0; j < stmt.tables.size(); j++) {
397                            TTable table = stmt.tables.getTable(j);
398                            collectTableRef(table, stmt);
399                        }
400                    }
401                }
402            }
403        } catch (Exception e) {
404            // Ignore parse failures in dynamic SQL
405        }
406    }
407
408    // ---- SET @var = expr (expression not traversed by TMssqlSet.acceptChildren) ----
409
410    @Override
411    public void preVisit(TMssqlSet node) {
412        if (node.getSetType() == TBaseType.mstLocalVar && node.getVarExpr() != null) {
413            // TMssqlSet.acceptChildren() does not traverse varExpr,
414            // so function calls in SET assignments are missed.
415            // Manually traverse the expression tree to pick up TFunctionCall nodes.
416            node.getVarExpr().acceptChildren(this);
417        }
418    }
419
420    // ---- Function calls in expressions ----
421
422    @Override
423    public void preVisit(TFunctionCall funcCall) {
424        if (visitedFunctionCalls.contains(funcCall)) return;
425        visitedFunctionCalls.add(funcCall);
426
427        if (funcCall.getFunctionName() == null) return;
428        String funcName = funcCall.getFunctionName().toString().trim();
429        if (funcName.isEmpty()) return;
430
431        // Filter XML data type methods (.nodes/.value/.query/.exist/.modify)
432        // and .WRITE() partial column update. These produce TFunctionCall nodes
433        // but are NOT user-defined function calls. Only filter when the name
434        // has a dot prefix (e.g., "alias.value", "@var.nodes", "col.WRITE").
435        int lastDot = funcName.lastIndexOf('.');
436        if (lastDot > 0) {
437            String lastPart = funcName.substring(lastDot + 1).toUpperCase();
438            if (DOT_METHOD_NAMES.contains(lastPart)) {
439                return;
440            }
441        }
442
443        // Skip known aggregate/window functions that the parser handles
444        EFunctionType ftype = funcCall.getFunctionType();
445        if (isParserHandledFunction(ftype)) {
446            // These are SQL-level aggregates/window functions, not user UDF calls.
447            // Still mark as external if they are built-in.
448            if (MssqlExternalDepClassifier.isExternal(funcName)) {
449                addExternalRef(funcName, MssqlExternalDepClassifier.classify(funcName), funcCall);
450            }
451            return;
452        }
453
454        MssqlNameNormalizer.NormalizedName normalized = MssqlNameNormalizer.normalize(funcName);
455
456        // Cross-database/remote → external
457        if (normalized.getPartCount() >= 3 && normalized.getDatabase() != null) {
458            addExternalRef(funcName, "CROSS_DATABASE", funcCall);
459            return;
460        }
461
462        // Check if known external
463        if (MssqlExternalDepClassifier.isExternal(funcName)) {
464            addExternalRef(funcName, MssqlExternalDepClassifier.classify(funcName), funcCall);
465            return;
466        }
467
468        // Check if it's a declared variable (not a function call)
469        String objectPart = normalized.getObject();
470        if (objectPart.startsWith("@")) {
471            // @variable is not a function call
472            return;
473        }
474
475        // User-defined function call
476        int argCount = 0;
477        if (funcCall.getArgs() != null) {
478            argCount = funcCall.getArgs().size();
479        }
480        addRoutineRef(funcName, argCount, funcCall, Confidence.HIGH, "function call in expression");
481    }
482
483    private boolean isParserHandledFunction(EFunctionType ftype) {
484        if (ftype == null) return false;
485        switch (ftype) {
486            case cast_t:
487            case convert_t:
488            case trim_t:
489            case extract_t:
490            case treat_t:
491            case contains_t:
492            case freetext_t:
493            case rank_t:
494            case builtin_t:
495                return true;
496            default:
497                return false;
498        }
499    }
500
501    // ==========================================
502    // Table reference collection
503    // ==========================================
504
505    @Override
506    public void preVisit(TSelectSqlStatement node) {
507        // Only process leaf SELECTs (not UNION containers)
508        if (node.getSetOperatorType() != ESetOperatorType.none) return;
509        collectTablesFromStatement(node);
510    }
511
512    @Override
513    public void preVisit(TInsertSqlStatement node) {
514        collectTablesFromStatement(node);
515    }
516
517    @Override
518    public void preVisit(TUpdateSqlStatement node) {
519        collectTablesFromStatement(node);
520    }
521
522    @Override
523    public void preVisit(TDeleteSqlStatement node) {
524        collectTablesFromStatement(node);
525    }
526
527    @Override
528    public void preVisit(TMergeSqlStatement node) {
529        collectTablesFromStatement(node);
530    }
531
532    private void collectTablesFromStatement(TCustomSqlStatement stmt) {
533        if (stmt == null || stmt.tables == null) return;
534        for (int i = 0; i < stmt.tables.size(); i++) {
535            TTable table = stmt.tables.getTable(i);
536            collectTableRef(table, stmt);
537        }
538    }
539
540    private void collectTableRef(TTable table, TCustomSqlStatement stmt) {
541        if (table == null || table.getTableName() == null) return;
542        String tableName = table.getTableName().toString().trim();
543        if (tableName.isEmpty()) return;
544
545        // Classify table
546        String category = classifyTable(tableName, table);
547
548        List<String> nameParts = MssqlNameNormalizer.splitParts(tableName);
549        for (int i = 0; i < nameParts.size(); i++) {
550            nameParts.set(i, MssqlNameNormalizer.stripQuotes(nameParts.get(i)));
551        }
552
553        EObjectRefKind refKind = mapTableType(table.getTableType());
554
555        BoundObjectRef objRef = new BoundObjectRef(
556                tableName, nameParts,
557                EBindingStatus.UNRESOLVED_SOFT, null, null,
558                refKind, null);
559        objRef.setSourceAnchor(anchor(table));
560        objRef.setProperty("owningRoutineId", currentRoutineId());
561
562        // Set table effect type
563        ETableEffectType effectType = table.getEffectType();
564        if (effectType != null) {
565            objRef.setProperty("tableEffectType", effectType);
566        }
567        // Fallback: statement type
568        if (stmt != null) {
569            objRef.setProperty("statementType", stmt.sqlstatementtype);
570        }
571
572        // Mssql table category
573        objRef.setProperty("mssql.tableCategory", category);
574
575        program.addObjectRef(objRef);
576    }
577
578    private String classifyTable(String tableName, TTable table) {
579        String upper = tableName.toUpperCase().trim();
580        String stripped = MssqlNameNormalizer.stripQuotes(upper);
581        // Check last part for prefix patterns
582        int dot = stripped.lastIndexOf('.');
583        String objectPart = dot >= 0 ? stripped.substring(dot + 1) : stripped;
584
585        if (objectPart.equals("INSERTED") || objectPart.equals("DELETED")) {
586            return "PSEUDO";
587        }
588        if (objectPart.startsWith("##")) {
589            return "GLOBAL_TEMP";
590        }
591        if (objectPart.startsWith("#")) {
592            return "TEMP";
593        }
594        if (objectPart.startsWith("@")) {
595            return "TABLE_VARIABLE";
596        }
597        return "REAL";
598    }
599
600    private EObjectRefKind mapTableType(ETableSource tableType) {
601        if (tableType == null) return EObjectRefKind.TABLE;
602        switch (tableType) {
603            case subquery:
604                return EObjectRefKind.DERIVED_TABLE;
605            case function:
606                return EObjectRefKind.TABLE_FUNCTION;
607            case pivoted_table:
608                return EObjectRefKind.PIVOT_TABLE;
609            default:
610                return EObjectRefKind.TABLE;
611        }
612    }
613
614    // ==========================================
615    // Anonymous batch handling
616    // ==========================================
617
618    /**
619     * Called for top-level DML statements that are not inside any routine definition.
620     * These are grouped as anonymous batches.
621     */
622    private void ensureAnonymousBatchContext(TParseTreeNode node) {
623        if (currentRoutineId() == null) {
624            // Create anonymous batch routine
625            batchCounter++;
626            String batchName = "<batch_" + batchCounter + ">";
627
628            BoundScope batchScope = pushScope(EScopeKind.ROUTINE, anchor(node));
629            BoundRoutineSymbol batch = new BoundRoutineSymbol(
630                    batchName, null, batchScope, anchor(node),
631                    Collections.<BoundParameterSymbol>emptyList(), null,
632                    ERoutineKind.ANONYMOUS_BLOCK);
633            program.registerRoutine(batch);
634            routineIdStack.push(batch.getRoutineId());
635        }
636    }
637
638    // ==========================================
639    // Helper methods
640    // ==========================================
641
642    private List<BoundParameterSymbol> extractParameters(TParameterDeclarationList paramList) {
643        List<BoundParameterSymbol> params = new ArrayList<BoundParameterSymbol>();
644        if (paramList == null) return params;
645
646        for (int i = 0; i < paramList.size(); i++) {
647            TParameterDeclaration pd = paramList.getParameterDeclarationItem(i);
648            if (pd == null || pd.getParameterName() == null) continue;
649
650            String paramName = pd.getParameterName().toString().trim();
651            BoundTypeRef typeRef = MssqlTypeRefMapper.map(pd.getDataType());
652
653            gudusoft.gsqlparser.ir.bound.EParameterMode mode =
654                    gudusoft.gsqlparser.ir.bound.EParameterMode.IN;
655            gudusoft.gsqlparser.EParameterMode pdMode = pd.getParameterMode();
656            if (pdMode == gudusoft.gsqlparser.EParameterMode.output
657                    || pdMode == gudusoft.gsqlparser.EParameterMode.out
658                    || pdMode == gudusoft.gsqlparser.EParameterMode.inout) {
659                mode = gudusoft.gsqlparser.ir.bound.EParameterMode.IN_OUT;
660            }
661
662            BoundParameterSymbol param = new BoundParameterSymbol(
663                    paramName, null, anchor(pd), typeRef, mode);
664            params.add(param);
665        }
666        return params;
667    }
668
669    private void addRoutineRef(String rawName, int argCount, TParseTreeNode node,
670                               Confidence confidence, String evidenceMsg) {
671        BoundRoutineRef ref = createRoutineRef(rawName, argCount, node, confidence, evidenceMsg);
672        program.addRoutineRef(ref);
673    }
674
675    private BoundRoutineRef createRoutineRef(String rawName, int argCount,
676                                              TParseTreeNode node,
677                                              Confidence confidence, String evidenceMsg) {
678        List<String> nameParts = MssqlNameNormalizer.splitParts(rawName);
679        for (int i = 0; i < nameParts.size(); i++) {
680            nameParts.set(i, MssqlNameNormalizer.stripQuotes(nameParts.get(i)));
681        }
682
683        Evidence evidence = new Evidence(EvidenceKind.STATIC_RESOLVED, evidenceMsg);
684        BoundRoutineRef ref = new BoundRoutineRef(
685                rawName, nameParts,
686                EBindingStatus.UNRESOLVED_SOFT, null, null,
687                Collections.<BoundArgument>emptyList(),
688                evidence, confidence);
689        ref.setSourceAnchor(anchor(node));
690        ref.setProperty("owningRoutineId", currentRoutineId());
691        ref.setProperty("argCount", argCount);
692        return ref;
693    }
694
695    private void addExternalRef(String rawName, String externalType, TParseTreeNode node) {
696        BoundRoutineRef ref = createRoutineRef(rawName, 0, node, Confidence.HIGH,
697                "External: " + externalType + " " + rawName);
698        ref.setProperty("externalDependency", true);
699        ref.setProperty("externalType", externalType);
700        ref.setProperty("externalName", MssqlNameNormalizer.stripQuotes(rawName));
701        if (MssqlExternalDepClassifier.isSecuritySensitive(rawName)) {
702            ref.setProperty("securitySensitive", true);
703        }
704        program.addRoutineRef(ref);
705    }
706
707    private String extractStringLiteral(TExpression expr) {
708        if (expr == null) return null;
709        String text = expr.toString().trim();
710        // Strip N prefix and surrounding quotes
711        if (text.toUpperCase().startsWith("N'") && text.endsWith("'") && text.length() >= 3) {
712            return text.substring(2, text.length() - 1).replace("''", "'");
713        }
714        if (text.startsWith("'") && text.endsWith("'") && text.length() >= 2) {
715            return text.substring(1, text.length() - 1).replace("''", "'");
716        }
717        return null;
718    }
719}