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}