001package gudusoft.gsqlparser.lineage.v2.identity;
002
003import gudusoft.gsqlparser.lineage.v2.SqllrException;
004import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
005
006import java.io.ByteArrayOutputStream;
007import java.math.BigInteger;
008import java.security.MessageDigest;
009import java.security.NoSuchAlgorithmException;
010import java.util.ArrayList;
011import java.util.Arrays;
012import java.util.Collection;
013import java.util.Collections;
014import java.util.Comparator;
015import java.util.HashMap;
016import java.util.HashSet;
017import java.util.LinkedHashMap;
018import java.util.List;
019import java.util.Map;
020import java.util.regex.Pattern;
021
022/**
023 * The identity algorithm family {@code PUBLIC-LINEAGE-ID-V1} ({@code lineage-spec/algorithms/identity-v1.md}).
024 *
025 * <p>Each algorithm takes its inputs as a JSON value in the shape of the specification's vectors and of the
026 * reference implementation ({@code sqllr_ids.py}, {@code compute}), validates them in the reference's order,
027 * and returns an {@link IdResult}: the identity status, and the framed input, digest, id and numeric key when
028 * an id is computed. Canonical segment keys are inputs; minting them belongs to the producer.</p>
029 *
030 * @since 4.2.17
031 */
032public final class IdentityV1 {
033
034    public static final String FAMILY = "PUBLIC-LINEAGE-ID-V1";
035    public static final String PATH_PROFILE_V1 = "sqllr-path-1";
036
037    public static final String SOURCE_UNIT = "SourceUnitIdV1";
038    public static final String ENDPOINT = "EndpointIdV1";
039    public static final String FACT = "FactIdV1";
040    public static final String OCCURRENCE_KEY = "OccurrenceKeyV1";
041    public static final String ANALYSIS_CONTEXT = "AnalysisContextV1";
042    public static final String OCCURRENCE_RECORD = "OccurrenceRecordIdV1";
043    public static final String EXPLAIN_NODE_KEY = "ExplainNodeKeyV1";
044    public static final String EXPLAIN_NODE_RECORD = "ExplainNodeRecordIdV1";
045    public static final String DEPENDENCY_SNAPSHOT = "DependencySnapshotIdV1";
046    public static final String METADATA_DIGEST = "MetadataDigestV1";
047
048    /** Algorithm name to the prefix of its ids. */
049    public static final Map<String, String> PREFIXES;
050
051    static {
052        Map<String, String> prefixes = new LinkedHashMap<String, String>();
053        prefixes.put(SOURCE_UNIT, "su1");
054        prefixes.put(ENDPOINT, "ep1");
055        prefixes.put(FACT, "fact1");
056        prefixes.put(OCCURRENCE_KEY, "occk1");
057        prefixes.put(ANALYSIS_CONTEXT, "actx1");
058        prefixes.put(OCCURRENCE_RECORD, "occr1");
059        prefixes.put(EXPLAIN_NODE_KEY, "xnk1");
060        prefixes.put(EXPLAIN_NODE_RECORD, "xnr1");
061        prefixes.put(DEPENDENCY_SNAPSHOT, "snap1");
062        PREFIXES = Collections.unmodifiableMap(prefixes);
063    }
064
065    public static final String PORTABLE = "PORTABLE";
066    public static final String LOCAL = "LOCAL";
067    public static final String UNAVAILABLE_COLLATION = "UNAVAILABLE_COLLATION";
068    public static final String UNAVAILABLE_MIXED_PROFILE = "UNAVAILABLE_MIXED_PROFILE";
069    public static final String OPAQUE_PRODUCER_LOCAL = "OPAQUE_PRODUCER_LOCAL";
070
071    private static final Pattern ID = Pattern.compile("(su1|ep1|fact1|occk1|actx1|occr1|xnk1|xnr1|snap1)-([0-9a-f]{64})");
072    private static final Pattern HEX64 = Pattern.compile("[0-9a-f]{64}");
073    /** One step of an {@code sqllr-path-1} path. A whole path is checked step by step: Java's regex engine
074     * recurses once per repetition of a group, so one pattern over a long path would exhaust the stack. */
075    private static final Pattern PATH_STEP = Pattern.compile("[A-Z][A-Z0-9_]*\\[(0|[1-9][0-9]*)\\]");
076    private static final long MAX_ORDINAL = 9007199254740991L;
077
078    private static List<String> list(String... values) {
079        return Collections.unmodifiableList(Arrays.asList(values));
080    }
081
082    public static final List<String> FACT_KINDS = list("VALUE", "ROW", "DATASET_DEPENDENCY", "CALL");
083    public static final List<String> ROW_ROLES = list("FILTER", "JOIN_CONDITION", "GROUP_BY", "HAVING", "ORDER_BY",
084            "WINDOW_PARTITION", "WINDOW_ORDER", "DISTINCT", "QUALIFY", "LIMIT", "CONDITIONAL", "ROW_PROPAGATION", "UNKNOWN");
085    public static final List<String> NON_PORTABLE = list(LOCAL, UNAVAILABLE_COLLATION, UNAVAILABLE_MIXED_PROFILE,
086            OPAQUE_PRODUCER_LOCAL);
087    public static final List<String> DIGEST_DOMAINS = list("RAW_BYTES_SHA256", "DECODED_TEXT_SHA256",
088            "REDACTED_TEXT_SHA256", "KEYED_TOKEN");
089    public static final List<String> METADATA_STATUSES = list("NONE", "PARTIAL", "COMPLETE");
090    public static final List<String> ANALYSIS_FEATURES = list("VALUE", "ROW", "DATASET_DEPENDENCY", "CALL",
091            "CONSTANT_INPUTS", "DYNAMIC_SQL", "ROUTINE_BODIES", "TEMPORARY_OBJECTS", "WILDCARD_EXPANSION", "NESTED_FIELDS");
092    public static final List<String> CAPABILITY_STATUSES = list("SUPPORTED", "UNSUPPORTED", "PARTIAL");
093    public static final List<String> BOM_HANDLING = list("NO_BOM", "BOM_STRIPPED");
094    public static final List<String> LINE_ENDING_HANDLING = list("PRESERVED", "NORMALIZED_LF");
095    public static final List<String> ROUTINE_NAMESPACES = list("CATALOG");
096    public static final List<String> ROUTINE_KINDS = list("FUNCTION", "PROCEDURE");
097    public static final List<String> SIGNATURE_FORMS = list("NOT_OVERLOADABLE", "PARAMETERS", "CATALOG_OVERLOAD_ID");
098    public static final List<String> PARAMETER_MODES = list("IN", "OUT", "INOUT", "VARIADIC");
099    public static final List<String> PATH_SEGMENT_KINDS = list("FIELD", "ARRAY_ELEMENT", "MAP_KEY", "MAP_VALUE", "INDEX");
100    public static final List<String> CANDIDATE_SET_MEMBERSHIP = list("COMPLETE", "INCOMPLETE");
101    public static final List<String> PATH_STEP_CODES = list(
102            "SELECT", "INSERT", "UPDATE", "DELETE", "MERGE", "CREATE_TABLE", "CREATE_VIEW", "COPY", "CALL", "EXECUTE",
103            "TARGET", "COLUMN_LIST", "SELECT_LIST", "FROM", "JOIN", "ON", "USING", "WHERE", "GROUP_BY", "HAVING",
104            "QUALIFY", "ORDER_BY", "LIMIT", "WINDOW", "PARTITION_BY", "WITH", "CTE", "SET_OP", "BRANCH", "SET",
105            "VALUES", "WHEN", "RETURNING", "SUBQUERY", "FUNCTION_ARG", "CASE_WHEN", "CASE_THEN", "CASE_ELSE", "PIVOT",
106            "LATERAL", "SOURCE", "CREATE_ROUTINE", "RETURN", "BLOCK", "ASSIGNMENT", "ASSIGN", "CONDITION", "CURSOR",
107            "FETCH");
108    public static final List<String> EXPLAIN_NODE_KINDS = list("RESULT_SET", "CTE", "SUBQUERY", "SET_OPERATION",
109            "ROW_SET", "FIELD", "FUNCTION", "CASE", "EXPRESSION", "TERMINAL", "TABLE_FUNCTION", "PIVOT");
110
111    /**
112     * Endpoint kind (registry group {@code endpointKinds}) to identity class (Section 5.2.1); a kind mapped to
113     * null has no identity class in V1 and is {@code LOCAL}.
114     */
115    public static final Map<String, String> IDENTITY_CLASS;
116
117    static {
118        Map<String, String> classes = new LinkedHashMap<String, String>();
119        for (String kind : new String[] {"RELATION", "TABLE", "VIEW", "MATERIALIZED_VIEW", "EXTERNAL_TABLE", "SYNONYM"}) {
120            classes.put(kind, "RELATION");
121        }
122        classes.put("FIELD", "FIELD");
123        classes.put("ROW_SET", "ROW_SET");
124        classes.put("ROUTINE", "ROUTINE");
125        for (String kind : new String[] {"QUERY_RESULT", "TEMP_TABLE", "TABLE_VARIABLE", "CTE", "VARIABLE", "PARAMETER",
126                "SEQUENCE", "STAGE", "STORAGE_LOCATION"}) {
127            classes.put(kind, null);
128        }
129        IDENTITY_CLASS = Collections.unmodifiableMap(classes);
130    }
131
132    /** Precedence of Section 4.2: the higher rank wins. */
133    private static final Map<String, Integer> STATUS_RANK = new HashMap<String, Integer>();
134    private static final Map<String, String> KEY_UNAVAILABLE = new HashMap<String, String>();
135
136    static {
137        STATUS_RANK.put(PORTABLE, 0);
138        STATUS_RANK.put(UNAVAILABLE_COLLATION, 1);
139        STATUS_RANK.put(UNAVAILABLE_MIXED_PROFILE, 2);
140        STATUS_RANK.put(LOCAL, 3);
141        STATUS_RANK.put(OPAQUE_PRODUCER_LOCAL, 4);
142        KEY_UNAVAILABLE.put("COLLATION", UNAVAILABLE_COLLATION);
143        KEY_UNAVAILABLE.put("MIXED_PROFILE", UNAVAILABLE_MIXED_PROFILE);
144    }
145
146    private IdentityV1() {
147    }
148
149    // ---------------------------------------------------------------------------------------------
150    // Results
151    // ---------------------------------------------------------------------------------------------
152
153    /** The outcome of one algorithm run. */
154    public static final class IdResult {
155        private final String status;
156        private final byte[] framed;
157        private final String sha256Hex;
158        private final String id;
159        private final String numericKey;
160
161        IdResult(String status, byte[] framed, String sha256Hex, String id, String numericKey) {
162            this.status = status;
163            this.framed = framed;
164            this.sha256Hex = sha256Hex;
165            this.id = id;
166            this.numericKey = numericKey;
167        }
168
169        /** The identity status ({@code PORTABLE}, {@code LOCAL}, ...). */
170        public String getStatus() {
171            return status;
172        }
173
174        /** The framed input (the identity content), or null when no id is computed. */
175        public byte[] getFramed() {
176            return framed == null ? null : framed.clone();
177        }
178
179        public String getFramedHex() {
180            return framed == null ? null : Framing.hex(framed);
181        }
182
183        public String getSha256Hex() {
184            return sha256Hex;
185        }
186
187        /** The id ({@code ep1-…}), the bare digest for {@code MetadataDigestV1}, or null. */
188        public String getId() {
189            return id;
190        }
191
192        /** {@code NumericKeyV1} of the id, or null. */
193        public String getNumericKey() {
194            return numericKey;
195        }
196    }
197
198    /** The status and the fields an algorithm frames; null fields when no id is computed. */
199    private static final class Outcome {
200        final String status;
201        final List<Object> fields;
202
203        Outcome(String status, List<Object> fields) {
204            this.status = status;
205            this.fields = fields;
206        }
207    }
208
209    private static Outcome portable(Object... fields) {
210        return new Outcome(PORTABLE, Arrays.asList(fields));
211    }
212
213    private static Outcome noId(String status) {
214        return new Outcome(status, null);
215    }
216
217    /**
218     * Runs one algorithm on its inputs.
219     *
220     * @param algorithm one of the names of Section 2, such as {@code EndpointIdV1}
221     * @throws SqllrException with an {@code ID_*} code (or a {@code SCHEMA_*} code for JSON that canonical JSON
222     *                        cannot carry) when the inputs are rejected
223     */
224    public static IdResult compute(String algorithm, Object inputs) {
225        Outcome outcome;
226        if (SOURCE_UNIT.equals(algorithm)) {
227            outcome = sourceUnitId(inputs);
228        } else if (ENDPOINT.equals(algorithm)) {
229            outcome = endpointId(inputs);
230        } else if (FACT.equals(algorithm)) {
231            outcome = factId(inputs);
232        } else if (OCCURRENCE_KEY.equals(algorithm)) {
233            outcome = occurrenceKey(inputs);
234        } else if (ANALYSIS_CONTEXT.equals(algorithm)) {
235            outcome = analysisContext(inputs);
236        } else if (OCCURRENCE_RECORD.equals(algorithm)) {
237            Map<?, ?> in = members(inputs, list("occurrenceKey", "analysisContext"), NONE, "OccurrenceRecordIdV1 input");
238            outcome = portable(ref(in.get("occurrenceKey"), "occk1", "occurrenceKey"),
239                    ref(in.get("analysisContext"), "actx1", "analysisContext"));
240        } else if (EXPLAIN_NODE_KEY.equals(algorithm)) {
241            outcome = explainNodeKey(inputs);
242        } else if (EXPLAIN_NODE_RECORD.equals(algorithm)) {
243            Map<?, ?> in = members(inputs, list("nodeKey", "analysisContext"), NONE, "ExplainNodeRecordIdV1 input");
244            outcome = portable(ref(in.get("nodeKey"), "xnk1", "nodeKey"),
245                    ref(in.get("analysisContext"), "actx1", "analysisContext"));
246        } else if (DEPENDENCY_SNAPSHOT.equals(algorithm)) {
247            Map<?, ?> in = members(inputs, list("manifest"), NONE, "DependencySnapshotIdV1 input");
248            if (!(in.get("manifest") instanceof Map<?, ?>)) {
249                throw invalid("manifest must be an object");
250            }
251            outcome = portable(CanonicalJson.jcsText(in.get("manifest")));
252        } else if (METADATA_DIGEST.equals(algorithm)) {
253            outcome = metadataDigest(inputs);
254        } else {
255            throw invalid("unknown algorithm " + algorithm);
256        }
257        if (outcome.fields == null) {
258            return new IdResult(outcome.status, null, null, null, null);
259        }
260        return mint(algorithm, outcome.status, outcome.fields);
261    }
262
263    private static IdResult mint(String algorithm, String status, List<Object> fields) {
264        ByteArrayOutputStream out = new ByteArrayOutputStream();
265        Framing.frameInto("SQL-LR/" + algorithm, out);
266        for (Object field : fields) {
267            Framing.frameInto(field, out);
268        }
269        byte[] framed = out.toByteArray();
270        String digest = Framing.hex(sha256(framed));
271        if (METADATA_DIGEST.equals(algorithm)) {
272            return new IdResult(status, framed, digest, digest, null);
273        }
274        return new IdResult(status, framed, digest, PREFIXES.get(algorithm) + "-" + digest, digest.substring(0, 32));
275    }
276
277    static byte[] sha256(byte[] data) {
278        try {
279            return MessageDigest.getInstance("SHA-256").digest(data);
280        } catch (NoSuchAlgorithmException e) {
281            throw new IllegalStateException(e);
282        }
283    }
284
285    /**
286     * {@code NumericKeyV1} (Section 5.10): the first 16 bytes of the digest of an id, as 32 lowercase
287     * hexadecimal characters.
288     *
289     * @throws SqllrException {@code ID_INVALID_REFERENCE} for a string that is not an id of the family
290     */
291    public static String numericKey(Object id) {
292        if (!(id instanceof String) || !ID.matcher((String) id).matches()) {
293            throw new SqllrException("ID_INVALID_REFERENCE", "not a PUBLIC-LINEAGE-ID-V1 id: " + id);
294        }
295        String text = (String) id;
296        return text.substring(text.indexOf('-') + 1, text.indexOf('-') + 33);
297    }
298
299    /**
300     * The importer rule of Section 5.10: two different ids with one numeric key are a fatal collision.
301     *
302     * @throws SqllrException {@code ID_NUMERIC_KEY_COLLISION}
303     */
304    public static void checkNumericKeys(Collection<?> ids) {
305        Map<String, Object> seen = new HashMap<String, Object>();
306        for (Object id : ids) {
307            String key = numericKey(id);
308            Object earlier = seen.get(key);
309            if (earlier != null && !earlier.equals(id)) {
310                throw new SqllrException("ID_NUMERIC_KEY_COLLISION", earlier + " and " + id + " share " + key);
311            }
312            seen.put(key, id);
313        }
314    }
315
316    /** True for a well-formed id with this prefix. */
317    public static boolean isId(Object value, String prefix) {
318        return value instanceof String && ID.matcher((String) value).matches() && ((String) value).startsWith(prefix + "-");
319    }
320
321    // ---------------------------------------------------------------------------------------------
322    // Input checks, in the reference's order
323    // ---------------------------------------------------------------------------------------------
324
325    private static final List<String> NONE = Collections.emptyList();
326
327    private static SqllrException invalid(String detail) {
328        return new SqllrException("ID_INVALID_INPUT", detail);
329    }
330
331    /** The members of an input object: required ones present, no unknown ones ({@code evidence} and {@code note} allowed). */
332    private static Map<?, ?> members(Object value, List<String> required, List<String> optional, String what) {
333        if (!(value instanceof Map<?, ?>)) {
334            throw invalid(what + " must be an object");
335        }
336        Map<?, ?> obj = (Map<?, ?>) value;
337        for (String name : required) {
338            if (!obj.containsKey(name)) {
339                throw invalid(what + " lacks member " + name);
340            }
341        }
342        for (Object name : obj.keySet()) {
343            if (!required.contains(name) && !optional.contains(name) && !"evidence".equals(name) && !"note".equals(name)) {
344                throw invalid(what + " has unknown member " + name);
345            }
346        }
347        return obj;
348    }
349
350    private static String text(Map<?, ?> obj, String name) {
351        return text(obj, name, false, false);
352    }
353
354    private static String text(Map<?, ?> obj, String name, boolean allowEmpty, boolean nullable) {
355        Object value = obj.get(name);
356        if (value == null && nullable) {
357            return null;
358        }
359        if (!(value instanceof String)) {
360            throw invalid(name + " must be a string");
361        }
362        Framing.utf8((String) value);
363        if (((String) value).isEmpty() && !allowEmpty) {
364            throw new SqllrException("ID_EMPTY_NAME", name + " must not be empty");
365        }
366        return (String) value;
367    }
368
369    /** An integer in 0..2^53-1, or null when {@code value} is not one. */
370    private static Long smallInteger(Object value) {
371        if (value instanceof Long || value instanceof Integer || value instanceof Short || value instanceof Byte) {
372            long v = ((Number) value).longValue();
373            return v >= 0 && v <= MAX_ORDINAL ? Long.valueOf(v) : null;
374        }
375        if (value instanceof BigInteger) {
376            BigInteger big = (BigInteger) value;
377            return big.signum() >= 0 && big.compareTo(BigInteger.valueOf(MAX_ORDINAL)) <= 0 ? Long.valueOf(big.longValue()) : null;
378        }
379        return null;
380    }
381
382    private static Long ordinal(Map<?, ?> obj, String name) {
383        Long value = smallInteger(obj.get(name));
384        if (value == null) {
385            throw invalid(name + " must be an integer in 0..2^53-1");
386        }
387        return value;
388    }
389
390    private static Code code(Map<?, ?> obj, String name, List<String> allowed) {
391        Code value = Code.of(obj.get(name));
392        if (allowed != null && !allowed.contains(value.value())) {
393            throw new SqllrException("ID_INVALID_CODE", name + ": " + value.value() + " is not one of " + allowed);
394        }
395        return value;
396    }
397
398    /** An id-valued input, framed as the string of its full text (Section 3.6). */
399    private static String ref(Object value, String prefix, String what) {
400        if (isId(value, prefix)) {
401            return (String) value;
402        }
403        throw new SqllrException("ID_INVALID_REFERENCE", what + " must be a " + prefix + "- id");
404    }
405
406    /** A reference given as {@code {"status": <non-portable status>}} instead of an id, or null. */
407    private static String statusOf(Object value) {
408        if (value instanceof Map<?, ?>) {
409            Map<?, ?> obj = members(value, list("status"), NONE, "non-portable reference");
410            Object status = obj.get("status");
411            if (!(status instanceof String) || !NON_PORTABLE.contains(status)) {
412                throw invalid("unknown identity status " + status);
413            }
414            return (String) status;
415        }
416        return null;
417    }
418
419    private static String worst(List<String> statuses) {
420        String out = null;
421        for (String status : statuses) {
422            if (out == null || STATUS_RANK.get(status) > STATUS_RANK.get(out)) {
423                out = status;
424            }
425        }
426        return out == null ? PORTABLE : out;
427    }
428
429    private static List<Object> pathScope(Object value) {
430        Map<?, ?> obj = members(value, list("profile", "producer"), NONE, "pathScope");
431        String profile = text(obj, "profile");
432        String producer = text(obj, "producer", false, true);
433        if (PATH_PROFILE_V1.equals(profile)) {
434            if (producer != null) {
435                throw new SqllrException("ID_INVALID_PATH_SCOPE", "producer must be null for " + PATH_PROFILE_V1);
436            }
437        } else if (producer == null) {
438            throw new SqllrException("ID_INVALID_PATH_SCOPE", "a producer path profile needs the producer name");
439        }
440        return Arrays.<Object>asList(profile, producer);
441    }
442
443    private static String semanticPath(List<Object> scope, Object value) {
444        if (!(value instanceof String) || ((String) value).isEmpty()) {
445            throw new SqllrException("ID_INVALID_PATH", "path must be a non-empty string");
446        }
447        String path = (String) value;
448        Framing.utf8(path);
449        if (PATH_PROFILE_V1.equals(scope.get(0))) {
450            String[] steps = path.split("/", -1);
451            for (String step : steps) {
452                if (!PATH_STEP.matcher(step).matches()) {
453                    throw new SqllrException("ID_INVALID_PATH", path + " is not a " + PATH_PROFILE_V1 + " path");
454                }
455            }
456            for (String step : steps) {
457                if (!PATH_STEP_CODES.contains(step.substring(0, step.indexOf('[')))) {
458                    throw new SqllrException("ID_INVALID_PATH", step + " is not a step code of " + PATH_PROFILE_V1);
459                }
460            }
461        }
462        return path;
463    }
464
465    private static List<Object> contentDigest(Object value) {
466        Map<?, ?> obj = members(value, list("domain", "value"), list("tokenDomain"), "content digest");
467        Code domain = code(obj, "domain", DIGEST_DOMAINS);
468        if ("KEYED_TOKEN".equals(domain.value())) {
469            return Arrays.<Object>asList(domain, text(obj, "tokenDomain"), text(obj, "value"));
470        }
471        if (obj.get("tokenDomain") != null) {
472            throw new SqllrException("ID_INVALID_DIGEST", "tokenDomain is allowed only for KEYED_TOKEN");
473        }
474        Object digest = obj.get("value");
475        if (!(digest instanceof String) || !HEX64.matcher((String) digest).matches()) {
476            throw new SqllrException("ID_INVALID_DIGEST", "a SHA-256 digest is 64 lowercase hex characters");
477        }
478        return Arrays.<Object>asList(domain, null, digest);
479    }
480
481    private static String hex64(Map<?, ?> obj, String name, boolean nullable) {
482        Object value = obj.get(name);
483        if (value == null && nullable) {
484            return null;
485        }
486        if (!(value instanceof String) || !HEX64.matcher((String) value).matches()) {
487            throw new SqllrException("ID_INVALID_DIGEST", name + " must be 64 lowercase hex characters");
488        }
489        return (String) value;
490    }
491
492    /** Framed name segments and the statuses that prevent an id (Section 3.9). */
493    private static final class Segments {
494        final List<Object> framed = new ArrayList<Object>();
495        final List<String> blocked = new ArrayList<String>();
496    }
497
498    private static Segments nameSegments(Object value) {
499        if (!(value instanceof List<?>) || ((List<?>) value).size() < 2) {
500            throw invalid("a full name has an AUTHORITY segment and an object segment");
501        }
502        List<?> items = (List<?>) value;
503        Segments out = new Segments();
504        for (int position = 0; position < items.size(); position++) {
505            Object item = items.get(position);
506            Object kind = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("kind") : null;
507            if (!"NAMED".equals(kind) && !"PRODUCER_DEFAULT".equals(kind)) {
508                throw invalid("segment.kind must be NAMED or PRODUCER_DEFAULT");
509            }
510            Map<?, ?> seg = (Map<?, ?>) item;
511            Code role = code(seg, "role", null);
512            if ("AUTHORITY".equals(role.value()) != (position == 0)) {
513                throw invalid("AUTHORITY is the first segment and only the first");
514            }
515            if ("PRODUCER_DEFAULT".equals(kind)) {
516                if (seg.containsKey("canonicalKey") || seg.containsKey("keyUnavailable")) {
517                    throw new SqllrException("ID_INVALID_DEFAULT_SEGMENT", "a PRODUCER_DEFAULT segment has no key");
518                }
519                if (position == items.size() - 1) {
520                    throw invalid("the object segment is never defaulted");
521                }
522                members(seg, list("role", "kind"), NONE, "segment");
523                out.framed.add(Arrays.<Object>asList(role, Code.of("PRODUCER_DEFAULT"), null));
524                continue;
525            }
526            if (seg.containsKey("keyUnavailable")) {
527                members(seg, list("role", "kind", "keyUnavailable"), NONE, "segment");
528                Object reason = seg.get("keyUnavailable");
529                if (!KEY_UNAVAILABLE.containsKey(reason) || position == 0) {
530                    throw invalid("keyUnavailable must be COLLATION or MIXED_PROFILE");
531                }
532                out.blocked.add(KEY_UNAVAILABLE.get(reason));
533                continue;
534            }
535            members(seg, list("role", "kind", "canonicalKey"), NONE, "segment");
536            // For AUTHORITY the key is the caller-declared name, byte for byte (Section 3.9).
537            out.framed.add(Arrays.<Object>asList(role, Code.of("NAMED"), text(seg, "canonicalKey")));
538        }
539        return out;
540    }
541
542    /** A field path (outermost first, starting with {@code FIELD}) and the statuses that prevent an id. */
543    private static Segments fieldPath(Object value) {
544        if (!(value instanceof List<?>) || ((List<?>) value).isEmpty()) {
545            throw new SqllrException("ID_INVALID_FIELD_PATH", "a field path has at least one segment");
546        }
547        List<?> items = (List<?>) value;
548        Segments out = new Segments();
549        for (int position = 0; position < items.size(); position++) {
550            Object item = items.get(position);
551            Object kindValue = item instanceof Map<?, ?> ? ((Map<?, ?>) item).get("kind") : null;
552            if (!(kindValue instanceof String) || !PATH_SEGMENT_KINDS.contains(kindValue)) {
553                throw new SqllrException("ID_INVALID_FIELD_PATH", "unknown path segment kind");
554            }
555            Map<?, ?> seg = (Map<?, ?>) item;
556            Code kind = Code.of(kindValue);
557            if (position == 0 && !"FIELD".equals(kind.value())) {
558                throw new SqllrException("ID_INVALID_FIELD_PATH", "the first path segment must be FIELD");
559            }
560            if ("FIELD".equals(kind.value())) {
561                if (seg.containsKey("keyUnavailable")) {
562                    members(seg, list("kind", "keyUnavailable"), NONE, "path segment");
563                    Object reason = seg.get("keyUnavailable");
564                    if (!KEY_UNAVAILABLE.containsKey(reason)) {
565                        throw invalid("keyUnavailable must be COLLATION or MIXED_PROFILE");
566                    }
567                    out.blocked.add(KEY_UNAVAILABLE.get(reason));
568                    continue;
569                }
570                members(seg, list("kind", "canonicalKey"), NONE, "path segment");
571                // A column key is never empty; a nested key of semi-structured data may be.
572                out.framed.add(Arrays.<Object>asList(kind, text(seg, "canonicalKey", position > 0, false)));
573            } else if ("INDEX".equals(kind.value())) {
574                members(seg, list("kind", "index"), NONE, "path segment");
575                Long index = smallInteger(seg.get("index"));
576                if (index == null) {
577                    throw new SqllrException("ID_INVALID_FIELD_PATH", "INDEX is a 0-based integer");
578                }
579                out.framed.add(Arrays.<Object>asList(kind, index));
580            } else {
581                members(seg, list("kind"), NONE, "path segment");
582                out.framed.add(Arrays.<Object>asList(kind, null));
583            }
584        }
585        return out;
586    }
587
588    private static List<Object> routineSignature(Object value) {
589        Map<?, ?> obj = members(value, list("form"), list("parameters", "overloadId"), "signature");
590        Code form = code(obj, "form", SIGNATURE_FORMS);
591        if ("NOT_OVERLOADABLE".equals(form.value())) {
592            members(obj, list("form"), NONE, "signature");
593            return Arrays.<Object>asList(form, null);
594        }
595        if ("CATALOG_OVERLOAD_ID".equals(form.value())) {
596            members(obj, list("form", "overloadId"), NONE, "signature");
597            return Arrays.<Object>asList(form, text(obj, "overloadId"));
598        }
599        members(obj, list("form", "parameters"), NONE, "signature");
600        Object params = obj.get("parameters");
601        if (!(params instanceof List<?>)) {
602            throw new SqllrException("ID_INVALID_SIGNATURE", "parameters must be a list (possibly empty)");
603        }
604        List<Object> framed = new ArrayList<Object>();
605        for (Object param : (List<?>) params) {
606            Map<?, ?> p = members(param, list("mode", "canonicalTypeKey"), NONE, "parameter");
607            framed.add(Arrays.<Object>asList(code(p, "mode", PARAMETER_MODES), text(p, "canonicalTypeKey")));
608        }
609        return Arrays.<Object>asList(form, framed);
610    }
611
612    // ---------------------------------------------------------------------------------------------
613    // The algorithms (Section 5)
614    // ---------------------------------------------------------------------------------------------
615
616    private static Outcome sourceUnitId(Object value) {
617        Object kind = value instanceof Map<?, ?> ? ((Map<?, ?>) value).get("kind") : null;
618        if ("LOCAL".equals(kind)) {
619            members(value, list("kind"), NONE, "SourceUnitIdV1 input");
620            return noId(LOCAL);
621        }
622        if ("NAMED".equals(kind)) {
623            Map<?, ?> in = members(value, list("kind", "namespace", "logicalName"), NONE, "SourceUnitIdV1 input");
624            return portable(Code.of("NAMED"), text(in, "namespace"), text(in, "logicalName"));
625        }
626        if ("CONTENT_ADDRESSED".equals(kind)) {
627            Map<?, ?> in = members(value, list("kind", "contextDomain", "contentDigest"), NONE, "SourceUnitIdV1 input");
628            Code code = Code.of("CONTENT_ADDRESSED");
629            String domain = text(in, "contextDomain");
630            return portable(code, domain, contentDigest(in.get("contentDigest")));
631        }
632        if ("DERIVED".equals(kind)) {
633            Map<?, ?> in = members(value, list("kind", "parentUnit", "statementOrdinal", "pathScope", "sitePath",
634                    "siteOrdinal", "variantOrdinal"), NONE, "SourceUnitIdV1 input");
635            String parentStatus = statusOf(in.get("parentUnit"));
636            if (parentStatus != null) {
637                return noId(worst(Arrays.asList(LOCAL, parentStatus)));
638            }
639            List<Object> scope = pathScope(in.get("pathScope"));
640            String parent = ref(in.get("parentUnit"), "su1", "parentUnit");
641            Long statement = ordinal(in, "statementOrdinal");
642            String sitePath = semanticPath(scope, in.get("sitePath"));
643            Long site = ordinal(in, "siteOrdinal");
644            Long variant = ordinal(in, "variantOrdinal");
645            return portable(Code.of("DERIVED"), parent, statement, scope, sitePath, site, variant);
646        }
647        throw invalid("SourceUnitIdV1 kind must be NAMED, CONTENT_ADDRESSED, DERIVED or LOCAL");
648    }
649
650    private static Outcome endpointId(Object value) {
651        if (!(value instanceof Map<?, ?>)) {
652            throw invalid("EndpointIdV1 input must be an object");
653        }
654        Map<?, ?> in = (Map<?, ?>) value;
655        Object endpointKind = in.get("endpointKind");
656        if (!(endpointKind instanceof String) || !IDENTITY_CLASS.containsKey(endpointKind)) {
657            throw new SqllrException("ID_INVALID_CODE", "unknown endpointKind " + endpointKind);
658        }
659        if (in.containsKey("opaque")) {
660            members(in, list("endpointKind", "opaque"), NONE, "EndpointIdV1 input");
661            members(in.get("opaque"), list("issuer", "algorithmId", "policyId", "comparisonDomain", "value"), NONE,
662                    "opaque identity");
663            return noId(OPAQUE_PRODUCER_LOCAL);
664        }
665        String kind = IDENTITY_CLASS.get(endpointKind);
666        if (kind == null) {
667            members(in, list("endpointKind"), NONE, "EndpointIdV1 input");
668            return noId(LOCAL);
669        }
670        if (in.containsKey("qualificationSatisfied") && !Boolean.TRUE.equals(in.get("qualificationSatisfied"))) {
671            return noId(LOCAL);
672        }
673        if ("RELATION".equals(kind)) {
674            members(in, list("endpointKind", "segments"), list("qualificationSatisfied"), "EndpointIdV1 RELATION input");
675            Segments segments = nameSegments(in.get("segments"));
676            if (!segments.blocked.isEmpty()) {
677                return noId(worst(segments.blocked));
678            }
679            return portable(Code.of("RELATION"), segments.framed);
680        }
681        if ("FIELD".equals(kind) || "ROW_SET".equals(kind)) {
682            List<String> required = "FIELD".equals(kind) ? list("endpointKind", "owner", "path") : list("endpointKind", "owner");
683            members(in, required, list("qualificationSatisfied"), "EndpointIdV1 " + kind + " input");
684            List<String> blocked = new ArrayList<String>();
685            String ownerStatus = statusOf(in.get("owner"));
686            if (ownerStatus != null) {
687                blocked.add(ownerStatus);
688            }
689            if ("ROW_SET".equals(kind)) {
690                if (!blocked.isEmpty()) {
691                    return noId(worst(blocked));
692                }
693                return portable(Code.of("ROW_SET"), ref(in.get("owner"), "ep1", "owner"));
694            }
695            Segments path = fieldPath(in.get("path"));
696            blocked.addAll(path.blocked);
697            if (!blocked.isEmpty()) {
698                return noId(worst(blocked));
699            }
700            return portable(Code.of("FIELD"), ref(in.get("owner"), "ep1", "owner"), path.framed);
701        }
702        members(in, list("endpointKind", "routineNamespace", "routineKind", "segments"),
703                list("signature", "qualificationSatisfied"), "EndpointIdV1 ROUTINE input");
704        Code namespace = code(in, "routineNamespace", ROUTINE_NAMESPACES);
705        Code routineKind = code(in, "routineKind", ROUTINE_KINDS);
706        Segments segments = nameSegments(in.get("segments"));
707        if (in.get("signature") == null) {
708            segments.blocked.add(LOCAL); // the overload is not proven (P1-3)
709        }
710        if (!segments.blocked.isEmpty()) {
711            return noId(worst(segments.blocked));
712        }
713        return portable(Code.of("ROUTINE"), namespace, routineKind, segments.framed, routineSignature(in.get("signature")));
714    }
715
716    private static Outcome factId(Object value) {
717        Map<?, ?> in = members(value, list("kind", "source", "target"), list("role"), "FactIdV1 input");
718        Code kind = code(in, "kind", FACT_KINDS);
719        Object role = in.get("role");
720        if ("ROW".equals(kind.value())) {
721            if (role == null) {
722                throw new SqllrException("ID_INVALID_ROLE", "a ROW Fact carries a role");
723            }
724            role = code(in, "role", ROW_ROLES);
725        } else if (role != null) {
726            throw new SqllrException("ID_INVALID_ROLE", "only a ROW Fact carries a role");
727        }
728        String sourceStatus = statusOf(in.get("source"));
729        String targetStatus = statusOf(in.get("target"));
730        if (sourceStatus != null || targetStatus != null) {
731            return noId(LOCAL);
732        }
733        String source = ref(in.get("source"), "ep1", "source");
734        String target = ref(in.get("target"), "ep1", "target");
735        return portable(kind, source, target, role);
736    }
737
738    /** A side of a FactCandidate subject, or null when a member has no {@code EndpointIdV1}. */
739    private static List<Object> candidateSide(Object value) {
740        Map<?, ?> obj = members(value, NONE, list("endpoint", "candidates", "membership"), "candidate side");
741        if (obj.containsKey("endpoint") == obj.containsKey("candidates")) {
742            throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a side has either endpoint or candidates");
743        }
744        if (obj.containsKey("endpoint")) {
745            if (obj.containsKey("membership")) {
746                throw new SqllrException("ID_INVALID_CANDIDATE_SET", "membership belongs to a candidate set");
747            }
748            if (statusOf(obj.get("endpoint")) != null) {
749                return null;
750            }
751            return Arrays.<Object>asList(Code.of("ENDPOINT"), ref(obj.get("endpoint"), "ep1", "endpoint"));
752        }
753        if (!obj.containsKey("membership")) {
754            throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate set declares its membership");
755        }
756        Code membership = code(obj, "membership", CANDIDATE_SET_MEMBERSHIP);
757        Object items = obj.get("candidates");
758        if (!(items instanceof List<?>)) {
759            throw new SqllrException("ID_INVALID_CANDIDATE_SET", "candidates must be a list");
760        }
761        List<?> candidates = (List<?>) items;
762        if ("COMPLETE".equals(membership.value()) && candidates.size() < 2) {
763            throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a COMPLETE candidate set has at least two members");
764        }
765        for (Object item : candidates) {
766            if (statusOf(item) != null) {
767                return null;
768            }
769        }
770        List<String> ids = new ArrayList<String>();
771        for (Object item : candidates) {
772            ids.add(ref(item, "ep1", "candidate"));
773        }
774        if (new HashSet<String>(ids).size() != ids.size()) {
775            throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate set has no duplicates");
776        }
777        Collections.sort(ids, UTF8_ORDER);
778        return Arrays.<Object>asList(Code.of("CANDIDATE_SET"), membership, new ArrayList<Object>(ids));
779    }
780
781    /** Unsigned UTF-8 byte order of strings, which is code point order. */
782    static final Comparator<String> UTF8_ORDER = new Comparator<String>() {
783        @Override
784        public int compare(String a, String b) {
785            return CanonicalJson.compareUnsigned(Framing.utf8(a), Framing.utf8(b));
786        }
787    };
788
789    private static List<Object> occurrenceSubject(Object value) {
790        Object kind = value instanceof Map<?, ?> ? ((Map<?, ?>) value).get("kind") : null;
791        if ("FACT".equals(kind)) {
792            Map<?, ?> obj = members(value, list("kind", "fact"), NONE, "subject");
793            if (statusOf(obj.get("fact")) != null) {
794                return null;
795            }
796            return Arrays.<Object>asList(Code.of("FACT"), ref(obj.get("fact"), "fact1", "fact"));
797        }
798        if ("CANDIDATE".equals(kind)) {
799            Map<?, ?> obj = members(value, list("kind", "factKind", "source", "target"), list("role"), "subject");
800            Code factKind = code(obj, "factKind", FACT_KINDS);
801            Object role = obj.get("role");
802            if ("ROW".equals(factKind.value())) {
803                if (role == null) {
804                    throw new SqllrException("ID_INVALID_ROLE", "a ROW candidate carries a role");
805                }
806                role = code(obj, "role", ROW_ROLES);
807            } else if (role != null) {
808                throw new SqllrException("ID_INVALID_ROLE", "only a ROW candidate carries a role");
809            }
810            if (!(obj.get("source") instanceof Map<?, ?>) || !(obj.get("target") instanceof Map<?, ?>)) {
811                throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate side must be an object");
812            }
813            if (!((Map<?, ?>) obj.get("source")).containsKey("candidates")
814                    && !((Map<?, ?>) obj.get("target")).containsKey("candidates")) {
815                throw new SqllrException("ID_INVALID_CANDIDATE_SET", "a candidate subject has at least one candidate set");
816            }
817            List<Object> source = candidateSide(obj.get("source"));
818            List<Object> target = candidateSide(obj.get("target"));
819            if (source == null || target == null) {
820                return null;
821            }
822            return Arrays.<Object>asList(Code.of("CANDIDATE"), factKind, role, source, target);
823        }
824        throw invalid("subject.kind must be FACT or CANDIDATE");
825    }
826
827    private static Outcome occurrenceKey(Object value) {
828        Map<?, ?> in = members(value, list("sourceUnit", "statementOrdinal", "subject", "pathScope", "path", "ordinal"),
829                NONE, "OccurrenceKeyV1 input");
830        List<Object> scope = pathScope(in.get("pathScope"));
831        String path = semanticPath(scope, in.get("path"));
832        Long statement = ordinal(in, "statementOrdinal");
833        Long occurrence = ordinal(in, "ordinal");
834        List<Object> subject = occurrenceSubject(in.get("subject"));
835        if (statusOf(in.get("sourceUnit")) != null || subject == null) {
836            return noId(LOCAL);
837        }
838        return portable(ref(in.get("sourceUnit"), "su1", "sourceUnit"), statement, subject, scope, path, occurrence);
839    }
840
841    private static Outcome metadataDigest(Object value) {
842        Map<?, ?> in = members(value, list("artifacts"), NONE, "MetadataDigestV1 input");
843        Object artifacts = in.get("artifacts");
844        if (!(artifacts instanceof List<?>)) {
845            throw invalid("artifacts must be a list");
846        }
847        List<List<Object>> framed = new ArrayList<List<Object>>();
848        for (Object artifact : (List<?>) artifacts) {
849            Map<?, ?> a = members(artifact, list("format", "sha256"), NONE, "metadata artifact");
850            framed.add(Arrays.<Object>asList(text(a, "format"), hex64(a, "sha256", false)));
851        }
852        Collections.sort(framed, new Comparator<List<Object>>() {
853            @Override
854            public int compare(List<Object> x, List<Object> y) {
855                int c = UTF8_ORDER.compare((String) x.get(0), (String) y.get(0));
856                return c != 0 ? c : UTF8_ORDER.compare((String) x.get(1), (String) y.get(1));
857            }
858        });
859        for (int i = 1; i < framed.size(); i++) {
860            if (framed.get(i).equals(framed.get(i - 1))) {
861                throw invalid("a metadata artifact is listed twice");
862            }
863        }
864        return portable(new ArrayList<Object>(framed));
865    }
866
867    private static List<Object> capabilityList(Object value) {
868        if (!(value instanceof Map<?, ?>) || ((Map<?, ?>) value).isEmpty()) {
869            throw invalid("capabilities must be a non-empty object");
870        }
871        Map<?, ?> obj = (Map<?, ?>) value;
872        List<String> features = new ArrayList<String>();
873        for (Object feature : obj.keySet()) {
874            Framing.utf8((String) feature);
875            features.add((String) feature);
876        }
877        Collections.sort(features, UTF8_ORDER);
878        List<Object> out = new ArrayList<Object>();
879        for (String feature : features) {
880            Code name = Code.of(feature);
881            if (!ANALYSIS_FEATURES.contains(name.value())) {
882                throw new SqllrException("ID_INVALID_CODE", "f: " + feature + " is not one of " + ANALYSIS_FEATURES);
883            }
884            out.add(Arrays.<Object>asList(name, code(obj, feature, CAPABILITY_STATUSES)));
885        }
886        return out;
887    }
888
889    private static List<Object> decodingList(Object value) {
890        if (value == null) {
891            return null;
892        }
893        Map<?, ?> obj = members(value, list("encoding", "bom", "lineEndings"), NONE, "decoding");
894        String encoding = text(obj, "encoding");
895        Code bom = code(obj, "bom", BOM_HANDLING);
896        return Arrays.<Object>asList(encoding, bom, code(obj, "lineEndings", LINE_ENDING_HANDLING));
897    }
898
899    private static Outcome analysisContext(Object value) {
900        Map<?, ?> in = members(value, list("producerName", "producerVersion", "dependencySnapshot", "dialectId",
901                "dialectVersion", "identifierPolicyId", "pathProfile", "effectiveOptionsDigest", "metadataStatus",
902                "metadataDigest", "redactionPolicyId", "redactionPolicyVersion", "capabilities", "decoding",
903                "sourceContentDigest", "resolutionInputDigests"), list("effectiveOptions", "metadataArtifacts"),
904                "AnalysisContextV1 input");
905        String optionsDigest = hex64(in, "effectiveOptionsDigest", false);
906        if (in.containsKey("effectiveOptions")) {
907            String computed = Framing.hex(sha256(CanonicalJson.jcsBytes(in.get("effectiveOptions"))));
908            if (!computed.equals(optionsDigest)) {
909                throw new SqllrException("ID_MISMATCH", "effectiveOptionsDigest does not match effectiveOptions");
910            }
911        }
912        Code status = code(in, "metadataStatus", METADATA_STATUSES);
913        String meta = hex64(in, "metadataDigest", true);
914        if ((meta == null) != "NONE".equals(status.value())) {
915            throw invalid("metadataDigest is null if and only if metadataStatus is NONE");
916        }
917        if (in.containsKey("metadataArtifacts")) {
918            Map<String, Object> artifacts = new LinkedHashMap<String, Object>();
919            artifacts.put("artifacts", in.get("metadataArtifacts"));
920            if (!compute(METADATA_DIGEST, artifacts).getId().equals(meta)) {
921                throw new SqllrException("ID_MISMATCH", "metadataDigest does not match metadataArtifacts");
922            }
923        }
924        Object inputs = in.get("resolutionInputDigests");
925        if (!(inputs instanceof List<?>)) {
926            throw invalid("resolutionInputDigests must be a list");
927        }
928        List<List<Object>> digests = new ArrayList<List<Object>>();
929        for (Object digest : (List<?>) inputs) {
930            digests.add(contentDigest(digest));
931        }
932        final Map<List<Object>, byte[]> framedDigests = new HashMap<List<Object>, byte[]>();
933        for (List<Object> digest : digests) {
934            framedDigests.put(digest, Framing.frame(digest));
935        }
936        Collections.sort(digests, new Comparator<List<Object>>() {
937            @Override
938            public int compare(List<Object> x, List<Object> y) {
939                return CanonicalJson.compareUnsigned(framedDigests.get(x), framedDigests.get(y));
940            }
941        });
942        for (int i = 1; i < digests.size(); i++) {
943            if (Arrays.equals(framedDigests.get(digests.get(i)), framedDigests.get(digests.get(i - 1)))) {
944                throw invalid("a resolution input digest is listed twice");
945            }
946        }
947        String producerName = text(in, "producerName");
948        String producerVersion = text(in, "producerVersion");
949        String snapshot = ref(in.get("dependencySnapshot"), "snap1", "dependencySnapshot");
950        String dialectId = text(in, "dialectId");
951        String dialectVersion = text(in, "dialectVersion", false, true);
952        String policyId = text(in, "identifierPolicyId");
953        String pathProfile = text(in, "pathProfile");
954        String redactionPolicyId = text(in, "redactionPolicyId");
955        String redactionPolicyVersion = text(in, "redactionPolicyVersion");
956        List<Object> capabilities = capabilityList(in.get("capabilities"));
957        List<Object> decoding = decodingList(in.get("decoding"));
958        List<Object> source = contentDigest(in.get("sourceContentDigest"));
959        return portable(producerName, producerVersion, snapshot, dialectId, dialectVersion, policyId, pathProfile,
960                optionsDigest, status, meta, redactionPolicyId, redactionPolicyVersion, capabilities, decoding, source,
961                new ArrayList<Object>(digests));
962    }
963
964    private static Outcome explainNodeKey(Object value) {
965        Map<?, ?> in = members(value, list("sourceUnit", "statementOrdinal", "pathScope", "path", "nodeKind", "ordinal"),
966                NONE, "ExplainNodeKeyV1 input");
967        List<Object> scope = pathScope(in.get("pathScope"));
968        String path = semanticPath(scope, in.get("path"));
969        Long statement = ordinal(in, "statementOrdinal");
970        Long occurrence = ordinal(in, "ordinal");
971        Code nodeKind = code(in, "nodeKind", EXPLAIN_NODE_KINDS);
972        if (statusOf(in.get("sourceUnit")) != null) {
973            return noId(LOCAL);
974        }
975        return portable(ref(in.get("sourceUnit"), "su1", "sourceUnit"), statement, scope, path, nodeKind, occurrence);
976    }
977
978    /** The algorithms in the order of Section 2. */
979    public static final List<String> ALGORITHMS = list(SOURCE_UNIT, ENDPOINT, FACT, OCCURRENCE_KEY, ANALYSIS_CONTEXT,
980            OCCURRENCE_RECORD, EXPLAIN_NODE_KEY, EXPLAIN_NODE_RECORD, DEPENDENCY_SNAPSHOT, METADATA_DIGEST);
981}