001package gudusoft.gsqlparser.dlineage.comment; 002 003import gudusoft.gsqlparser.*; 004import gudusoft.gsqlparser.nodes.*; 005import gudusoft.gsqlparser.dlineage.dataflow.model.*; 006import gudusoft.gsqlparser.dlineage.util.Pair; 007import gudusoft.gsqlparser.dlineage.comment.CommentBindingBundle.*; 008import gudusoft.gsqlparser.dlineage.comment.CommentHostCollector.Draft; 009import java.nio.charset.StandardCharsets; 010import java.security.MessageDigest; 011import java.util.*; 012 013/** Per-analysis, opt-in draft store. build() releases all AST and token references. */ 014public final class CommentBindingCollector { 015 private static final java.util.regex.Pattern ENGINE_DIRECTIVE = java.util.regex.Pattern.compile("(?m)(?:^|,)\\s*(?:--\\s*)?(?:db-instance|db|schema|process_label|process)\\s*[:=]"); 016 private static final List<String> KINDS = Arrays.asList("container", "routine", "call", "statement", "column", "table_ref", "object_declaration"); 017 private static final class Unit { 018 SourceUnit source; String sql; int base; CommentTokenIndex index; boolean derived; 019 final List<Draft> hosts = new ArrayList<Draft>(); 020 final List<TSourceToken> comments = new ArrayList<TSourceToken>(); 021 } 022 private static final class Parsed { 023 CommentTokenIndex index; CommentHostCollector hosts; List<Unit> units; boolean partial; 024 TSourceTokenList tokens; 025 } 026 private final Option option; 027 private final Map<String,List<Unit>> inputs = new LinkedHashMap<String,List<Unit>>(); 028 private final List<Parsed> parsed = new ArrayList<Parsed>(); 029 private final List<Unit> units = new ArrayList<Unit>(); 030 private boolean failed; 031 private boolean missingSource; 032 private int derivedOrdinal; 033 private final Set<TSourceTokenList> captured = Collections.newSetFromMap(new IdentityHashMap<TSourceTokenList, Boolean>()); 034 public CommentBindingCollector(Option option) { this.option = option; } 035 036 public void register(String key, String sql, int base, int inputOrdinal, String fileName, boolean derived) { 037 try { registerUnit(key, sql, base, inputOrdinal, fileName, derived); } 038 catch (Exception e) { failed = true; } 039 } 040 private Unit registerUnit(String key, String sql, int base, int ordinal, String fileName, boolean derived) throws Exception { 041 Unit u = new Unit(); u.sql = sql; u.base = base; u.derived = derived; 042 String id = "s" + (units.size() + 1); 043 byte[] hash = MessageDigest.getInstance("SHA-256").digest(sql.getBytes(StandardCharsets.UTF_8)); 044 StringBuilder digest = new StringBuilder(); for (byte b : hash) digest.append(String.format(Locale.ROOT, "%02x", b & 255)); 045 if (fileName != null) fileName = fileName.replace('\\', '/'); 046 if (fileName != null) fileName = fileName.substring(fileName.lastIndexOf('/') + 1); 047 u.source = new SourceUnit(id, ordinal, option.getVendor().toString(), digest.toString(), derived ? "derived" : "original", fileName); 048 u.index = new CommentTokenIndex(sql, new TSourceTokenList()); 049 units.add(u); 050 inputs.computeIfAbsent(key, k -> new ArrayList<Unit>()).add(u); 051 return u; 052 } 053 /** Imported lineage without a bundle does not provide original SQL comment facts. */ 054 public void sourceUnavailable() { missingSource = true; } 055 056 public void captureDerived(TGSqlParser parser, boolean parsePartial) { 057 capture("derived:" + (++derivedOrdinal), parser, parsePartial); 058 } 059 public void capture(String key, TGSqlParser parser, boolean parsePartial) { 060 try { 061 Parsed primary = captureOne(key, parser, parsePartial); 062 Deque<TGSqlParser> foreign = new ArrayDeque<TGSqlParser>(primary.hosts.foreignParsers); 063 while (!foreign.isEmpty()) { 064 TGSqlParser nested = foreign.removeFirst(); 065 if (captured.contains(nested.sourcetokenlist)) continue; 066 // Routine-body parsers already ran. Capture iteratively without parsing again. 067 Parsed child = captureOne("derived:" + (++derivedOrdinal), nested, nested.getErrorCount() != 0); 068 foreign.addAll(child.hosts.foreignParsers); 069 } 070 } catch (Exception e) { failed = true; } 071 } 072 private Parsed captureOne(String key, TGSqlParser parser, boolean parsePartial) throws Exception { 073 captured.add(parser.sourcetokenlist); 074 Parsed p = new Parsed(); 075 p.units = inputs.get(key); 076 if (p.units == null) { 077 Unit u = registerUnit(key, parser.sqltext, 0, units.size(), null, true); 078 p.units = Collections.singletonList(u); 079 } 080 p.tokens = parser.sourcetokenlist; 081 p.index = new CommentTokenIndex(parser.sqltext, parser.sourcetokenlist); 082 p.hosts = new CommentHostCollector(p.index, parser); 083 p.partial = parsePartial; 084 parsed.add(p); 085 return p; 086 } 087 public void clear() { inputs.clear(); parsed.clear(); units.clear(); captured.clear(); } 088 089 public CommentBindingBundle build(ModelBindingManager manager) { 090 CommentBindingBundle out = new CommentBindingBundle(); 091 out.supportedHostKinds.addAll(KINDS); 092 out.limits = new Limits(option.getCommentBindingMaxRawLength(), option.getCommentBindingMaxCommentsPerSourceUnit(), option.getCommentBindingMaxTotalRawLength()); 093 try { 094 final IdentityHashMap<Object,List<Target>> models = new IdentityHashMap<Object,List<Target>>(); 095 manager.visitExactBindings((key, model) -> observe(models, key, model)); 096 IdentityHashMap<TSourceToken,Parsed> tokenOwners = new IdentityHashMap<TSourceToken,Parsed>(); 097 for (Parsed p : parsed) for (TSourceToken token : p.index.code) tokenOwners.put(token,p); 098 for (Relationship relation : manager.getRelations()) if (relation instanceof CallRelationship) { 099 CallRelationship call = (CallRelationship)relation; 100 addTarget(models, call.getCallObject(), new Target("relationship", String.valueOf(call.getId()), null)); 101 Parsed p = call.getCallObject() == null || call.getCallObject().getStartToken() == null ? null 102 : tokenOwners.get(call.getCallObject().getStartToken()); 103 if (p != null) { 104 Draft h = p.hosts.add(call.getCallObject(), null, "call", "call_occurrence", null, 0); 105 if (h != null) h.kind = "call"; 106 } 107 } 108 int total = 0; 109 Set<Unit> processed = Collections.newSetFromMap(new IdentityHashMap<Unit, Boolean>()); 110 for (Unit u : units) out.sourceUnits.add(u.source); 111 for (Parsed p : parsed) { 112 p.hosts.hosts.sort(Comparator.comparingInt((Draft h) -> h.start).thenComparingInt(h -> -h.end).thenComparing(h -> h.kind)); 113 for (int i = 0; i < p.hosts.hosts.size(); i++) p.hosts.hosts.get(i).id = "h" + (out.hosts.size() + i + 1); 114 NavigableMap<Integer,Unit> boundaries = new TreeMap<Integer,Unit>(); 115 for (Unit u : p.units) boundaries.put(u.base,u); 116 for (Draft h : p.hosts.hosts) { 117 Map.Entry<Integer,Unit> entry = boundaries.floorEntry(h.start); 118 if (entry != null && h.end <= entry.getValue().base + entry.getValue().sql.length()) entry.getValue().hosts.add(h); 119 } 120 for (TSourceToken token : p.index.comments) { 121 Map.Entry<Integer,Unit> entry = boundaries.floorEntry(CommentTokenIndex.start(token)); 122 if (entry != null && CommentTokenIndex.end(token) <= entry.getValue().base + entry.getValue().sql.length()) entry.getValue().comments.add(token); 123 } 124 CommentBinder binder = new CommentBinder(p.index, p.hosts.hosts, p.hosts.expressionBoundaries); 125 for (Unit u : p.units) { 126 processed.add(u); 127 String sourceId = u.source.id; 128 Draft container = new Draft(); container.id = sourceId; 129 container.kind = "container"; container.start = u.base; container.end = u.base + u.sql.length(); 130 out.hosts.add(new Host(container.id, sourceId, "container", "script", range(u, u.base, container.end), null, null, null, 0, 131 "source_only", Collections.<Target>emptyList(), null, null)); 132 for (Draft h : u.hosts) { 133 List<Target> targets = new ArrayList<Target>(); 134 copyTargets(targets, models.get(h.node)); 135 if (!"select_item".equals(h.syntax)) copyTargets(targets, models.get(h.anchor)); 136 if (h.node instanceof TResultColumn && ((TResultColumn)h.node).getExpr() != null 137 && h.syntax.contains("assignment")) { 138 TExpression lhs = ((TResultColumn)h.node).getExpr().getLeftOperand(); 139 copyTargets(targets, models.get(lhs)); 140 if (lhs != null) copyTargets(targets, models.get(lhs.getObjectOperand())); 141 } 142 if ("call".equals(h.kind)) targets.removeIf(t -> !"relationship".equals(t.space)); 143 String processId = null, procedureId = null; 144 Draft parent = h.parent; 145 if (parent != null && (parent.start < u.base || parent.end > container.end)) parent = null; 146 Draft query = parent; 147 while (query != null && !"sstselect".equals(query.syntax)) query = query.parent; 148 for (Target t : targets) { 149 if ("process".equals(t.space)) processId = t.id; 150 if ("procedure".equals(t.space)) procedureId = t.id; 151 } 152 out.hosts.add(new Host(h.id, sourceId, h.kind, h.syntax, range(u,h.start,h.end), range(u,h.nameStart,h.nameEnd), 153 query == null ? null : query.id, parent == null ? container.id : parent.id, h.ordinal, 154 targets.isEmpty() ? "source_only" : "resolved", targets, processId, procedureId)); 155 } 156 boolean limited = false; int count = 0; 157 for (TSourceToken token : u.comments) { 158 int a = CommentTokenIndex.start(token), b = CommentTokenIndex.end(token); 159 if (a < u.base || b > container.end) continue; 160 if (count >= out.limits.maxCommentsPerSourceUnit) { limited = true; break; } 161 count++; 162 String id = sourceId + ":c" + count; 163 if (!p.index.exact(token)) { diagnostic(out,"SOURCE_MAP_UNAVAILABLE",sourceId,id,null); continue; } 164 String raw = token.getAstext(); String lexical = lexicalKind(raw); 165 int length = raw.length(), keep = Math.min(length, Math.min(out.limits.maxRawLength, Math.max(0,out.limits.maxTotalRawLength-total))); 166 if (keep < length && keep > 0 && Character.isHighSurrogate(raw.charAt(keep-1))) keep--; 167 total += keep; String encoding = null; 168 if (invalidSurrogate(raw)) { diagnostic(out,"INVALID_INPUT",sourceId,id,null); raw = ""; keep = 0; } 169 else raw = raw.substring(0,keep); 170 boolean truncated = keep < length; 171 if (truncated) limited = true; 172 if (!xmlSafe(raw)) { raw = Base64.getEncoder().encodeToString(raw.getBytes(StandardCharsets.UTF_8)); encoding = "base64-utf8"; } 173 out.comments.add(new Comment(id, sourceId, range(u,a,b), lexical, raw, encoding, length, truncated)); 174 if (!"line".equals(lexical) && !"block".equals(lexical)) continue; 175 CommentBinder.Choice choice = binder.bind(token,u.base,container.end,container); 176 if (choice == null) diagnostic(out,"NO_HOST",sourceId,id,null); 177 else if (choice.host == null) diagnostic(out,choice.rule,sourceId,id,null); 178 else out.bindings.add(new Binding(id,choice.host.id,choice.placement,choice.rule)); 179 } 180 String state = (limited || option.isIgnoreCoordinate()) ? "limited" : p.partial ? "parse_partial" : u.derived ? "unsupported" : "complete"; 181 for (String kind : KINDS) out.coverage.add(new Coverage(sourceId,kind,state)); 182 if (!"complete".equals(state)) out.state = "partial"; 183 if (limited) diagnostic(out,"LIMIT_REACHED",sourceId,null,null); 184 if (option.isIgnoreCoordinate()) diagnostic(out,"COORDINATES_DISABLED",sourceId,null,null); 185 if (u.derived) diagnostic(out,"SOURCE_MAP_UNAVAILABLE",sourceId,null,null); 186 } 187 } 188 for (Unit u : units) if (!processed.contains(u)) { 189 for (String kind : KINDS) out.coverage.add(new Coverage(u.source.id, kind, "parse_partial")); 190 out.state = "partial"; 191 diagnostic(out, "PARSE_UNAVAILABLE", u.source.id, null, null); 192 } 193 if (missingSource) diagnostic(out,"SOURCE_MAP_UNAVAILABLE",null,null,null); 194 if (failed) { out.state = "failed"; diagnostic(out,"COLLECTION_FAILED",null,null,null); } 195 } catch (Exception e) { out.state = "failed"; diagnostic(out,"COLLECTION_FAILED",null,null,null); } 196 finally { clear(); } 197 return out; 198 } 199 private Range range(Unit u,int a,int b) { return option.isIgnoreCoordinate() ? null : u.index.range(a-u.base,b-u.base); } 200 private static void diagnostic(CommentBindingBundle b,String code,String source,String comment,String host) { 201 b.diagnostics.add(new Diagnostic(code,source,comment,host)); 202 if (code.equals("INVALID_INPUT") || code.equals("SOURCE_MAP_UNAVAILABLE")) b.state = "partial"; 203 } 204 private static String lexicalKind(String raw) { 205 if (raw.startsWith("/*+")) return "hint"; 206 if (raw.startsWith("/*!")) return "executable"; 207 if (raw.startsWith("/*M!")) return "executable"; 208 if (raw.startsWith("--+")) return "hint"; 209 if (raw.startsWith("-- " + TBaseType.sqlflow_stmt_delimiter_str)) return "directive"; 210 if (raw.startsWith("--") && ENGINE_DIRECTIVE.matcher(raw).find()) return "directive"; 211 if (raw.startsWith("--#") || raw.startsWith("/*#")) return "directive"; 212 return raw.startsWith("/*") ? "block" : "line"; 213 } 214 private static boolean invalidSurrogate(String s) { 215 for (int i=0;i<s.length();i++) { 216 char c=s.charAt(i); 217 if (Character.isHighSurrogate(c)) { if (++i>=s.length() || !Character.isLowSurrogate(s.charAt(i))) return true; } 218 else if (Character.isLowSurrogate(c)) return true; 219 } 220 return false; 221 } 222 private static boolean xmlSafe(String s) { 223 for (int i=0;i<s.length();i++) { char c=s.charAt(i); if ((c<32 && c!='\t' && c!='\r' && c!='\n') || c==0xfffe || c==0xffff) return false; } 224 return true; 225 } 226 private static void copyTargets(List<Target> to,List<Target> from) { 227 if (from != null) for (Target t : from) { 228 boolean found = false; 229 for (Target old : to) if (old.space.equals(t.space) && old.id.equals(t.id) && Objects.equals(old.parentId,t.parentId)) { found=true; break; } 230 if (!found) to.add(t); 231 } 232 } 233 private static void addTarget(IdentityHashMap<Object,List<Target>> map,Object key,Target target) { 234 if (key != null) map.computeIfAbsent(key,k->new ArrayList<Target>()).add(target); 235 } 236 private static void observe(IdentityHashMap<Object,List<Target>> map,Object key,Object value) { 237 if (key instanceof Pair) key = ((Pair<?,?>)key).second; 238 if (value instanceof List) { for (Object v:(List<?>)value) observe(map,key,v); return; } 239 Target target = null; 240 if (value instanceof Table) target = new Target("table",String.valueOf(((Table)value).getId()),null); 241 else if (value instanceof ResultSet) target = new Target("resultset",String.valueOf(((ResultSet)value).getId()),null); 242 else if (value instanceof TableColumn) { TableColumn c=(TableColumn)value; target=new Target("column",String.valueOf(c.getId()),String.valueOf(c.getTable().getId())); } 243 else if (value instanceof ResultColumn) { ResultColumn c=(ResultColumn)value; target=new Target("column",String.valueOf(c.getId()),String.valueOf(c.getResultSet().getId())); } 244 else if (value instanceof Procedure) { Procedure p=(Procedure)value; target=new Target("procedure",String.valueOf(p.getId()),null); addTarget(map,p.getProcedureObject(),target); } 245 else if (value instanceof OraclePackage) { OraclePackage p=(OraclePackage)value; target=new Target("package",String.valueOf(p.getId()),null); addTarget(map,p.getProcedureObject(),target); } 246 else if (value instanceof gudusoft.gsqlparser.dlineage.dataflow.model.Process) target=new Target("process",String.valueOf(((gudusoft.gsqlparser.dlineage.dataflow.model.Process)value).getId()),null); 247 if (target != null && key instanceof TParseTreeNode) addTarget(map,key,target); 248 } 249}