001package gudusoft.gsqlparser.lineage.v2.producer;
002
003import java.lang.reflect.Field;
004import java.lang.reflect.Modifier;
005import java.util.Collections;
006import java.util.LinkedHashMap;
007import java.util.Map;
008import java.util.TreeMap;
009
010import gudusoft.gsqlparser.dlineage.dataflow.model.Option;
011import gudusoft.gsqlparser.lineage.v2.SqllrResources;
012import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
013
014/**
015 * The analysis options of a SQL-LR production run (plan 06, Section 3.1).
016 *
017 * <p>Every field of {@link Option} has one class:</p>
018 * <ul>
019 *   <li>{@link Kind#FIXED}: the producer sets it. Anything that hides an input, picks a candidate or changes what
020 *   the runtime graph contains is fixed, because a digest only records a suppression, it does not repair it.</li>
021 *   <li>{@link Kind#CALLER}: the caller decides it ({@link ProducerConfig}); it enters
022 *   {@code effectiveOptionsDigest}.</li>
023 *   <li>{@link Kind#IRRELEVANT}: it changes only legacy output, progress reporting or delegation, which the producer
024 *   never uses.</li>
025 * </ul>
026 *
027 * <p>{@code ProducerOptionsTest} fails when {@link Option} gains a field this table does not classify.</p>
028 *
029 * @since 4.2.17
030 */
031public final class ProducerOptions {
032
033    /** The class of an {@link Option} field. */
034    public enum Kind {
035        FIXED, CALLER, IRRELEVANT
036    }
037
038    private static final Map<String, Kind> KINDS;
039
040    static {
041        Map<String, Kind> m = new LinkedHashMap<String, Kind>();
042        // set by the caller (ProducerConfig), recorded in the digest
043        for (String name : new String[] {"vendor", "defaultServer", "defaultDatabase", "defaultSchema",
044            "analyzeDynamicSql"}) {
045            m.put(name, Kind.CALLER);
046        }
047        // fixed by the producer: they change the runtime graph or hide inputs
048        for (String name : new String[] {"simpleOutput", "showJoin", "ignoreRecordSet", "ignoreTemporaryTable",
049            "ignoreTopSelect", "ignoreUnusedSynonym", "showCallRelation", "linkOrphanColumnToFirstTable",
050            "transform", "transformCoordinate", "collectAuthoritativeLineageEvidence", "showImplicitSchema",
051            "showCountTableColumn", "showConstantTable", "showCaseWhenAsDirect", "filePathDatabase", "filePathSchema",
052            "showERDiagram", "targetResultSetTypes", "filterRelationTypes", "sqlflowIgnoreFunction",
053            "dynamicSqlTrustMode", "excludedProcedureNames", "excludedProcedurePatterns", "showCandidateTable",
054            "ignoreInsertIntoValues", "enablePipelinedStitching", "identityFirstVariablePools",
055            "maxPipelinedExpansionDepth", "maxStitchedSourcesPerColumn", "analyzeMode", "identifierProfile",
056            "reportDynamicSqlSitesAsErrors", "dynamicSqlCaseSensitiveCompare", "recoverRoutineBodyErrors",
057            "assumeExternalScriptPassthrough", "autoDetectLargeFile",
058            // client placeholders are not T-SQL, and Power Query input is not a producer input format
059            "enableMssqlColonBindVariables", "powerQueryInnerVendor"}) {
060            m.put(name, Kind.FIXED);
061        }
062        // legacy output, progress, delegation and comment collection only
063        for (String name : new String[] {"collectCommentBindings", "commentBindingMaxRawLength",
064            "commentBindingMaxCommentsPerSourceUnit", "commentBindingMaxTotalRawLength", "handleListener",
065            "textFormat", "simpleShowFunction", "simpleShowSynonym", "simpleShowUdfFunctionOnly", "simpleShowCursor",
066            "simpleShowVariable", "simpleRetainIntermediate", "simpleShowTopSelectResultSet", "ignoreCoordinate",
067            "startId", "output", "traceSQL", "showComment", "traceProcedure", "parallel", "simpleShowRelationTypes",
068            "normalizeOutput", "traceTablePosition", "largeSqlInfoCountThreshold", "largeSqlTotalSizeThreshold",
069            "largeQueryCountThreshold", "largeShardCountThreshold", "largeFileSplitSizeMB",
070            "largeSqlThresholdMultiplier", "estimatedMemoryPerTaskMB"}) {
071            m.put(name, Kind.IRRELEVANT);
072        }
073        KINDS = Collections.unmodifiableMap(m);
074    }
075
076    private ProducerOptions() {
077    }
078
079    /** The class of every {@link Option} field this table knows. */
080    public static Map<String, Kind> kinds() {
081        return KINDS;
082    }
083
084    /** The instance fields of {@link Option}, by declaration. */
085    static Map<String, Field> optionFields() {
086        Map<String, Field> fields = new TreeMap<String, Field>();
087        for (Field field : Option.class.getDeclaredFields()) {
088            if (!Modifier.isStatic(field.getModifiers()) && !field.isSynthetic()) {
089                fields.put(field.getName(), field);
090            }
091        }
092        return fields;
093    }
094
095    /** The {@link Option} a production run analyses with. */
096    public static Option option(ProducerConfig config) {
097        Option option = new Option();
098        option.setVendor(config.getVendor());
099        option.setOutput(false);
100        option.setSimpleOutput(false);
101        option.setShowJoin(false);
102        option.setIgnoreRecordSet(false);
103        option.setIgnoreTemporaryTable(false);
104        option.setIgnoreTopSelect(false);
105        option.setShowCallRelation(false);
106        option.setLinkOrphanColumnToFirstTable(false);
107        option.setTransform(false);
108        option.setTransformCoordinate(false);
109        option.setCollectAuthoritativeLineageEvidence(false);
110        option.setShowImplicitSchema(false);
111        option.setShowCountTableColumn(true);
112        option.setShowConstantTable(true);
113        option.setShowCaseWhenAsDirect(true);
114        option.setIgnoreInsertIntoValues(false);
115        option.setIdentityFirstVariablePools(true);
116        option.setAutoDetectLargeFile(false);
117        option.setReportDynamicSqlSitesAsErrors(false);
118        option.setRecoverRoutineBodyErrors(false);
119        option.setEnableMssqlColonBindVariables(false);
120        option.setPowerQueryInnerVendor(null);
121        option.setAnalyzeDynamicSql(config.isAnalyzeDynamicSql());
122        if (config.getDefaultServer() != null) {
123            option.setDefaultServer(config.getDefaultServer());
124        }
125        if (config.getDefaultDatabase() != null) {
126            option.setDefaultDatabase(config.getDefaultDatabase());
127        }
128        if (config.getDefaultSchema() != null) {
129            option.setDefaultSchema(config.getDefaultSchema());
130        }
131        return option;
132    }
133
134    /**
135     * The effective analysis options as a JSON object: every {@link Kind#FIXED} and {@link Kind#CALLER} field of the
136     * option a run uses, plus the producer's own settings that change what a document asserts. Field names carry
137     * the {@code gsp.} prefix; values are JSON scalars or sorted lists.
138     */
139    public static Map<String, Object> effectiveOptions(ProducerConfig config) {
140        Option option = option(config);
141        Map<String, Object> out = new TreeMap<String, Object>();
142        for (Map.Entry<String, Field> entry : optionFields().entrySet()) {
143            Kind kind = KINDS.get(entry.getKey());
144            if (kind == Kind.IRRELEVANT || kind == null) {
145                continue;
146            }
147            Object value;
148            try {
149                Field field = entry.getValue();
150                field.setAccessible(true);
151                value = field.get(option);
152            } catch (IllegalAccessException e) {
153                throw new IllegalStateException(e);
154            }
155            out.put("gsp.option." + entry.getKey(), jsonValue(value));
156        }
157        out.put("gsp.producer.constantDisclosure", config.getConstantDisclosure());
158        out.put("gsp.producer.foldMaxStates", Long.valueOf(config.getMaxFoldStates()));
159        out.put("gsp.producer.foldMaxWorlds", Long.valueOf(config.getMaxFoldWorlds()));
160        return out;
161    }
162
163    /** {@code effectiveOptionsDigest}: SHA-256 of the JCS form of {@link #effectiveOptions}. */
164    public static String effectiveOptionsDigest(ProducerConfig config) {
165        return SqllrResources.sha256Hex(CanonicalJson.jcsBytes(effectiveOptions(config)));
166    }
167
168    private static Object jsonValue(Object value) {
169        if (value == null || value instanceof Boolean || value instanceof String) {
170            return value;
171        }
172        if (value instanceof Integer || value instanceof Long || value instanceof Short) {
173            return Long.valueOf(((Number) value).longValue());
174        }
175        if (value instanceof Enum<?>) {
176            return ((Enum<?>) value).name();
177        }
178        if (value instanceof java.util.Collection<?>) {
179            java.util.List<String> items = new java.util.ArrayList<String>();
180            for (Object item : (java.util.Collection<?>) value) {
181                items.add(String.valueOf(jsonValue(item)));
182            }
183            Collections.sort(items);
184            return items;
185        }
186        if (value instanceof gudusoft.gsqlparser.sqlenv.IdentifierProfile) {
187            // the producer runs only under the vendor default profile (IdentityPolicy); its fingerprint names it
188            return Long.valueOf(((gudusoft.gsqlparser.sqlenv.IdentifierProfile) value).getFingerprint());
189        }
190        return String.valueOf(value);
191    }
192}