001package gudusoft.gsqlparser.lineage.v2.producer; 002 003import java.util.Collections; 004import java.util.EnumMap; 005import java.util.LinkedHashMap; 006import java.util.Map; 007 008import gudusoft.gsqlparser.EDbVendor; 009import gudusoft.gsqlparser.lineage.v2.SqllrResources; 010import gudusoft.gsqlparser.lineage.v2.identity.SegmentKeyV1; 011import gudusoft.gsqlparser.sqlenv.IdentifierProfile; 012 013/** 014 * Whether an analysis may mint portable SQL-LR identities, and with which key function. 015 * 016 * <p>The standard's rule data, not GSP's name service, decides a document's canonical keys, so a portable 017 * identity is only honest when GSP analyses under the same naming policy that the dependency snapshot declares 018 * ({@code lineage-spec/snapshots/FORMAT.md}, Section 6: different settings are a different snapshot). The 019 * producer ships one snapshot, the draft snapshot, so:</p> 020 * <ul> 021 * <li>a dialect outside the snapshot's six rule sets mints no portable identity (its endpoints are 022 * {@code LOCAL});</li> 023 * <li>for one of the six, the analysis must run under GSP's default profile for the vendor, whose flags are 024 * the snapshot's effective flags and collations; any other effective profile (other flags, a group or 025 * object-type override) is refused with {@link SqllrProducerException}, never published under a snapshot 026 * that does not describe it.</li> 027 * </ul> 028 * 029 * @since 4.2.17 030 */ 031public final class IdentityPolicy { 032 033 /** The error code of a refused configuration. */ 034 public static final String CONFIGURATION_UNSUPPORTED = "PRODUCER_CONFIGURATION_UNSUPPORTED"; 035 036 private static final Map<EDbVendor, String> DIALECTS; 037 038 static { 039 Map<EDbVendor, String> map = new EnumMap<EDbVendor, String>(EDbVendor.class); 040 map.put(EDbVendor.dbvoracle, "oracle"); 041 map.put(EDbVendor.dbvmssql, "sqlserver"); 042 map.put(EDbVendor.dbvazuresql, "sqlserver"); 043 map.put(EDbVendor.dbvmysql, "mysql"); 044 map.put(EDbVendor.dbvpostgresql, "postgresql"); 045 map.put(EDbVendor.dbvbigquery, "bigquery"); 046 map.put(EDbVendor.dbvsnowflake, "snowflake"); 047 DIALECTS = Collections.unmodifiableMap(map); 048 } 049 050 private final EDbVendor vendor; 051 private final String dialect; 052 private final SegmentKeyV1 keys; 053 054 private IdentityPolicy(EDbVendor vendor, String dialect, SegmentKeyV1 keys) { 055 this.vendor = vendor; 056 this.dialect = dialect; 057 this.keys = keys; 058 } 059 060 /** 061 * The SQL-LR dialect code of a vendor whose identifier rules the draft snapshot carries, or {@code null}. 062 * Derived dialects (Greenplum, Redshift, GaussDB, ...) are not mapped: their naming rules are not the 063 * parent's. 064 */ 065 public static String dialectCode(EDbVendor vendor) { 066 return DIALECTS.get(vendor); 067 } 068 069 /** The vendors that can mint portable identities. */ 070 public static Map<EDbVendor, String> portableVendors() { 071 return DIALECTS; 072 } 073 074 /** 075 * The policy of one analysis. 076 * 077 * @param vendor the analysed vendor 078 * @param effective the analysis's frozen identifier profile; {@code null} means the vendor default 079 * @throws SqllrProducerException {@link #CONFIGURATION_UNSUPPORTED} when the vendor has a rule set but the 080 * profile differs from the vendor default the draft snapshot describes 081 */ 082 public static IdentityPolicy of(EDbVendor vendor, IdentifierProfile effective) { 083 String dialect = dialectCode(vendor); 084 if (dialect == null) { 085 return new IdentityPolicy(vendor, null, null); 086 } 087 // the parser analyses Azure SQL as SQL Server (TGSqlParser), so its frozen profile names dbvmssql 088 EDbVendor analysed = effective != null ? effective.getVendor() : vendor; 089 if (!dialect.equals(dialectCode(analysed))) { 090 throw new SqllrProducerException(CONFIGURATION_UNSUPPORTED, "the analysis profile is for " + analysed 091 + ", not for the requested " + vendor); 092 } 093 IdentifierProfile standard = IdentifierProfile.forVendor(analysed, IdentifierProfile.VendorFlags.defaults()); 094 if (effective != null && !effective.hasSameEffectivePolicyAs(standard)) { 095 throw new SqllrProducerException(CONFIGURATION_UNSUPPORTED, "the identifier profile of this analysis (" 096 + effective + ") is not the " + vendor + " default that the shipped dependency snapshot " 097 + "describes; portable identities would be minted under rules the document does not declare"); 098 } 099 SegmentKeyV1 keys = SegmentKeyV1.draft(dialect); 100 checkSnapshotMatchesDefaults(dialect, keys); 101 return new IdentityPolicy(vendor, dialect, keys); 102 } 103 104 /** 105 * The draft snapshot's effective settings must be GSP's vendor defaults; a snapshot change that breaks this 106 * fails here instead of minting keys under the wrong rules. 107 */ 108 static void checkSnapshotMatchesDefaults(String dialect, SegmentKeyV1 keys) { 109 IdentifierProfile.VendorFlags defaults = IdentifierProfile.VendorFlags.defaults(); 110 Map<String, Object> expected = new LinkedHashMap<String, Object>(); 111 Map<String, String> collations = new LinkedHashMap<String, String>(); 112 if ("mysql".equals(dialect)) { 113 expected.put("lowerCaseTableNames", Long.valueOf(defaults.mysqlLowerCaseTableNames)); 114 expected.put("ansiQuotes", Boolean.valueOf(defaults.mysqlAnsiQuotes)); 115 } else if ("snowflake".equals(dialect)) { 116 expected.put("quotedIdentifiersIgnoreCase", Boolean.valueOf(defaults.snowflakeQuotedIdentifiersIgnoreCase)); 117 } else if ("bigquery".equals(dialect)) { 118 expected.put("datasetCaseInsensitive", Boolean.FALSE); 119 } else if ("sqlserver".equals(dialect)) { 120 collations.put("INSTANCE", defaults.defaultCollation); 121 collations.put("DATABASE", defaults.defaultCollation); 122 } 123 if (!expected.equals(keys.getFlags()) || !collations.equals(keys.getCollations())) { 124 throw new IllegalStateException("the shipped snapshot's " + dialect + " settings " + keys.getFlags() + " " 125 + keys.getCollations() + " are not GSP's defaults " + expected + " " + collations); 126 } 127 } 128 129 public EDbVendor getVendor() { 130 return vendor; 131 } 132 133 /** The dialect code, or {@code null} when the vendor mints no portable identity. */ 134 public String getDialect() { 135 return dialect; 136 } 137 138 /** Whether endpoints of this analysis can be {@code PORTABLE} at all. */ 139 public boolean isPortable() { 140 return keys != null; 141 } 142 143 /** The key function; {@code null} when {@link #isPortable()} is false. */ 144 public SegmentKeyV1 getKeys() { 145 return keys; 146 } 147 148 /** 149 * The id of the snapshot in force: the shipped one, also for a dialect outside its rule sets, whose analysis still 150 * runs under its algorithms, profiles and canonical forms and mints no portable identity (the analysis context 151 * requires the id, spec Section 6.2; plan 06 gate E4). 152 */ 153 public String getSnapshotId() { 154 return (String) ((Map<?, ?>) SqllrResources.json(SegmentKeyV1.DRAFT_SNAPSHOT)).get("snapshotId"); 155 } 156}