001package gudusoft.gsqlparser.sqlenv.parser; 002 003import gudusoft.gsqlparser.EDbVendor; 004import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 005import gudusoft.gsqlparser.sqlenv.TSQLEnv; 006import gudusoft.gsqlparser.sqlenv.parser.grabit.GrabitSQLEnv; 007import gudusoft.gsqlparser.sqlenv.parser.grabit.MultipleGrabitSQLEnv; 008import gudusoft.gsqlparser.sqlenv.IdentifierInputForm; 009import gudusoft.gsqlparser.sqlenv.parser.sharded.SqlflowShardedSQLEnv; 010import gudusoft.gsqlparser.sqlenv.parser.sqldep.MultipleSQLDepSQLEnv; 011import gudusoft.gsqlparser.sqlenv.parser.sqldep.SQLDepSQLEnv; 012import gudusoft.gsqlparser.sqlenv.parser.sqlflow.SqlflowSQLEnv; 013import gudusoft.gsqlparser.dlineage.dataflow.metadata.MetadataReader; 014import gudusoft.gsqlparser.util.SQLUtil; 015import gudusoft.gsqlparser.util.json.JSON; 016 017import java.io.File; 018import java.io.IOException; 019import java.nio.charset.StandardCharsets; 020import java.nio.file.Files; 021import java.nio.file.Path; 022import java.nio.file.Paths; 023import java.util.ArrayList; 024import java.util.List; 025import java.util.Map; 026import java.util.logging.Level; 027import java.util.logging.Logger; 028 029@SuppressWarnings("rawtypes") 030public class TJSONSQLEnvParser implements TSQLEnvParser { 031 032 private String defaultServer; 033 private String defaultDatabase; 034 private String defaultSchema; 035 036 /** 037 * The identifier configuration every environment this parser builds is indexed with; null 038 * means each environment uses its own vendor's default. 039 */ 040 private IdentifierProfile identifierProfile; 041 042 /** 043 * Configure the identifier rules the parsed environments register their metadata under. 044 * 045 * <p>Set it before {@link #parseSQLEnv}. A metadata file that declares its own 046 * {@code dbVendor} is built with that vendor's default instead, since a profile is a 047 * configuration of one dialect ({@link IdentifierProfile#forVendorOr}). 048 * 049 * @param identifierProfile the analysis's configuration, or null for vendor defaults 050 * @return this parser 051 */ 052 public TJSONSQLEnvParser withIdentifierProfile(IdentifierProfile identifierProfile) { 053 this.identifierProfile = identifierProfile; 054 return this; 055 } 056 057 public TJSONSQLEnvParser(String defaultServer, String defaultDatabase, String defaultSchema) { 058 this.defaultServer = defaultServer; 059 this.defaultDatabase = defaultDatabase; 060 this.defaultSchema = defaultSchema; 061 } 062 063 public TSQLEnv[] parseSQLEnv(EDbVendor vendor, String sql) { 064 return parseSQLEnv(vendor, sql, null); 065 } 066 067 /** 068 * @param explicitBaseDir directory to resolve a sharded manifest's relative 069 * catalog/source paths against, used when {@code sql} is inline manifest 070 * CONTENT (which carries no location of its own). Ignored when {@code sql} 071 * is a file path, since the path already yields the base directory. 072 */ 073 public TSQLEnv[] parseSQLEnv(EDbVendor vendor, String sql, String explicitBaseDir) { 074 if (SQLUtil.isEmpty(sql)) 075 return null; 076 077 String trimSQL = sql.trim(); 078 079 // Check if input is a file path (for sqlflow-sharded format). For inline 080 // content, fall back to the caller-supplied base directory so relative 081 // catalog paths still resolve. 082 String baseDir = explicitBaseDir; 083 String jsonContent = trimSQL; 084 085 if (!trimSQL.startsWith("{") && !trimSQL.startsWith("[")) { 086 // Try to interpret as a file path 087 File file = new File(trimSQL); 088 if (file.exists() && file.isFile()) { 089 Path filePath = file.toPath(); 090 Path parentDir = filePath.getParent(); 091 baseDir = (parentDir != null) ? parentDir.toString() : ""; 092 093 try { 094 jsonContent = new String(Files.readAllBytes(filePath), StandardCharsets.UTF_8).trim(); 095 } catch (IOException e) { 096 Logger.getLogger(TJSONSQLEnvParser.class.getName()) 097 .log(Level.WARNING, "Failed to read file: " + trimSQL, e); 098 return null; 099 } 100 } 101 } 102 103 if (jsonContent.startsWith("{") && jsonContent.endsWith("}")) { 104 return getJSONSQLEnv(vendor, jsonContent, baseDir); 105 } 106 107 if (jsonContent.startsWith("[") && jsonContent.endsWith("]")) { 108 return getMultipleJSONSQLEnv(vendor, jsonContent); 109 } 110 111 return null; 112 } 113 114 private TSQLEnv[] getMultipleJSONSQLEnv(EDbVendor vendor, String trimSQL) { 115 try { 116 List json = (List) JSON.parseObject(trimSQL); 117 if (trimSQL.indexOf("createdBy") != -1) { 118 if (trimSQL.toLowerCase(java.util.Locale.ROOT).indexOf("sqldep") != -1) { 119 return new TSQLEnv[] { new MultipleSQLDepSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) }; 120 } 121 if (trimSQL.toLowerCase(java.util.Locale.ROOT).indexOf("grabit") != -1) { 122 return new TSQLEnv[] { new MultipleGrabitSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) }; 123 } 124 } 125 } catch (Exception e) { 126 Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING, "Parse json failed.", e); 127 } 128 return null; 129 } 130 131 private TSQLEnv[] getJSONSQLEnv(EDbVendor vendor, String trimSQL, String baseDir) { 132 try { 133 Map json = (Map) JSON.parseObject(trimSQL); 134 // Route on the authoritative `format` field BEFORE the createdBy 135 // product brand. A sqlflow-sharded manifest carries the exporter 136 // brand (SQLdep/grabit/sqlflow) in createdBy, so a brand-first check 137 // sent a SQLdep-branded sharded manifest to the legacy SQLDepSQLEnv 138 // and silently lost the external catalog used for column resolution. 139 if (MetadataReader.isSqlflowSharded(trimSQL)) { 140 if (!MetadataReader.isSupportedSqlflowSharded(trimSQL)) { 141 // Unknown future formatVersion: do not load a catalog we may 142 // mis-read. No lineage catalog is better than a wrong one. 143 Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING, 144 "Unsupported sqlflow-sharded formatVersion " 145 + MetadataReader.shardedFormatVersion(trimSQL) 146 + "; this build supports up to " 147 + MetadataReader.SUPPORTED_SHARDED_FORMAT_VERSION 148 + ". SQLEnv catalog not loaded."); 149 return null; 150 } 151 List servers = (List) json.get("servers"); 152 if (servers == null) 153 return null; 154 // Declared once, at the manifest root, for the manifest and all its shards. 155 IdentifierInputForm shardedNameForm = TSQLEnv.parseNameForm( 156 json.get("nameForm"), IdentifierInputForm.SQL_LEXICAL); 157 List<TSQLEnv> sqlEnvs = new ArrayList<TSQLEnv>(); 158 for (int i = 0; i < servers.size(); i++) { 159 Map server = (Map) servers.get(i); 160 String dbVendor = (String) server.get("dbVendor"); 161 if (SQLUtil.isEmpty(dbVendor)) { 162 sqlEnvs.add(new SqlflowShardedSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, server, baseDir, shardedNameForm, identifierProfile)); 163 } else { 164 sqlEnvs.add(new SqlflowShardedSQLEnv(defaultServer, defaultDatabase, defaultSchema, EDbVendor.valueOf(dbVendor), server, baseDir, shardedNameForm, identifierProfile)); 165 } 166 } 167 return sqlEnvs.toArray(new TSQLEnv[0]); 168 } 169 if (json.containsKey("createdBy")) { 170 String createdBy = (String) json.get("createdBy"); 171 if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("sqldep") != -1) { 172 TSQLEnv.requireSqlLexicalNameForm(json.get("nameForm"), "SQLDep"); 173 return new TSQLEnv[] { new SQLDepSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) }; 174 } 175 if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("grabit") != -1) { 176 TSQLEnv.requireSqlLexicalNameForm(json.get("nameForm"), "Grabit"); 177 return new TSQLEnv[] { new GrabitSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, json, identifierProfile) }; 178 } 179 if (createdBy.toLowerCase(java.util.Locale.ROOT).indexOf("sqlflow") != -1) { 180 { 181 List servers = (List) json.get("servers"); 182 if (servers == null) 183 return null; 184 // One declaration per file: a supplier may send SQL spellings or catalog names, 185 // and the only thing that can be asked of them is to be consistent within a 186 // file. This format defaults to SQL_LEXICAL, which is what every existing file 187 // (and the sqlflow-ingester exporter) uses. 188 IdentifierInputForm nameForm = TSQLEnv.parseNameForm( 189 json.get("nameForm"), IdentifierInputForm.SQL_LEXICAL); 190 List<TSQLEnv> sqlEnvs = new ArrayList<TSQLEnv>(); 191 for (int i = 0; i < servers.size(); i++) { 192 Map server = (Map) servers.get(i); 193 String dbVendor = (String) server.get("dbVendor"); 194 if (SQLUtil.isEmpty(dbVendor)) { 195 sqlEnvs.add(new SqlflowSQLEnv(defaultServer, defaultDatabase, defaultSchema, vendor, server, nameForm, identifierProfile)); 196 } else { 197 sqlEnvs.add(new SqlflowSQLEnv(defaultServer, defaultDatabase, defaultSchema, EDbVendor.valueOf(dbVendor), server, nameForm, identifierProfile)); 198 } 199 } 200 return sqlEnvs.toArray(new TSQLEnv[0]); 201 } 202 } 203 } 204 } catch (TSQLEnv.InvalidNameFormException e) { 205 // Not swallowed: see InvalidNameFormException. 206 throw e; 207 } catch (Exception e) { 208 Logger.getLogger(TJSONSQLEnvParser.class.getName()).log(Level.WARNING, "Parse json failed.", e); 209 } 210 return null; 211 } 212}