001package gudusoft.gsqlparser.dlineage;
002
003import gudusoft.gsqlparser.EDbVendor;
004import gudusoft.gsqlparser.TBaseType;
005import gudusoft.gsqlparser.TGSqlParser;
006import gudusoft.gsqlparser.dlineage.dataflow.listener.DataFlowHandleListener;
007import gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader;
008import gudusoft.gsqlparser.dlineage.dataflow.metadata.grabit.GrabitMetadataAnalyzer;
009import gudusoft.gsqlparser.dlineage.dataflow.metadata.sqlflow.SqlflowMetadataAnalyzer;
010import gudusoft.gsqlparser.dlineage.dataflow.metadata.sqlflow.sharded.SqlflowShardedMetadataAnalyzer;
011import gudusoft.gsqlparser.dlineage.dataflow.model.*;
012import gudusoft.gsqlparser.dlineage.dataflow.model.json.Coordinate;
013import gudusoft.gsqlparser.dlineage.dataflow.model.xml.*;
014import gudusoft.gsqlparser.dlineage.dataflow.sqlenv.SQLEnvParser;
015import gudusoft.gsqlparser.dlineage.util.*;
016import gudusoft.gsqlparser.sqlenv.ESQLDataObjectType;
017import gudusoft.gsqlparser.sqlenv.TSQLEnv;
018
019import gudusoft.gsqlparser.util.IndexedLinkedHashMap;
020import gudusoft.gsqlparser.util.Logger;
021import gudusoft.gsqlparser.util.LoggerFactory;
022import gudusoft.gsqlparser.util.SQLUtil;
023import gudusoft.gsqlparser.util.json.JSON;
024
025import java.io.File;
026import java.io.FileInputStream;
027import java.io.IOException;
028import java.io.BufferedReader;
029import java.io.InputStreamReader;
030import java.util.zip.GZIPInputStream;
031import java.util.*;
032import java.util.concurrent.*;
033import java.util.concurrent.atomic.AtomicInteger;
034
035public class ParallelDataFlowAnalyzer implements IDataFlowAnalyzer {
036    private static final Logger logger = LoggerFactory.getLogger(ParallelDataFlowAnalyzer.class);
037    private SqlInfo[] sqlInfos;
038    private Option option = new Option();
039    private TSQLEnv sqlenv = null;
040    private dataflow dataflow;
041    private String dataflowString;
042    private List<ErrorInfo> errorInfos = new CopyOnWriteArrayList<ErrorInfo>();
043    private IndexedLinkedHashMap<String, List<SqlInfo>> sqlInfoMap = new IndexedLinkedHashMap<String, List<SqlInfo>>();
044    //autogenerate 并发任务在 run() 中并发 putAll,必须使用并发容器,否则 HashMap 并发写入会丢失/损坏条目
045    private final Map<String, String> hashSQLMap = new ConcurrentHashMap<>();
046    private dataflow metadataFlow;
047
048    public ParallelDataFlowAnalyzer(SqlInfo[] sqlInfos, EDbVendor dbVendor, boolean simpleOutput) {
049        this.sqlInfos = sqlInfos;
050        option.setVendor(dbVendor);
051        option.setSimpleOutput(simpleOutput);
052        ModelBindingManager.setGlobalOption(option);
053    }
054
055    public ParallelDataFlowAnalyzer(String[] sqlContents, EDbVendor dbVendor, boolean simpleOutput, String defaultServer, String defaultDatabase, String defaltSchema) {
056        SqlInfo[] sqlInfos = new SqlInfo[sqlContents.length];
057        for (int i = 0; i < sqlContents.length; i++) {
058            SqlInfo info = new SqlInfo();
059            info.setSql(sqlContents[i]);
060            info.setOriginIndex(0);
061            sqlInfos[i] = info;
062        }
063        option.setVendor(dbVendor);
064        option.setSimpleOutput(simpleOutput);
065                option.setDefaultServer(defaultServer);
066                option.setDefaultDatabase(defaultDatabase);
067                option.setDefaultSchema(defaltSchema);
068        this.sqlInfos = convertSQL(dbVendor, JSON.toJSONString(sqlInfos)).toArray(new SqlInfo[0]);
069    }
070    
071    public ParallelDataFlowAnalyzer(String[] sqlContents, EDbVendor dbVendor, boolean simpleOutput) {
072        SqlInfo[] sqlInfos = new SqlInfo[sqlContents.length];
073        for (int i = 0; i < sqlContents.length; i++) {
074            SqlInfo info = new SqlInfo();
075            info.setSql(sqlContents[i]);
076            info.setOriginIndex(0);
077            sqlInfos[i] = info;
078        }
079        option.setVendor(dbVendor);
080        option.setSimpleOutput(simpleOutput);
081        this.sqlInfos = convertSQL(dbVendor, JSON.toJSONString(sqlInfos)).toArray(new SqlInfo[0]);
082    }
083
084    public ParallelDataFlowAnalyzer(SqlInfo[] sqlInfos, Option option) {
085        this.sqlInfos = sqlInfos;
086        this.option = option;
087        ModelBindingManager.setGlobalOption(option);
088    }
089
090    public ParallelDataFlowAnalyzer(File[] sqlFiles, Option option) {
091        SqlInfo[] sqlInfos = new SqlInfo[sqlFiles.length];
092        for (int i = 0; i < sqlFiles.length; i++) {
093            SqlInfo info = new SqlInfo();
094            info.setSql(SQLUtil.getFileContent(sqlFiles[i]));
095            info.setFileName(sqlFiles[i].getName());
096            info.setFilePath(sqlFiles[i].getAbsolutePath());
097            info.setOriginIndex(0);
098            sqlInfos[i] = info;
099        }
100        this.sqlInfos = sqlInfos;
101        this.option = option;
102        ModelBindingManager.setGlobalOption(option);
103    }
104    
105    public ParallelDataFlowAnalyzer(File[] sqlFiles, Option option, int splitSizeMB, File splitDir) {
106        List<SqlInfo> sqlInfoList = new ArrayList<SqlInfo>();
107        for (int i = 0; i < sqlFiles.length; i++) {
108            try {
109                // Ensure split directory exists
110                if (!splitDir.exists() && !splitDir.mkdirs()) {
111                    throw new IOException("Failed to create split directory: " + splitDir.getAbsolutePath());
112                }
113                
114                // Split the file if it's large
115                List<File> splitFiles = gudusoft.gsqlparser.util.FileSplitter.splitFile(sqlFiles[i], splitDir, splitSizeMB, option.getVendor());
116                
117                if (splitFiles.isEmpty()) {
118                    // If no split files were created, process the original file
119                    SqlInfo info = new SqlInfo();
120                    info.setSql(SQLUtil.getFileContent(sqlFiles[i]));
121                    info.setFileName(sqlFiles[i].getName());
122                    info.setFilePath(sqlFiles[i].getAbsolutePath());
123                    info.setOriginIndex(0);
124                    sqlInfoList.add(info);
125                } else {
126                    // Process each split file
127                    for (File splitFile : splitFiles) {
128                        SqlInfo info = new SqlInfo();
129                        info.setSql(SQLUtil.getFileContent(splitFile));
130                        info.setFileName(splitFile.getName());
131                        info.setFilePath(splitFile.getAbsolutePath());
132                        info.setOriginIndex(0);
133                        sqlInfoList.add(info);
134                    }
135                }
136            } catch (Exception e) {
137                sqlInfoList.clear();
138                logger.error("Error splitting file: " + sqlFiles[i].getAbsolutePath(), e);
139                // Fallback to original file if splitting fails
140                SqlInfo info = new SqlInfo();
141                info.setSql(SQLUtil.getFileContent(sqlFiles[i]));
142                info.setFileName(sqlFiles[i].getName());
143                info.setFilePath(sqlFiles[i].getAbsolutePath());
144                info.setOriginIndex(0);
145                sqlInfoList.add(info);
146            }
147        }
148        this.sqlInfos = sqlInfoList.toArray(new SqlInfo[0]);
149        this.option = option;
150        ModelBindingManager.setGlobalOption(option);
151    }
152    
153    public ParallelDataFlowAnalyzer(File[] sqlFiles, Option option, int splitSizeMB, String splitDirPath) {
154        this(sqlFiles, option, splitSizeMB, new File(splitDirPath));
155    }
156    
157    protected List<SqlInfo> convertSQL(EDbVendor vendor, String json) {
158                List<SqlInfo> sqlInfos = new ArrayList<SqlInfo>();
159                List sqlContents = (List) JSON.parseObject(json);
160                for (int j = 0; j < sqlContents.size(); j++) {
161                        Map sqlContent = (Map) sqlContents.get(j);
162                        String sql = (String) sqlContent.get("sql");
163                        String fileName = (String) sqlContent.get("fileName");
164                        String filePath = (String) sqlContent.get("filePath");
165                        if (sql != null && sql.trim().startsWith("{")) {
166                                if (sql.indexOf("createdBy") != -1) {
167                        // Exclude sqlflow-sharded manifests from the brand branch so a
168                        // SQLdep/grabit-branded sharded export routes to the sharded
169                        // reader below instead of the legacy queries[] path.
170                        if ((sql.toLowerCase().indexOf("sqldep") != -1 || sql.toLowerCase().indexOf("grabit") != -1)
171                                        && !gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader.isSqlflowSharded(sql)) {
172                                Map queryObject = (Map) JSON.parseObject(sql);
173                                        List querys = (List) queryObject.get("queries");
174                                        if (querys != null) {
175                                                for (int i = 0; i < querys.size(); i++) {
176                                                        Map object = (Map) querys.get(i);
177                                                        SqlInfo info = new SqlInfo();
178                                                        info.setSql(JSON.toJSONString(object));
179                                                        info.setFileName(fileName);
180                                                        info.setFilePath(filePath);
181                                                        info.setOriginIndex(i);
182                                                        sqlInfos.add(info);
183                                                }
184                                                queryObject.remove("queries");
185                                                SqlInfo info = new SqlInfo();
186                                                info.setSql(JSON.toJSONString(queryObject));
187                                                info.setFileName(fileName);
188                                                info.setFilePath(filePath);
189                                                info.setOriginIndex(querys.size());
190                                                sqlInfos.add(info);
191                                        } else {
192                                                SqlInfo info = new SqlInfo();
193                                                info.setSql(JSON.toJSONString(queryObject));
194                                                info.setFileName(fileName);
195                                                info.setFilePath(filePath);
196                                                info.setOriginIndex(0);
197                                                sqlInfos.add(info);
198                                        }
199                        }
200                        else if (sql.toLowerCase().indexOf("sqlflow") != -1) {
201                                                Map sqlflow = (Map) JSON.parseObject(sql);
202                                                if ("sqlflow-sharded".equals(sqlflow.get("format"))
203                                                                && !gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader.isSupportedSqlflowSharded(sql)) {
204                                                        // Unknown future formatVersion: do not consume it as a
205                                                        // version we understand.
206                                                } else if ("sqlflow-sharded".equals(sqlflow.get("format"))) {
207                                                        String baseDir = null;
208                                                        if (filePath != null) {
209                                                                baseDir = new File(filePath).getParent();
210                                                        }
211                                                        String sourceCompression = (String) sqlflow.get("sourceCompression");
212                                                        List<Map> servers = (List<Map>) sqlflow.get("servers");
213                                                        if (servers != null) {
214                                                                for (Map serverObject : servers) {
215                                                                        String name = (String) serverObject.get("name");
216                                                                        String dbVendor = (String) serverObject.get("dbVendor");
217                                                                        List<Map> databases = (List<Map>) serverObject.get("databases");
218                                                                        // Source shards are under databases[] (catalog topology) or
219                                                                        // schemas[] (schema topology, the natural Oracle shape). Read
220                                                                        // whichever is present; databases-only dropped every source
221                                                                        // record of a schema-topology manifest -> empty lineage.
222                                                                        List<Map> schemaShards = (List<Map>) serverObject.get("schemas");
223                                                                        if (schemaShards != null && !schemaShards.isEmpty()) {
224                                                                                databases = schemaShards;
225                                                                        }
226                                                                        if (databases != null) {
227                                                                                for (Map database : databases) {
228                                                                                        Map source = (Map) database.get("source");
229                                                                                        if (source != null) {
230                                                                                                String sourcePath = (String) source.get("path");
231                                                                                                if (sourcePath != null && baseDir != null) {
232                                                    File sourceFile = new File(baseDir, sourcePath);
233                                                    String fullPath = sourceFile.getAbsolutePath();
234                                                    if ("block".equals(sourceCompression)) {
235                                                                                                                readGzipBlockSource(fullPath, fileName, filePath, name, dbVendor, sqlInfos);
236                                                                                                        } else {
237                                                                                                                String sourceContent = SQLUtil.getFileContent(fullPath);
238                                                                                                                if (sourceContent != null) {
239                                                                                                                        String[] lines = sourceContent.split("\\r?\\n");
240                                                                                                                        for (int i = 0; i < lines.length; i++) {
241                                                                                                                                String line = lines[i].trim();
242                                                                                                                                if (line.isEmpty()) {
243                                                                                                                                        continue;
244                                                                                                                                }
245                                                                                                                                try {
246                                                                                                                                        Map sourceObject = (Map) JSON.parseObject(line);
247                                                                                                                                        String sourceCode = (String) sourceObject.get("sourceCode");
248                                                                                                                                        if (sourceCode != null && !sourceCode.isEmpty() && !DataFlowAnalyzer.isSourceUnavailable(sourceObject)) {
249                                                                                                                                                SqlInfo info = new SqlInfo();
250                                                                                                                                                info.setSql(sourceCode);
251                                                                                                                                                info.setFileName(sourceFile.getName());
252                                                                                                                                                info.setFilePath(sourceFile.getAbsolutePath());
253                                                                                                                                                info.setOriginIndex(i);
254                                                                                                                                                info.setDbVendor(dbVendor);
255                                                                                                                                                info.setServer(name);
256                                                                                                                                                sqlInfos.add(info);
257                                                                                                                                        }
258                                                                                                                                } catch (Exception e) {
259                                                                                                                                        logger.warn("Parse source jsonl line failed.", e);
260                                                                                                                                }
261                                                                                                                        }
262                                                                                                                }
263                                                                                                        }
264                                                                                                }
265                                                                                        }
266                                                                                }
267                                                                        }
268                                                                        SqlInfo serverInfo = new SqlInfo();
269                                                                        serverInfo.setSql(JSON.toJSONString(serverObject));
270                                                                        serverInfo.setFileName(fileName);
271                                                                        serverInfo.setFilePath(filePath);
272                                                                        serverInfo.setDbVendor(dbVendor);
273                                                                        serverInfo.setServer(name);
274                                                                        sqlInfos.add(serverInfo);
275                                                                }
276                                                        }
277                                                } else {
278                                                        List<Map> servers = (List<Map>) sqlflow.get("servers");
279                                                        if (servers != null) {
280                                                                for (Map queryObject : servers) {
281                                                                        String name = (String) queryObject.get("name");
282                                                                        String dbVendor = (String) queryObject.get("dbVendor");
283                                                                        List querys = (List) queryObject.get("queries");
284                                                                        if (querys != null) {
285                                                                                for (int i = 0; i < querys.size(); i++) {
286                                                                                        Map object = (Map) querys.get(i);
287                                                                                        SqlInfo info = new SqlInfo();
288                                                                                        info.setSql(JSON.toJSONString(object));
289                                                                                        info.setFileName(fileName);
290                                                                                        info.setFilePath(filePath);
291                                                                                        info.setOriginIndex(i);
292                                                                                        info.setDbVendor(dbVendor);
293                                                                                        info.setServer(name);
294                                                                                        sqlInfos.add(info);
295                                                                                }
296                                                                                queryObject.remove("queries");
297                                                                                SqlInfo info = new SqlInfo();
298                                                                                info.setSql(JSON.toJSONString(queryObject));
299                                                                                info.setFileName(fileName);
300                                                                                info.setFilePath(filePath);
301                                                                                info.setOriginIndex(querys.size());
302                                                                                info.setDbVendor(dbVendor);
303                                                                                info.setServer(filePath);
304                                                                                sqlInfos.add(info);
305                                                                        } else {
306                                                                                SqlInfo info = new SqlInfo();
307                                                                                info.setSql(JSON.toJSONString(queryObject));
308                                                                                info.setFileName(fileName);
309                                                                                info.setFilePath(filePath);
310                                                                                info.setOriginIndex(0);
311                                                                                sqlInfos.add(info);
312                                                                        }
313                                                                }
314                                                        }
315                                                }
316                                        }
317                    }
318                        } else if (sql != null) {
319                                SqlInfo info = new SqlInfo();
320                                info.setSql(sql);
321                                info.setFileName(fileName);
322                                info.setFilePath(filePath);
323                                info.setOriginIndex(0);
324                                sqlInfos.add(info);
325                        }
326                }
327                return sqlInfos;
328        }
329
330        private void readGzipBlockSource(String fullPath, String fileName, String filePath, String serverName, String dbVendor, List<SqlInfo> sqlInfos) {
331                try (FileInputStream fis = new FileInputStream(fullPath);
332                         GZIPInputStream gzis = new GZIPInputStream(fis);
333                         BufferedReader reader = new BufferedReader(new InputStreamReader(gzis, "UTF-8"))) {
334                        String line;
335                        int j = 0;
336                        while ((line = reader.readLine()) != null) {
337                                line = line.trim();
338                                if (line.isEmpty()) {
339                                        continue;
340                                }
341                                try {
342                                        Map sourceObject = (Map) JSON.parseObject(line);
343                                        String sourceCode = (String) sourceObject.get("sourceCode");
344                                        if (sourceCode != null && !sourceCode.isEmpty() && !DataFlowAnalyzer.isSourceUnavailable(sourceObject)) {
345                                                SqlInfo info = new SqlInfo();
346                                                info.setSql(sourceCode);
347                                                info.setFileName(fileName);
348                                                info.setFilePath(filePath);
349                                                info.setOriginIndex(j);
350                                                info.setDbVendor(dbVendor);
351                                                info.setServer(serverName);
352                                                sqlInfos.add(info);
353                                        }
354                                } catch (Exception e) {
355                                        logger.warn("Parse gzip source jsonl line failed.", e);
356                                }
357                                j++;
358                        }
359                } catch (Exception e) {
360                        logger.warn("Read gzip source file failed: " + fullPath, e);
361                }
362        }
363
364    @Override
365    public boolean isIgnoreRecordSet() {
366        return option.isIgnoreRecordSet();
367    }
368
369    @Override
370    public void setIgnoreRecordSet(boolean ignoreRecordSet) {
371        option.setIgnoreRecordSet(ignoreRecordSet);
372    }
373
374    @Override
375    public boolean isSimpleShowTopSelectResultSet() {
376        return option.isSimpleShowTopSelectResultSet();
377    }
378
379    @Override
380    public void setSimpleShowTopSelectResultSet(boolean simpleShowTopSelectResultSet) {
381        option.setSimpleShowTopSelectResultSet(simpleShowTopSelectResultSet);
382    }
383
384    @Override
385    public boolean isSimpleShowFunction() {
386        return option.isSimpleShowFunction();
387    }
388
389    @Override
390    public void setSimpleShowFunction(boolean simpleShowFunction) {
391        option.setSimpleShowFunction(simpleShowFunction);
392    }
393
394    @Override
395    public boolean isShowJoin() {
396        return option.isShowJoin();
397    }
398
399    @Override
400    public void setShowJoin(boolean showJoin) {
401        option.setShowJoin(showJoin);
402    }
403
404    @Override
405    public boolean isShowImplicitSchema() {
406        return option.isShowImplicitSchema();
407    }
408
409    @Override
410    public void setShowImplicitSchema(boolean showImplicitSchema) {
411        option.setShowImplicitSchema(showImplicitSchema);
412    }
413
414    @Override
415    public boolean isShowConstantTable() {
416        return option.isShowConstantTable();
417    }
418
419    @Override
420    public void setShowConstantTable(boolean showConstantTable) {
421        option.setShowConstantTable(showConstantTable);
422    }
423
424    @Override
425    public boolean isShowCountTableColumn() {
426        return option.isShowCountTableColumn();
427    }
428
429    @Override
430    public void setShowCountTableColumn(boolean showCountTableColumn) {
431        option.setShowCountTableColumn(showCountTableColumn);
432    }
433
434    @Override
435    public boolean isTransform() {
436        return option.isTransform();
437    }
438
439    @Override
440    public void setTransform(boolean transform) {
441        option.setTransform(transform);
442        if (option.isTransformCoordinate()) {
443            option.setTransform(true);
444        }
445    }
446
447    @Override
448    public boolean isTransformCoordinate() {
449        return option.isTransformCoordinate();
450    }
451
452    @Override
453    public void setTransformCoordinate(boolean transformCoordinate) {
454        option.setTransformCoordinate(transformCoordinate);
455        if (transformCoordinate) {
456            option.setTransform(true);
457        }
458    }
459
460    @Override
461    public boolean isLinkOrphanColumnToFirstTable() {
462        return option.isLinkOrphanColumnToFirstTable();
463    }
464
465    @Override
466    public void setLinkOrphanColumnToFirstTable(boolean linkOrphanColumnToFirstTable) {
467        option.setLinkOrphanColumnToFirstTable(linkOrphanColumnToFirstTable);
468    }
469
470    @Override
471    public boolean isIgnoreCoordinate() {
472        return option.isIgnoreCoordinate();
473    }
474
475    @Override
476    public void setIgnoreCoordinate(boolean ignoreCoordinate) {
477        option.setIgnoreCoordinate(ignoreCoordinate);
478    }
479
480    @Override
481    public void setHandleListener(DataFlowHandleListener listener) {
482        option.setHandleListener(listener);
483    }
484
485    @Override
486    public void setSqlEnv(TSQLEnv sqlenv) {
487        this.sqlenv = sqlenv;
488    }
489
490    @Override
491    public void setOption(Option option) {
492        this.option = option;
493    }
494
495    @Override
496    public Option getOption() {
497        return option;
498    }
499
500    @Override
501    public List<ErrorInfo> getErrorMessages() {
502        return errorInfos;
503    }
504
505    @Override
506    public synchronized String generateSqlInfos() {
507        return JSON.toJSONString(sqlInfoMap);
508    }
509
510    @Override
511    public synchronized String generateDataFlow() {
512        return generateDataFlow(false);
513    }
514
515    @Override
516    public Map<String, List<SqlInfo>> getSqlInfos() {
517        return sqlInfoMap;
518    }
519
520    @Override
521    /**
522     * @deprecated please use SqlInfoHelper.getSelectedDbObjectInfo
523     */
524    public DbObjectPosition getSelectedDbObjectInfo(Coordinate start, Coordinate end) {
525        if (start == null || end == null) {
526            throw new IllegalArgumentException("Coordinate can't be null.");
527        }
528
529        String hashCode = start.getHashCode();
530
531        if (hashCode == null) {
532            throw new IllegalArgumentException("Coordinate hashcode can't be null.");
533        }
534
535        int dbObjectStartLine = (int) start.getX() - 1;
536        int dbObjectStarColumn = (int) start.getY() - 1;
537        int dbObjectEndLine = (int) end.getX() - 1;
538        int dbObjectEndColumn = (int) end.getY() - 1;
539        List<SqlInfo> sqlInfoList;
540        if (hashCode.matches("\\d+")) {
541            sqlInfoList = sqlInfoMap.getValueAtIndex(Integer.valueOf(hashCode));
542        } else {
543            sqlInfoList = sqlInfoMap.get(hashCode);
544        }
545        for (int j = 0; j < sqlInfoList.size(); j++) {
546            SqlInfo sqlInfo = sqlInfoList.get(j);
547            int startLine = sqlInfo.getLineStart();
548            int endLine = sqlInfo.getLineEnd();
549            if (dbObjectStartLine >= startLine && dbObjectStartLine <= endLine) {
550                DbObjectPosition position = new DbObjectPosition();
551                position.setFile(sqlInfo.getFileName());
552                position.setFilePath(sqlInfo.getFilePath());
553                position.setSql(sqlInfo.getSql());
554                position.setIndex(sqlInfo.getOriginIndex());
555                List<Pair<Integer, Integer>> positions = position.getPositions();
556                positions.add(new Pair<Integer, Integer>(
557                        dbObjectStartLine - startLine + sqlInfo.getOriginLineStart() + 1, dbObjectStarColumn + 1));
558                positions.add(new Pair<Integer, Integer>(dbObjectEndLine - startLine + sqlInfo.getOriginLineStart() + 1,
559                        dbObjectEndColumn + 1));
560                return position;
561            }
562        }
563        return null;
564    }
565
566    @Override
567    public synchronized String generateDataFlow(final boolean withExtraInfo) {
568        return generateDataFlow(withExtraInfo, true);
569    }
570    
571    public synchronized String generateDataFlow(final boolean withExtraInfo, boolean useSaveMemoryMode) {
572        sqlInfoMap.clear();
573        errorInfos.clear();
574        Map<String, Pair3<StringBuilder, AtomicInteger, String>> databaseMap = new LinkedHashMap<String, Pair3<StringBuilder, AtomicInteger, String>>();
575        for (int i = 0; i < sqlInfos.length; i++) {
576            SqlInfo sqlInfo = sqlInfos[i];
577                        if (sqlInfo != null && sqlInfo.getSql() == null && sqlInfo.getFilePath() != null) {
578                                sqlInfo.setSql(SQLUtil.getFileContent(sqlInfo.getFilePath()));
579                        }
580            if (sqlInfo != null && sqlInfo.getSql() == null && sqlInfo.getFileName() != null) {
581                sqlInfo.setSql(SQLUtil.getFileContent(sqlInfo.getFileName()));
582            }
583            if (sqlInfo == null || sqlInfo.getSql() == null) {
584                sqlInfoMap.put(String.valueOf(i), new ArrayList<SqlInfo>());
585                continue;
586            }
587            String sql = sqlInfo.getSql();
588            if (sql != null && sql.trim().startsWith("{")) {
589                if (MetadataReader.isGrabit(sql) || MetadataReader.isSqlflow(sql) || MetadataReader.isSqlflowSharded(sql)) {
590                    String hash = SHA256.getMd5(sql);
591                    String fileHash = SHA256.getMd5(hash);
592                    if (!sqlInfoMap.containsKey(fileHash)) {
593                        sqlInfoMap.put(fileHash, new ArrayList<SqlInfo>());
594                        sqlInfoMap.get(fileHash).add(sqlInfo);
595                    }
596                    if (this.metadataFlow == null) {
597                        this.metadataFlow = analyzeMetadata(sql, sqlInfo.getFilePath());
598                    }
599                } else {
600                    Map queryObject = (Map) JSON.parseObject(sql);
601                    appendSqlInfo(databaseMap, i, sqlInfo, queryObject);
602                }
603            } else {
604                String content = sql;
605                String hash = SHA256.getMd5(content);
606                String fileHash = SHA256.getMd5(hash);
607                if (!sqlInfoMap.containsKey(fileHash)) {
608                    sqlInfoMap.put(fileHash, new ArrayList<SqlInfo>());
609
610                    String database = TSQLEnv.DEFAULT_DB_NAME;
611                    String schema = TSQLEnv.DEFAULT_SCHEMA_NAME;
612                    if(sqlenv!=null) {
613                        // Prefer the per-analysis Option default database over the attached
614                        // env's default catalog (mirrors DataFlowAnalyzer), so a shared
615                        // multi-catalog TSQLEnv can be reused across analyses with different
616                        // default databases.
617                        if (!SQLUtil.isEmpty(option.getDefaultDatabase())
618                                && !TSQLEnv.DEFAULT_DB_NAME.equals(option.getDefaultDatabase())) {
619                            database = option.getDefaultDatabase();
620                        } else {
621                            database = sqlenv.getDefaultCatalogName();
622                        }
623                        if(database == null) {
624                            database = TSQLEnv.DEFAULT_DB_NAME;
625                        }
626                        schema = sqlenv.getDefaultSchemaName();
627                        if(schema == null) {
628                            schema = TSQLEnv.DEFAULT_SCHEMA_NAME;
629                        }
630                    }
631                    boolean supportCatalog = TSQLEnv.supportCatalog(option.getVendor());
632                    boolean supportSchema = TSQLEnv.supportSchema(option.getVendor());
633                    StringBuilder builder = new StringBuilder();
634                    if (supportCatalog) {
635                        builder.append(database);
636                    }
637                    if (supportSchema) {
638                        if (builder.length() > 0) {
639                            builder.append(".");
640                        }
641                        builder.append(schema);
642                    }
643                    String group = builder.toString();
644                    SqlInfo sqlInfoItem = new SqlInfo();
645                    sqlInfoItem.setFileName(sqlInfo.getFileName());
646                    sqlInfoItem.setFilePath(sqlInfo.getFilePath());
647                    sqlInfoItem.setSql(sqlInfo.getSql());
648                    sqlInfoItem.setOriginIndex(0);
649                    sqlInfoItem.setOriginLineStart(0);
650                    sqlInfoItem.setOriginLineEnd(sqlInfo.getSql().split("\n").length - 1);
651                    sqlInfoItem.setIndex(0);
652                    sqlInfoItem.setLineStart(0);
653                    sqlInfoItem.setLineEnd(sqlInfo.getSql().split("\n").length - 1);
654                    sqlInfoItem.setHash(fileHash);
655                    sqlInfoItem.setGroup(group);
656
657                    sqlInfoMap.get(fileHash).add(sqlInfoItem);
658                }
659            }
660        }
661
662        final TSQLEnv[] env = new TSQLEnv[]{sqlenv};
663        if (sqlenv == null) {
664                if (option.getHandleListener() != null) {
665                        option.getHandleListener().startParseSQLEnv();
666                }
667                TSQLEnv[] envs = new SQLEnvParser(option.getDefaultServer(), option.getDefaultDatabase(), option.getDefaultSchema()).parseSQLEnv(option.getVendor(), sqlInfos);
668                        if (envs != null && envs.length > 0) {
669                                env[0] = envs[0];
670                        }
671                        if (option.getHandleListener() != null) {
672                                option.getHandleListener().endParseSQLEnv();
673                        }
674        }
675        // Guard against a zero-size pool: newFixedThreadPool(0) throws. sqlInfos can be
676        // empty when the only input was a manifest that produced no analyzable source
677        // (e.g. an unsupported sharded version that was refused).
678        int poolSize = calculateMemoryAwarePoolSize(option.getParallel(), sqlInfos.length, option.getEstimatedMemoryPerTaskMB());
679        ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(poolSize);
680        
681        final File[] tempFilesArray = new File[sqlInfos.length];
682        final Map<SqlInfo, dataflow> dataflowMap = useSaveMemoryMode ? null : new ConcurrentHashMap<>();
683        
684        try {
685            final CountDownLatch latch = new CountDownLatch(sqlInfos.length);
686
687            logger.info("start parallel analyze " + sqlInfos.length + " sqlinfos");
688
689            for (int i = 0; i < sqlInfos.length; i++) {
690                final SqlInfo[] sqlInfoCopy = new SqlInfo[sqlInfos.length];
691                final SqlInfo item = sqlInfos[i];
692                sqlInfoCopy[i] = item;
693                final Option optionCopy = (Option) option.clone();
694                optionCopy.setStartId(5000000L * i);
695                optionCopy.setOutput(false);
696                optionCopy.setSimpleRetainIntermediate(true);
697                optionCopy.setAutoDetectLargeFile(false);
698                // Worker-local relationship IDs are remapped during merge. Until
699                // the evidence merge performs the same remap, publishing worker
700                // evidence would create invalid correlations.
701                optionCopy.setCollectAuthoritativeLineageEvidence(false);
702                final int index = i;
703                Runnable task = new Runnable() {
704                    @Override
705                    public void run() {
706                        try {
707                            logger.info("start analyze sqlinfo[" + index + "]" + (sqlInfos[index].getFilePath() != null ? ", file name = " + new File(sqlInfos[index].getFilePath()).getName() : ""));
708                            DataFlowAnalyzer analyzer = new DataFlowAnalyzer(sqlInfoCopy, optionCopy);
709                            if (env[0] != null) {
710                                analyzer.setSqlEnv(env[0].copy());
711                            }
712                            analyzer.generateDataFlow(true);
713                            dataflow dataflow = analyzer.getDataFlow();
714                            if (dataflow == null) {
715                                logger.warn("analyze sqlinfo[" + index + "] done, but dataflow is null" + (sqlInfos[index].getFilePath() != null ? ", file name = " + new File(sqlInfos[index].getFilePath()).getName() : ""));
716                                errorInfos.addAll(analyzer.getErrorMessages());
717                                analyzer.dispose();
718                                return;
719                            }
720                            logger.info("analyze sqlinfo[" + index + "] done, relation count: " + dataflow.getRelationships().size()+", error count: "+ analyzer.getErrorMessages().size());
721                            //autogenerate analyzer 错误集合与 dataflow 错误集合不同源,各自按自身 size 安全截断,避免 subList 越界
722                            List<ErrorInfo> taskErrors = analyzer.getErrorMessages();
723                            if (taskErrors.size() > 10000) {
724                                errorInfos.addAll(taskErrors.subList(0, 10000));
725                                logger.warn("Too many errors ("+taskErrors.size()+"), truncating to first 10000 errors" + (sqlInfos[index].getFileName() != null ? ", file name = " + sqlInfos[index].getFileName() : ""));
726                            } else {
727                                errorInfos.addAll(taskErrors);
728                            }
729                            if (dataflow.getErrors() != null && dataflow.getErrors().size() > 10000) {
730                                dataflow.setErrors(new ArrayList<>(dataflow.getErrors().subList(0, 10000)));
731                            }
732                            
733                            if (useSaveMemoryMode) {
734                                File tempFile = File.createTempFile("dataflow_" + index + "_" + System.currentTimeMillis() + "_", ".xml.zip");
735                                //autogenerate saveXML 失败时删除已创建的临时文件,避免泄漏
736                                try {
737                                    XML2Model.saveXML(dataflow, tempFile);
738                                    tempFilesArray[index] = tempFile;
739                                } catch (Exception ex) {
740                                    tempFile.delete();
741                                    throw ex;
742                                }
743                            } else {
744                                dataflowMap.put(item, dataflow);
745                            }
746                            
747                            hashSQLMap.putAll(analyzer.getHashSQLMap());
748                            analyzer.dispose();
749                        }
750                        catch (Exception e) {
751                            logger.error("analyze sqlinfo[" + index + "] failed.", e);
752                        }
753                        finally {
754                            latch.countDown();
755                        }
756                    }
757                };
758                executor.submit(task);
759            }
760            latch.await();
761        } catch (Exception e) {
762            logger.error("execute task failed.", e);
763        }
764        executor.shutdown();
765
766        ModelBindingManager modelManager = new ModelBindingManager();
767        modelManager.setGlobalVendor(option.getVendor());
768        modelManager.setGlobalOption(option);
769        ModelBindingManager.set(modelManager);
770        
771        if (useSaveMemoryMode) {
772            List<File> tempFiles = new ArrayList<>(sqlInfos.length + 1);
773            for (File f : tempFilesArray) {
774                if (f != null) {
775                    tempFiles.add(f);
776                }
777            }
778            if (metadataFlow != null) {
779                File metadataFile = null;
780                //autogenerate metadata 临时 ZIP 在 saveXML 失败时必须删除,避免泄漏
781                try {
782                    metadataFile = File.createTempFile("gsp-metadata-", ".xml.zip");
783                    XML2Model.saveXML(metadataFlow, metadataFile);
784                    tempFiles.add(metadataFile);
785                } catch (Exception e) {
786                    if (metadataFile != null) {
787                        metadataFile.delete();
788                    }
789                    logger.error("Failed to save metadata to temp file", e);
790                }
791            }
792            logger.info("start merge dataflow, dataflow count: " + tempFiles.size()+", useSaveMemoryMode: "+ useSaveMemoryMode+", gsp version: "+ TBaseType.versionid);
793            //autogenerate 合并与清理放入 try-finally,异常路径也必须删除临时 ZIP,避免泄漏
794            try {
795                this.dataflow = iterativeMergeDataFlows(tempFiles, 5000000L * sqlInfos.length);
796                if (this.dataflow != null && this.dataflow.getRelationships() != null) {
797                    logger.info("merge dataflow done, dataflow count: " + tempFiles.size() + ", relation count: " + this.dataflow.getRelationships().size());
798                }
799            } finally {
800                for (File tempFile : tempFiles) {
801                    tempFile.delete();
802                }
803            }
804        } else {
805            if (metadataFlow != null) {
806                dataflowMap.put(new SqlInfo(), metadataFlow);
807            }
808            logger.info("start merge dataflow, dataflow count: " + dataflowMap.size()+", useSaveMemoryMode: "+ useSaveMemoryMode+", gsp version: "+ TBaseType.versionid);
809            this.dataflow = mergeDataFlows(dataflowMap, 5000000L * sqlInfos.length);
810            if (this.dataflow != null && this.dataflow.getRelationships() != null) {
811                logger.info("merge dataflow done, dataflow count: " + dataflowMap.size() + ", relation count: " + this.dataflow.getRelationships().size());
812            }
813            dataflowMap.clear();
814        }
815        
816                if (this.dataflow != null) {
817                        logger.info("merge done, relation count: " + this.dataflow.getRelationships().size());
818                }
819
820        if(option.isSimpleOutput()){
821            List<String> showTypes = new ArrayList<>();
822            if(option.getSimpleShowRelationTypes()!=null) {
823                showTypes.addAll(option.getSimpleShowRelationTypes());
824            }
825            if(showTypes.isEmpty()) {
826                showTypes.add("fdd");
827            }
828            if(option.isShowCallRelation()) {
829                showTypes.add("call");
830            }
831            if(option.isShowERDiagram()) {
832                showTypes.add("er");
833            }
834            try {
835                this.dataflow = new DataFlowAnalyzer("", option).getSimpleDataflow(this.dataflow, option.isSimpleOutput(), showTypes);
836            } catch (Exception e) {
837                throw new RuntimeException(e);
838            }
839        }
840
841        if (dataflow != null && option.isOutput()) {
842            if (option.isTextFormat()) {
843                dataflowString = DataFlowAnalyzer.getTextOutput(dataflow);
844            } else {
845                try {
846                    dataflowString = XML2Model.saveXML(dataflow);
847                }catch (Exception e){
848                    logger.error("save dataflow as xml failed.", e);
849                    dataflowString = null;
850                }
851            }
852        }
853        ModelBindingManager.remove();
854        return dataflowString;
855    }
856    
857    private dataflow iterativeMergeDataFlows(List<File> tempFiles, long startId) {
858        return DataflowUtility.iterativeMergeDataflowsFromFilesByStartId(tempFiles, startId);
859    }
860
861    private void appendSqlInfo(Map<String, Pair3<StringBuilder, AtomicInteger, String>> databaseMap, int index,
862                               SqlInfo sqlInfo, Map queryObject) {
863        EDbVendor vendor = option.getVendor();
864        if(!SQLUtil.isEmpty(sqlInfo.getDbVendor())){
865            vendor = EDbVendor.valueOf(sqlInfo.getDbVendor());
866        }
867
868        boolean supportCatalog = TSQLEnv.supportCatalog(vendor);
869        boolean supportSchema = TSQLEnv.supportSchema(vendor);
870
871        String groupName = (String) queryObject.get("groupName");
872        if (DlineageUtil.isProcedureExcluded(groupName)) {
873            return;
874        }
875
876        if (DataFlowAnalyzer.isSourceUnavailable(queryObject)) {
877            return; // DDL body not retrieved; its text is not the definition
878        }
879
880        String content = (String) queryObject.get("sourceCode");
881        if (SQLUtil.isEmpty(content)) {
882            return;
883        }
884
885        StringBuilder builder = new StringBuilder();
886        if (supportCatalog) {
887            String database = (String) queryObject.get("database");
888            if (database.indexOf(".") != -1) {
889                database = SQLUtil.quoteDottedName(vendor, ESQLDataObjectType.dotCatalog, database);
890            }
891            builder.append(database);
892        }
893        if (supportSchema) {
894            String schema = (String) queryObject.get("schema");
895            if (schema.indexOf(".") != -1) {
896                schema = SQLUtil.quoteDottedName(vendor, ESQLDataObjectType.dotSchema, schema);
897            }
898            if (builder.length() > 0) {
899                builder.append(".");
900            }
901            builder.append(schema);
902        }
903        String group = builder.toString();
904        String sqlHash = SHA256.getMd5(content);
905        String hash = SHA256.getMd5(sqlHash);
906        if (!databaseMap.containsKey(sqlHash)) {
907            databaseMap.put(sqlHash,
908                    new Pair3<StringBuilder, AtomicInteger, String>(new StringBuilder(), new AtomicInteger(), group));
909        }
910        String delimiterChar = String.valueOf(TGSqlParser.getDelimiterChar(option.getVendor()));
911        StringBuilder buffer = new StringBuilder(content);
912        if (content.trim().endsWith(delimiterChar) || content.trim().endsWith(";")) {
913            buffer.append("\n");
914        } else if(vendor == EDbVendor.dbvredshift
915                || vendor == EDbVendor.dbvgaussdb
916                || vendor == EDbVendor.dbvedb
917                || vendor == EDbVendor.dbvpostgresql
918                || vendor == EDbVendor.dbvmysql
919                || vendor == EDbVendor.dbvteradata){
920            buffer.append("\n\n-- " + TBaseType.sqlflow_stmt_delimiter_str + "\n\n");
921        } else{
922            SQLUtil.endTrim(buffer);
923            buffer.append(";").append("\n");
924        }
925
926        int lineStart = databaseMap.get(sqlHash).first.toString().split("\n", -1).length - 1;
927        if (databaseMap.get(sqlHash).first.toString().length() == 0) {
928            lineStart = 0;
929        }
930        databaseMap.get(sqlHash).first.append(buffer.toString());
931        SqlInfo sqlInfoItem = new SqlInfo();
932        sqlInfoItem.setFileName(sqlInfo.getFileName());
933        sqlInfoItem.setFilePath(sqlInfo.getFilePath());
934        sqlInfoItem.setSql(buffer.toString());
935        sqlInfoItem.setOriginIndex(index);
936        sqlInfoItem.setOriginLineStart(0);
937        sqlInfoItem.setOriginLineEnd(buffer.toString().split("\n", -1).length - 1);
938        sqlInfoItem.setIndex(databaseMap.get(sqlHash).second.getAndIncrement());
939        sqlInfoItem.setLineStart(lineStart);
940        sqlInfoItem.setLineEnd(databaseMap.get(sqlHash).first.toString().split("\n", -1).length - 1);
941        sqlInfoItem.setGroup(group);
942        sqlInfoItem.setHash(hash);
943
944        if (!sqlInfoMap.containsKey(hash)) {
945            sqlInfoMap.put(hash, new ArrayList<SqlInfo>());
946        }
947        sqlInfoMap.get(hash).add(sqlInfoItem);
948    }
949
950    @Override
951    public void dispose() {
952        ModelBindingManager.remove();
953    }
954
955    private dataflow mergeDataFlows(Map<SqlInfo, dataflow> dataflowMap, long startId) {
956        return DataflowUtility.mergeDataflowsByStartId(dataflowMap.values(), startId);
957    }
958
959    private dataflow analyzeMetadata(String manifestJson, String manifestPath) {
960        try {
961            if (MetadataReader.isSqlflowSharded(manifestJson)) {
962                String baseDir = manifestPath != null ? new File(manifestPath).getParent() : null;
963                SqlflowShardedMetadataAnalyzer analyzer = new SqlflowShardedMetadataAnalyzer(
964                        sqlenv, baseDir);
965                return analyzer.analyzeMetadata(option.getVendor(), manifestJson);
966            } else if (MetadataReader.isSqlflow(manifestJson)) {
967                SqlflowMetadataAnalyzer analyzer = new SqlflowMetadataAnalyzer(sqlenv);
968                return analyzer.analyzeMetadata(option.getVendor(), manifestJson);
969            } else if (MetadataReader.isGrabit(manifestJson)) {
970                GrabitMetadataAnalyzer analyzer = new GrabitMetadataAnalyzer();
971                return analyzer.analyzeMetadata(option.getVendor(), manifestJson);
972            }
973        } catch (Exception e) {
974            logger.error("Failed to analyze metadata", e);
975        }
976        return null;
977    }
978
979    @Override
980    public synchronized dataflow getDataFlow() {
981        if (dataflow != null) {
982            return dataflow;
983        } else if (dataflowString != null) {
984            return XML2Model.loadXML(dataflow.class, dataflowString);
985        }
986        return null;
987    }
988
989    @Override
990    public Map<String, String> getHashSQLMap() {
991        return hashSQLMap;
992    }
993
994    static int calculateMemoryAwarePoolSize(int requestedParallel, int taskCount, long estimatedMemoryPerTaskMB) {
995        int basePoolSize = Math.max(1, requestedParallel < taskCount ? requestedParallel : taskCount);
996
997        long maxMemory = Runtime.getRuntime().maxMemory();
998        if (maxMemory == Long.MAX_VALUE) {
999            return basePoolSize;
1000        }
1001
1002        long reservedMemory = (long) (maxMemory * 0.2);
1003        long usableMemory = maxMemory - reservedMemory;
1004
1005        long estimatedBytesPerTask = estimatedMemoryPerTaskMB * 1024L * 1024L;
1006        int memoryBasedLimit = (int) Math.max(1, usableMemory / estimatedBytesPerTask);
1007
1008        int poolSize = Math.min(basePoolSize, memoryBasedLimit);
1009
1010        if (poolSize < basePoolSize) {
1011            logger.warn("Thread pool size reduced from " + basePoolSize + " to " + poolSize
1012                    + " due to memory constraints. "
1013                    + "(maxMemory=" + (maxMemory / (1024 * 1024)) + "MB, "
1014                    + "usableMemory=" + (usableMemory / (1024 * 1024)) + "MB, "
1015                    + "estimatedMemoryPerTask=" + estimatedMemoryPerTaskMB + "MB)");
1016        }
1017
1018        return poolSize;
1019    }
1020
1021    public static void main(String[] args) throws Exception {
1022        Option option = new Option();
1023        option.setVendor(EDbVendor.dbvmssql);
1024        option.setOutput(false);
1025//        option.setSimpleOutput(true);
1026        File parentDir = new File("C:\\Users\\KK\\Desktop\\sql");
1027        ParallelDataFlowAnalyzer analyzer = new ParallelDataFlowAnalyzer(new File[]{new File("C:\\Users\\KK\\Desktop\\metadata_with_query\\客户原始sql.json")}, option, 5, new File("C:\\Users\\KK\\Desktop\\metadata_with_query"));
1028        analyzer.generateDataFlow(false, true);
1029        dataflow dataflow = analyzer.getDataFlow();
1030//        System.out.println(XML2Model.saveXML(dataflow));
1031//        dataflow dataflow = XML2Model.loadXML(gudusoft.gsqlparser.dlineage.dataflow.model.xml.dataflow.class,new File("D:\\dataflow.xml.zip"));
1032        XML2Model.saveXML(dataflow, new File("D:\\dataflow.xml.zip"));
1033//        dataflow = DataflowUtility.convertToTableLevelDataflow(dataflow);
1034//        dataflow = DataflowUtility.convertTableLevelToFunctionCallDataflow(dataflow, true, EDbVendor.dbvoracle);
1035//        System.out.println(XML2Model.saveXML(dataflow));
1036    }
1037
1038}