001package gudusoft.gsqlparser.lineage.v2.merge;
002
003import gudusoft.gsqlparser.lineage.v2.json.ArrayRegistry;
004import gudusoft.gsqlparser.lineage.v2.json.CanonicalJson;
005import gudusoft.gsqlparser.lineage.v2.model.Document;
006import gudusoft.gsqlparser.lineage.v2.model.Producer;
007import gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex;
008import gudusoft.gsqlparser.lineage.v2.validation.LineageValidator;
009import gudusoft.gsqlparser.lineage.v2.validation.SchemaValidator;
010import gudusoft.gsqlparser.lineage.v2.validation.SourceTextProvider;
011
012import java.security.MessageDigest;
013import java.security.NoSuchAlgorithmException;
014import java.util.ArrayDeque;
015import java.util.ArrayList;
016import java.util.Arrays;
017import java.util.Collections;
018import java.util.Deque;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Iterator;
022import java.util.LinkedHashMap;
023import java.util.LinkedHashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Set;
027import java.util.TreeMap;
028import java.util.TreeSet;
029
030import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.intRef;
031import static gudusoft.gsqlparser.lineage.v2.validation.DocumentIndex.items;
032
033/**
034 * The minimal L1 merger of milestone M1 (specification Sections 9 and 15.2; contract: section 7 of
035 * {@code docs/designs/data-lineage/05-sqllr-m1-core-library-plan-zh.md}).
036 *
037 * <p>Each input must be a valid document, and so is the result: a merge that would produce an invalid document,
038 * or that cannot keep what an input asserts, is refused with a {@link MergeException}; nothing is dropped
039 * silently.</p>
040 *
041 * <p>Source units merge by (unit id, analysis context id). The content of such a unit (statements, scopes,
042 * Occurrences, writes, diagnostics, ExplainGraph content, module objects) is carried from one input whose content
043 * includes every other input's, compared with references replaced by identities; a {@code LOCAL} unit is carried
044 * once per input. Endpoints merge by portable identity, Facts by {@code FactIdV1}, Occurrences by record id,
045 * analysis contexts and objects without an identity that no unit owns by their content. References are renumbered
046 * in the order of the inputs.</p>
047 *
048 * <p>The merge module of the specification is not defined yet (Section 20, item 16), so the input digests and the
049 * ledger are returned in the {@link Result}, not written into the document. Because the document cannot yet carry
050 * two endpoints of one identity, a kind difference other than a generic {@code RELATION} refined by a specific
051 * relation kind is refused (Section 9.4 asks to keep both).</p>
052 *
053 * @since 4.2.17
054 */
055public final class LineageMerger {
056
057    /** The inputs cannot be merged by this merger; nothing was merged. */
058    public static class MergeException extends RuntimeException {
059        private static final long serialVersionUID = 1L;
060
061        public MergeException(String message) {
062            super(message);
063        }
064    }
065
066    /** One entry of the ledger: an identity whose generic kind an input refined. */
067    public static final class Conflict {
068        private final String identity;
069        private final String detail;
070
071        Conflict(String identity, String detail) {
072            this.identity = identity;
073            this.detail = detail;
074        }
075
076        /** The identity (an {@code ep1} id). */
077        public String getIdentity() {
078            return identity;
079        }
080
081        public String getDetail() {
082            return detail;
083        }
084
085        @Override
086        public String toString() {
087            return identity + ": " + detail;
088        }
089    }
090
091    /** The merged document and what the merge module would carry. */
092    public static final class Result {
093        private final Document document;
094        private final List<String> inputDigests;
095        private final List<Conflict> conflicts;
096        private final List<String> notes;
097
098        Result(Document document, List<String> inputDigests, List<Conflict> conflicts, List<String> notes) {
099            this.document = document;
100            this.inputDigests = Collections.unmodifiableList(inputDigests);
101            this.conflicts = Collections.unmodifiableList(conflicts);
102            this.notes = Collections.unmodifiableList(notes);
103        }
104
105        public Document getDocument() {
106            return document;
107        }
108
109        /** The sha256 of each distinct input's canonical bytes, in the order the inputs were first given. */
110        public List<String> getInputDigests() {
111            return inputDigests;
112        }
113
114        /** The ledger: identities whose generic kind an input refined. */
115        public List<Conflict> getConflicts() {
116            return conflicts;
117        }
118
119        public List<String> getNotes() {
120            return notes;
121        }
122    }
123
124    private static final List<String> LEVELS = Arrays.asList("L1", "L2", "L3");
125    /** The specific endpoint kinds of identity class RELATION (identity-v1, Section 5.2.1). */
126    private static final Set<String> RELATION_KINDS = new HashSet<String>(Arrays.asList("TABLE", "VIEW",
127            "MATERIALIZED_VIEW", "EXTERNAL_TABLE", "SYNONYM"));
128    /** The root members the merger carries, besides the module arrays. */
129    private static final Set<String> ROOT = new HashSet<String>(Arrays.asList("specUri", "specVersion",
130            "declaredLevel", "modules", "header", "analysisContexts", "sourceUnits", "statements", "scopes", "endpoints",
131            "facts", "factCandidates", "candidateSets", "occurrences", "targetFieldOccurrences", "analysisCompleteness",
132            "featureCompleteness", "servingCompleteness", "diagnostics", "explainGraph"));
133    private static final Set<String> GRAPH = new HashSet<String>(Arrays.asList("completeness", "statementCompleteness",
134            "nodes", "microEdges", "candidateSets", "supportPaths"));
135    /** The collections of the output, by kind of object. */
136    private static final String[][] COLLECTIONS = {
137            {"analysisContext", "analysisContexts"}, {"sourceUnit", "sourceUnits"}, {"statement", "statements"},
138            {"scope", "scopes"}, {"endpoint", "endpoints"}, {"fact", "facts"}, {"factCandidate", "factCandidates"},
139            {"rootCandidateSet", "candidateSets"}, {"occurrence", "occurrences"}, {"tfo", "targetFieldOccurrences"}, {"diagnostic", "diagnostics"},
140            {"node", "nodes"}, {"microEdge", "microEdges"}, {"candidateSet", "candidateSets"},
141            {"supportPath", "supportPaths"}};
142    private static final Set<String> GRAPH_KINDS = new HashSet<String>(Arrays.asList("node", "microEdge",
143            "candidateSet", "supportPath"));
144    /** Members that modules add to objects of the core (registry group modules); a carrier may add them. */
145    private static final Set<String> MODULE_MEMBERS_OF_OBJECTS = new HashSet<String>(Semantics.MODULE_ADDED);
146    /** The kinds whose objects belong to the unit of their statement or source unit. */
147    private static final Set<String> UNIT_CONTENT = new HashSet<String>(Arrays.asList("statement", "scope",
148            "occurrence", "tfo", "diagnostic", "node", "microEdge", "endpoint", "rootCandidateSet", "supportPath",
149            "candidateSet"));
150
151    private final Producer producer;
152    private final String createdAt;
153    private final LineageValidator validator;
154
155    /**
156     * @param producer  the producer of the merged document: the merger; must be a valid {@code Producer}
157     * @param createdAt the {@code header.createdAt} of the merged document, or null for none; never read from a clock
158     */
159    public LineageMerger(Producer producer, String createdAt) {
160        this(producer, createdAt, LineageValidator.standard(SourceTextProvider.NONE));
161    }
162
163    public LineageMerger(Producer producer, String createdAt, LineageValidator validator) {
164        if (producer == null || !SchemaValidator.lineageDocument().isValid(producer.toJson(), "Producer")) {
165            throw new IllegalArgumentException("the merged document names the merger as its producer, which must be"
166                    + " a valid Producer: " + producer);
167        }
168        this.producer = producer;
169        this.createdAt = createdAt;
170        this.validator = validator;
171    }
172
173    public Result merge(List<Document> inputs) {
174        if (inputs == null || inputs.isEmpty()) {
175            throw new MergeException("nothing to merge");
176        }
177        List<byte[]> distinct = new ArrayList<byte[]>();
178        List<String> digests = new ArrayList<String>();
179        for (Document input : inputs) {
180            byte[] bytes = input.toBytes();
181            String digest = sha256(bytes);
182            if (!digests.contains(digest)) {
183                digests.add(digest);
184                distinct.add(bytes);
185            }
186        }
187        for (int i = 0; i < distinct.size(); i++) {
188            LineageValidator.Result check = validator.validate(distinct.get(i));
189            if (!check.isValid()) {
190                throw new MergeException("input " + i + " is not valid: " + check.getStage() + " " + check.getCodes());
191            }
192        }
193        Result result = new Run(distinct).run(digests);
194        LineageValidator.Result check = validator.validate(result.getDocument().toBytes());
195        if (!check.isValid()) {
196            throw new MergeException("the merged document would not be valid: " + check.getStage() + " "
197                    + check.getFindings());
198        }
199        return result;
200    }
201
202    static String sha256(byte[] data) {
203        try {
204            StringBuilder out = new StringBuilder();
205            for (byte b : MessageDigest.getInstance("SHA-256").digest(data)) {
206                out.append(Character.forDigit((b >> 4) & 0xf, 16)).append(Character.forDigit(b & 0xf, 16));
207            }
208            return out.toString();
209        } catch (NoSuchAlgorithmException e) {
210            throw new IllegalStateException(e);
211        }
212    }
213
214    /** Analysis completeness of a merge (Section 4.4): one status when all agree, otherwise PARTIAL. */
215    static Object combined(Object a, Object b) {
216        return a == null ? b : (a.equals(b) ? a : "PARTIAL");
217    }
218
219    // ---------------------------------------------------------------------------------------------
220
221    private final class Run {
222        private final List<byte[]> bytes;
223        private final List<Map<String, Object>> docs = new ArrayList<Map<String, Object>>();
224        private final List<Semantics> semantics = new ArrayList<Semantics>();
225        private final List<DocumentIndex> indexes = new ArrayList<DocumentIndex>();
226        private final List<Map<Long, Long>> remaps = new ArrayList<Map<Long, Long>>();
227        /** For each input, the ref of every keyed object by key, and of every object by kind and form. */
228        private final List<Map<String, Long>> keyed = new ArrayList<Map<String, Long>>();
229        private final List<Map<String, Long>> byForm = new ArrayList<Map<String, Long>>();
230        /** For each input, the refs of the candidate sets of its ExplainGraph (not those inside FactCandidates). */
231        private final List<Set<Long>> graphCsets = new ArrayList<Set<Long>>();
232        /** For each input, the FactCandidate and end ({@code source}, {@code target}) of each embedded candidate set. */
233        private final List<Map<Long, Object[]>> embeddedIn = new ArrayList<Map<Long, Object[]>>();
234        /** The new ref of the candidate set at each end of each carried FactCandidate ("new FactCandidate ref|end"). */
235        private final Map<String, Long> embeddedSlots = new HashMap<String, Long>();
236        /** The input that carries the content of each (unit id, analysis context id). */
237        private final Map<String, Integer> carrier = new HashMap<String, Integer>();
238        private final Map<String, Long> byKey = new HashMap<String, Long>();
239        private final Map<String, TreeMap<Long, Map<String, Object>>> out = new HashMap<String, TreeMap<Long, Map<String, Object>>>();
240        private final Deque<Object[]> pending = new ArrayDeque<Object[]>();
241        private final Map<String, String> kindOfIdentity = new HashMap<String, String>();
242        private final Map<Long, String> kindOverride = new HashMap<Long, String>();
243        private final List<Conflict> conflicts = new ArrayList<Conflict>();
244        private final List<String> notes = new ArrayList<String>();
245        private final TreeMap<Long, Object[]> statementGraph = new TreeMap<Long, Object[]>();
246        private long nextRef = 1;
247
248        @SuppressWarnings("unchecked")
249        Run(List<byte[]> bytes) {
250            this.bytes = bytes;
251            for (String[] collection : COLLECTIONS) {
252                out.put(collection[0], new TreeMap<Long, Map<String, Object>>());
253            }
254            for (String member : DocumentIndex.MODULE_MEMBERS) {
255                out.put("module:" + member, new TreeMap<Long, Map<String, Object>>());
256            }
257            for (byte[] data : bytes) {
258                docs.add((Map<String, Object>) CanonicalJson.parse(data));
259            }
260        }
261
262        Result run(List<String> digests) {
263            checkCompatible();
264            for (int i = 0; i < bytes.size(); i++) {
265                Semantics forms = new Semantics(bytes.get(i), true, String.valueOf(i));
266                semantics.add(forms);
267                indexes.add(new DocumentIndex(docs.get(i)));
268                remaps.add(new HashMap<Long, Long>());
269                Map<String, Long> keys = new HashMap<String, Long>();
270                Map<String, Long> formed = new HashMap<String, Long>();
271                for (Long ref : allRefs(i)) {
272                    String key = keyOf(i, ref);
273                    if (key != null && !keys.containsKey(key)) {
274                        keys.put(key, ref);
275                    }
276                    String form = formKey(i, ref);
277                    if (!formed.containsKey(form)) {
278                        formed.put(form, ref);
279                    }
280                }
281                keyed.add(keys);
282                byForm.add(formed);
283                Set<Long> csets = new HashSet<Long>();
284                for (Map<?, ?> cset : items(indexes.get(i).graph().get("candidateSets"))) {
285                    csets.add(intRef(cset.get("ref")));
286                }
287                graphCsets.add(csets);
288                Map<Long, Object[]> owners = new HashMap<Long, Object[]>();
289                for (Map<?, ?> candidate : indexes.get(i).all("factCandidate")) {
290                    for (String end : new String[] {"source", "target"}) {
291                        Object side = candidate.get(end);
292                        Object cset = side instanceof Map<?, ?> ? ((Map<?, ?>) side).get("candidateSet") : null;
293                        Long ref = cset instanceof Map<?, ?> ? intRef(((Map<?, ?>) cset).get("ref")) : null;
294                        if (ref != null) {
295                            owners.put(ref, new Object[] {candidate.get("ref"), end});
296                        }
297                    }
298                }
299                embeddedIn.add(owners);
300            }
301            chooseCarriers();
302            for (int i = 0; i < bytes.size(); i++) {
303                mergeInput(i);
304            }
305            return new Result(buildDocument(), new ArrayList<String>(digests), conflicts, notes);
306        }
307
308        private TreeSet<Long> allRefs(int i) {
309            List<Map<?, ?>> objects = new ArrayList<Map<?, ?>>(items(docs.get(i).get("analysisContexts")));
310            for (String[] collection : COLLECTIONS) {
311                objects.addAll(semantics.get(i).index().all(collection[0]));
312            }
313            for (String member : DocumentIndex.MODULE_MEMBERS) {
314                objects.addAll(semantics.get(i).index().all("module:" + member));
315            }
316            TreeSet<Long> refs = new TreeSet<Long>();
317            for (Map<?, ?> obj : objects) {
318                Long ref = intRef(obj.get("ref"));
319                if (ref != null) {
320                    refs.add(ref);
321                }
322            }
323            return refs;
324        }
325
326        private void checkCompatible() {
327            Map<String, Object> first = docs.get(0);
328            for (int i = 0; i < docs.size(); i++) {
329                Map<String, Object> doc = docs.get(i);
330                for (String member : doc.keySet()) {
331                    if (!ROOT.contains(member) && !DocumentIndex.MODULE_MEMBERS.contains(member)) {
332                        throw new MergeException("input " + i + ": member " + member + " is not supported by this merger");
333                    }
334                }
335                Map<?, ?> graph = doc.get("explainGraph") instanceof Map<?, ?> ? (Map<?, ?>) doc.get("explainGraph")
336                        : Collections.emptyMap();
337                for (Object member : graph.keySet()) {
338                    if (!GRAPH.contains(member)) {
339                        throw new MergeException("input " + i + ": explainGraph." + member + " is not supported");
340                    }
341                }
342                for (String member : new String[] {"specUri", "specVersion"}) {
343                    if (!same(first.get(member), doc.get(member))) {
344                        throw new MergeException("input " + i + ": " + member + " differs from input 0");
345                    }
346                }
347                Map<?, ?> header = (Map<?, ?>) doc.get("header");
348                Map<?, ?> header0 = (Map<?, ?>) first.get("header");
349                for (String member : new String[] {"registryVersion", "sourceEmbedding"}) {
350                    if (!same(header0.get(member), header.get(member))) {
351                        throw new MergeException("input " + i + ": header." + member + " differs from input 0");
352                    }
353                }
354                if (header.containsKey("extensions")) {
355                    throw new MergeException("input " + i + ": header extensions are not supported by this merger");
356                }
357            }
358        }
359
360        private boolean same(Object a, Object b) {
361            return CanonicalJson.jcsText(a).equals(CanonicalJson.jcsText(b));
362        }
363
364        // -----------------------------------------------------------------------------------------
365
366        private String kindOf(int i, Object ref) {
367            return semantics.get(i).kindOf(ref);
368        }
369
370        /** The (unit id, analysis context id) key of a unit of input i (read with identifiers filled). */
371        private String unitKey(int i, Object unitRef) {
372            Map<?, ?> unit = semantics.get(i).index().get("sourceUnit", unitRef);
373            if (unit == null) {
374                return null;
375            }
376            Object id = unit.get("unitId");
377            Object context = unit.get("analysisContextId");
378            return id instanceof String && context instanceof String ? "unit|" + id + "|" + context : null;
379        }
380
381        /** The unit ref an object of unit content belongs to, or null when no unit owns it. */
382        private Long owningUnit(int i, Long ref) {
383            Long current = ref;
384            for (int guard = 0; guard < 4096 && current != null; guard++) {
385                String kind = kindOf(i, current);
386                Map<?, ?> obj = semantics.get(i).object(current);
387                if (obj == null) {
388                    return null;
389                }
390                if ("sourceUnit".equals(kind)) {
391                    return current;
392                }
393                if (!UNIT_CONTENT.contains(kind) && !kind.startsWith("module:")) {
394                    return null;
395                }
396                DocumentIndex index = semantics.get(i).index();
397                if ("statement".equals(kind)) {
398                    return intRef(obj.get("sourceUnitRef"));
399                }
400                Map<?, ?> statement = index.get("statement", "rootCandidateSet".equals(kind)
401                        ? semantics.get(i).rootSetStatement(current) : "candidateSet".equals(kind)
402                        ? semantics.get(i).graphSetStatement(current) : obj.get("statementRef"));
403                if (statement != null) {
404                    return intRef(statement.get("sourceUnitRef"));
405                }
406                Long unit = intRef(obj.get("sourceUnitRef"));
407                for (String member : new String[] {"span", "location"}) {
408                    if (unit == null && obj.get(member) instanceof Map<?, ?>) {
409                        unit = intRef(((Map<?, ?>) obj.get(member)).get("sourceUnitRef"));
410                    }
411                }
412                if (unit != null) {
413                    return unit;
414                }
415                if ("scope".equals(kind)) {
416                    current = intRef(obj.get("parentRef"));
417                } else if ("supportPath".equals(kind)) {
418                    // a support path is its micro-edges' statement's: two paths of one Occurrence through two LOCAL
419                    // endpoints have one shape, and are still two objects (plan 09, ruling f)
420                    Object edges = obj.get("microEdgeRefs");
421                    current = edges instanceof List<?> && !((List<?>) edges).isEmpty()
422                            ? intRef(((List<?>) edges).get(0)) : null;
423                } else if ("endpoint".equals(kind)) {
424                    // a LOCAL endpoint belongs where its scope or its owner (a QUERY_RESULT, a temporary table) is
425                    current = obj.get("scopeRef") != null ? intRef(obj.get("scopeRef")) : intRef(obj.get("ownerRef"));
426                } else {
427                    current = null;
428                }
429            }
430            return null;
431        }
432
433        /** The unit key whose carrier carries an object, or null. */
434        private String owningKey(int i, Long ref) {
435            Long unit = owningUnit(i, ref);
436            return unit == null ? null : unitKey(i, unit);
437        }
438
439        /** The merge key of an object of input i, or null when it has none. */
440        private String keyOf(int i, Long ref) {
441            String kind = kindOf(i, ref);
442            Map<?, ?> obj = semantics.get(i).object(ref);
443            if (obj == null) {
444                return null;
445            }
446            if ("analysisContext".equals(kind)) {
447                return semantics.get(i).keyedIdentity(ref);
448            }
449            if ("sourceUnit".equals(kind)) {
450                return unitKey(i, ref);
451            }
452            if ("statement".equals(kind)) {
453                String unit = unitKey(i, obj.get("sourceUnitRef"));
454                return unit == null ? null : "statement|" + unit + "|" + obj.get("ordinal");
455            }
456            if ("endpoint".equals(kind)) {
457                Map<?, ?> identity = obj.get("identity") instanceof Map<?, ?> ? (Map<?, ?>) obj.get("identity") : null;
458                if (identity != null && "PORTABLE".equals(identity.get("status")) && identity.get("id") instanceof String) {
459                    return "endpoint|" + identity.get("id");
460                }
461                return null;
462            }
463            if ("fact".equals(kind)) {
464                return obj.get("id") instanceof String ? "fact|" + obj.get("id") : null;
465            }
466            if ("occurrence".equals(kind) || "node".equals(kind)) {
467                return obj.get("recordId") instanceof String ? kind + "|" + obj.get("recordId") : null;
468            }
469            return null;
470        }
471
472        /** The analysis contexts of an input, as one key. */
473        private String analysisKey(int i) {
474            TreeSet<String> contexts = new TreeSet<String>();
475            for (Map<?, ?> context : items(docs.get(i).get("analysisContexts"))) {
476                contexts.add(String.valueOf(semantics.get(i).keyedIdentity(context.get("ref"))));
477            }
478            return sha256(CanonicalJson.jcsBytes(new ArrayList<String>(contexts)));
479        }
480
481        private String formKey(int i, Long ref) {
482            return "form|" + kindOf(i, ref) + "|"
483                    + CanonicalJson.jcsText(semantics.get(i).formOf(ref));
484        }
485
486        /** One object of a unit's content: its form without the members modules add, and those members. */
487        private final class Item {
488            final String core;
489            final Map<String, String> added = new TreeMap<String, String>();
490
491            Item(int i, Long ref) {
492                Map<String, Object> form = new TreeMap<String, Object>();
493                Object full = semantics.get(i).formOf(ref);
494                for (Map.Entry<?, ?> entry : ((Map<?, ?>) full).entrySet()) {
495                    String member = String.valueOf(entry.getKey());
496                    if (MODULE_MEMBERS_OF_OBJECTS.contains(member)) {
497                        added.put(member, CanonicalJson.jcsText(entry.getValue()));
498                    } else {
499                        form.put(member, entry.getValue());
500                    }
501                }
502                core = kindOf(i, ref) + " " + CanonicalJson.jcsText(form);
503            }
504
505            /** True when this item says at least what another says. */
506            boolean covers(Item other) {
507                for (Map.Entry<String, String> entry : other.added.entrySet()) {
508                    if (!entry.getValue().equals(added.get(entry.getKey()))) {
509                        return false;
510                    }
511                }
512                return true;
513            }
514        }
515
516        /**
517         * Section 9.2: the records of one unit under one context are unioned. This merger carries the content of
518         * such a unit from one input, which must include every other input's content: compared with references
519         * replaced by identities, every statement, scope, Occurrence, write, diagnostic, ExplainGraph node and
520         * micro-edge, endpoint and module object the unit owns, each with at least the members modules add to it
521         * (support paths, evidence). Among such inputs it takes the highest declared level, then the most content,
522         * then the first; when there is none it refuses.
523         */
524        private void chooseCarriers() {
525            Map<String, List<Integer>> holders = new LinkedHashMap<String, List<Integer>>();
526            List<Map<String, List<Item>>> contents = new ArrayList<Map<String, List<Item>>>();
527            for (int i = 0; i < bytes.size(); i++) {
528                Map<String, List<Item>> mine = new HashMap<String, List<Item>>();
529                for (Long ref : allRefs(i)) {
530                    String unit = owningKey(i, ref);
531                    if (unit == null) {
532                        continue;
533                    }
534                    if ("sourceUnit".equals(kindOf(i, ref))) {
535                        List<Integer> list = holders.get(unit);
536                        if (list == null) {
537                            list = new ArrayList<Integer>();
538                            holders.put(unit, list);
539                        }
540                        if (!list.contains(i)) {
541                            list.add(i);
542                        }
543                    }
544                    List<Item> items = mine.get(unit);
545                    if (items == null) {
546                        items = new ArrayList<Item>();
547                        mine.put(unit, items);
548                    }
549                    items.add(new Item(i, ref));
550                }
551                contents.add(mine);
552            }
553            for (Map.Entry<String, List<Integer>> entry : holders.entrySet()) {
554                String unit = entry.getKey();
555                Integer chosen = null;
556                for (Integer i : entry.getValue()) {
557                    boolean all = true;
558                    for (Integer j : entry.getValue()) {
559                        all &= j.intValue() == i.intValue() || contains(unitItems(contents, i, unit), unitItems(contents, j, unit));
560                    }
561                    if (all && (chosen == null || better(i, chosen, unitItems(contents, i, unit).size(),
562                            unitItems(contents, chosen, unit).size()))) {
563                        chosen = i;
564                    }
565                }
566                if (chosen == null) {
567                    throw new MergeException("inputs " + entry.getValue() + " assert different content for one source"
568                            + " unit under one analysis context and none includes the others: " + unit);
569                }
570                carrier.put(unit, chosen);
571            }
572        }
573
574        private List<Item> unitItems(List<Map<String, List<Item>>> contents, int i, String unit) {
575            List<Item> items = contents.get(i).get(unit);
576            return items == null ? Collections.<Item>emptyList() : items;
577        }
578
579        /** True when every item of a part is covered by a distinct item of a whole with the same core. */
580        private boolean contains(List<Item> whole, List<Item> part) {
581            Map<String, List<Item>> byCore = new HashMap<String, List<Item>>();
582            for (Item item : whole) {
583                List<Item> list = byCore.get(item.core);
584                if (list == null) {
585                    list = new ArrayList<Item>();
586                    byCore.put(item.core, list);
587                }
588                list.add(item);
589            }
590            for (Item item : part) {
591                List<Item> list = byCore.get(item.core);
592                boolean found = false;
593                for (Iterator<Item> it = list == null ? Collections.<Item>emptyList().iterator() : list.iterator();
594                     it.hasNext(); ) {
595                    if (it.next().covers(item)) {
596                        it.remove();
597                        found = true;
598                        break;
599                    }
600                }
601                if (!found) {
602                    return false;
603                }
604            }
605            return true;
606        }
607
608        private boolean better(int i, int chosen, int size, int chosenSize) {
609            int level = LEVELS.indexOf(docs.get(i).get("declaredLevel")) - LEVELS.indexOf(docs.get(chosen).get("declaredLevel"));
610            return level != 0 ? level > 0 : size > chosenSize;
611        }
612
613        private boolean carries(int i, Long ref) {
614            String unit = owningKey(i, ref);
615            return unit == null || carrier.get(unit) == i;
616        }
617
618        /** The new ref of a ref of input i; the object is queued to be carried when it is new. */
619        private Long map(int i, Object value) {
620            Long ref = intRef(value);
621            if (ref == null) {
622                return null;
623            }
624            Long known = remaps.get(i).get(ref);
625            if (known != null) {
626                return known;
627            }
628            String kind = kindOf(i, ref);
629            if (kind == null) {
630                throw new MergeException("input " + i + ": ref " + ref + " names nothing");
631            }
632            String key = keyOf(i, ref);
633            String unit = owningKey(i, ref);
634            Integer from = unit == null ? null : carrier.get(unit);
635            if (from != null && from != i) {
636                // the object belongs to a unit another input carries: use that input's equal object
637                Long there = key != null ? keyed.get(from).get(key) : null;
638                if (there == null) {
639                    there = byForm.get(from).get(formKey(i, ref));
640                }
641                if (there == null) {
642                    throw new MergeException("input " + i + ": " + kind + " ref " + ref + " belongs to " + unit
643                            + ", which input " + from + " carries without it");
644                }
645                Long mapped = map(from, there);
646                remaps.get(i).put(ref, mapped);
647                return mapped;
648            }
649            boolean embedded = "candidateSet".equals(kind) && !graphCsets.get(i).contains(ref);
650            if (embedded && embeddedIn.get(i).containsKey(ref)) {
651                // the set at one end of a FactCandidate: when an equal FactCandidate (of this input or another) is
652                // carried in its place, a reference to this set names the set of that one
653                Object[] owner = embeddedIn.get(i).get(ref);
654                String slot = map(i, owner[0]) + "|" + owner[1];
655                Long shared = embeddedSlots.get(slot);
656                if (shared == null) {
657                    shared = nextRef++;
658                    embeddedSlots.put(slot, shared);
659                }
660                remaps.get(i).put(ref, shared);
661                return shared;
662            }
663            if (key == null && unit == null && !embedded) {
664                // an object without identity that no unit owns is one object per content within one analysis: two
665                // inputs with the same analysis contexts assert it once, inputs with different contexts each keep it
666                key = "analysis|" + analysisKey(i) + "|" + formKey(i, ref);
667            }
668            Long mapped = key == null ? null : byKey.get(key);
669            Map<?, ?> obj = "analysisContext".equals(kind) || "rootCandidateSet".equals(kind)
670                    ? semantics.get(i).object(ref) : indexes.get(i).get(kind, ref);
671            if ("endpoint".equals(kind) && key != null && key.startsWith("endpoint|")) {
672                checkKind(i, key.substring("endpoint|".length()), String.valueOf(obj.get("kind")), mapped);
673            }
674            if (mapped == null) {
675                mapped = nextRef++;
676                if (key != null) {
677                    byKey.put(key, mapped);
678                }
679                // a candidate set inside a FactCandidate end is copied with its FactCandidate, not on its own
680                if (!embedded) {
681                    pending.add(new Object[] {i, kind, obj, mapped});
682                }
683            }
684            remaps.get(i).put(ref, mapped);
685            return mapped;
686        }
687
688        /**
689         * The endpoint kind is not an identity input (identity-v1, Section 5.2.1): FROM orders does not say table or
690         * view. A generic RELATION is refined by a specific relation kind; any other difference cannot be kept in one
691         * document until the merge module carries MergeConflict, so it is refused.
692         */
693        private void checkKind(int i, String identity, String kind, Long mapped) {
694            String seen = kindOfIdentity.get(identity);
695            if (seen == null || seen.equals(kind)) {
696                kindOfIdentity.put(identity, kind);
697                return;
698            }
699            if ("RELATION".equals(seen) && RELATION_KINDS.contains(kind)) {
700                kindOfIdentity.put(identity, kind);
701                kindOverride.put(mapped, kind);
702                conflicts.add(new Conflict(identity, "kind RELATION refined to " + kind + " by input " + i));
703            } else if (!("RELATION".equals(kind) && RELATION_KINDS.contains(seen))) {
704                throw new MergeException("input " + i + ": endpoint " + identity + " is a " + kind
705                        + " where an earlier input has a " + seen + "; both cannot be kept (Section 9.4)");
706            }
707        }
708
709        private void mergeInput(int i) {
710            DocumentIndex index = indexes.get(i);
711            for (Long ref : allRefs(i)) {
712                String kind = kindOf(i, ref);
713                boolean embedded = "candidateSet".equals(kind) && !graphCsets.get(i).contains(ref);
714                if (!embedded && carries(i, ref) && !"supportPath".equals(kind) && !"factCandidate".equals(kind)
715                        && !"fact".equals(kind)) {
716                    map(i, ref);
717                }
718            }
719            Map<?, ?> graph = index.graph();
720            Map<Long, Map<?, ?>> listed = new HashMap<Long, Map<?, ?>>();
721            for (Map<?, ?> entry : items(graph.get("statementCompleteness"))) {
722                listed.put(intRef(entry.get("statementRef")), entry);
723            }
724            for (Map<?, ?> statement : index.all("statement")) {
725                Long ref = intRef(statement.get("ref"));
726                if (!carries(i, ref)) {
727                    continue;
728                }
729                Map<?, ?> entry = listed.get(ref);
730                Object completeness = entry != null ? entry.get("completeness")
731                        : index.hasGraph() ? graph.get("completeness") : "PARTIAL";
732                Long out = remaps.get(i).get(ref);
733                Object[] prior = statementGraph.get(out);
734                // a statement several inputs carry has the graph of the one that has the most of it (an L1 input has
735                // none of it), and of equals the one that says more, whatever the order of the inputs: merging stays
736                // associative
737                if (prior == null || graphRank(completeness) > graphRank(prior[0])
738                        || (graphRank(completeness) == graphRank(prior[0]) && prior[1] == null && entry != null)) {
739                    statementGraph.put(out, new Object[] {completeness, entry == null ? null : copy(i, entry, false)});
740                }
741            }
742            drain();
743        }
744
745        /** Carries every queued object, which may queue the objects it references. */
746        private void drain() {
747            while (!pending.isEmpty()) {
748                Object[] job = pending.poll();
749                int i = (Integer) job[0];
750                String kind = (String) job[1];
751                @SuppressWarnings("unchecked")
752                Map<String, Object> copied = (Map<String, Object>) copy(i, job[2], true);
753                copied.put("ref", job[3]);
754                out.get(kind).put((Long) job[3], copied);
755            }
756        }
757
758        /** One container being copied. */
759        private final class Frame {
760            final Iterator<?> items;
761            final Map<String, Object> map;
762            final List<Object> list;
763            String key;
764
765            Frame(Object container) {
766                if (container instanceof Map<?, ?>) {
767                    items = ((Map<?, ?>) container).entrySet().iterator();
768                    map = new LinkedHashMap<String, Object>();
769                    list = null;
770                } else {
771                    items = ((List<?>) container).iterator();
772                    map = null;
773                    list = new ArrayList<Object>();
774                }
775            }
776
777            void add(Object value) {
778                if (map != null) {
779                    map.put(key, value);
780                } else {
781                    list.add(value);
782                }
783            }
784        }
785
786        /**
787         * A copy of a JSON value of input i with every reference renumbered, made with an explicit stack. The
788         * {@code ref} of the top object is its own and is left for the caller; a {@code ref} below it (a candidate
789         * set inside a FactCandidate) is renumbered.
790         */
791        private Object copy(int i, Object value, boolean top) {
792            if (!(value instanceof Map<?, ?> || value instanceof List<?>)) {
793                return value;
794            }
795            Deque<Frame> stack = new ArrayDeque<Frame>();
796            stack.push(new Frame(value));
797            Object result = null;
798            while (!stack.isEmpty()) {
799                Frame frame = stack.peek();
800                if (!frame.items.hasNext()) {
801                    stack.pop();
802                    Object built = frame.map != null ? frame.map : frame.list;
803                    if (stack.isEmpty()) {
804                        result = built;
805                    } else {
806                        stack.peek().add(built);
807                    }
808                    continue;
809                }
810                Object item = frame.items.next();
811                Object child;
812                if (frame.map != null) {
813                    Map.Entry<?, ?> entry = (Map.Entry<?, ?>) item;
814                    String member = (String) entry.getKey();
815                    child = entry.getValue();
816                    frame.key = member;
817                    if ("extensions".equals(member)) {
818                        frame.add(child);
819                        continue;
820                    }
821                    if ("ref".equals(member)) {
822                        frame.add(top && stack.size() == 1 ? child : map(i, child));
823                        continue;
824                    }
825                    if (member.endsWith("Refs") && child instanceof List<?>) {
826                        List<Object> refs = new ArrayList<Object>();
827                        for (Object r : (List<?>) child) {
828                            refs.add(map(i, r));
829                        }
830                        frame.add(refs);
831                        continue;
832                    }
833                    if (member.endsWith("Ref") && intRef(child) != null) {
834                        frame.add(map(i, child));
835                        continue;
836                    }
837                } else {
838                    child = item;
839                }
840                if (child instanceof Map<?, ?> || child instanceof List<?>) {
841                    stack.push(new Frame(child));
842                } else {
843                    frame.add(child);
844                }
845            }
846            return result;
847        }
848
849        // -----------------------------------------------------------------------------------------
850
851        private Document buildDocument() {
852            Map<String, Object> first = docs.get(0);
853            Map<String, Object> doc = new LinkedHashMap<String, Object>();
854            doc.put("specUri", first.get("specUri"));
855            doc.put("specVersion", first.get("specVersion"));
856            String level = "L1";
857            Set<Object> modules = new TreeSet<Object>();
858            Object analysis = null;
859            List<Object> serving = new ArrayList<Object>();
860            Map<Object, List<Object>> features = new TreeMap<Object, List<Object>>();
861            for (int i = 0; i < docs.size(); i++) {
862                Map<String, Object> input = docs.get(i);
863                if (LEVELS.indexOf(input.get("declaredLevel")) > LEVELS.indexOf(level)) {
864                    level = (String) input.get("declaredLevel");
865                }
866                modules.addAll((List<?>) input.get("modules"));
867                analysis = combined(analysis, input.get("analysisCompleteness"));
868                serving.add(copy(i, input.get("servingCompleteness"), false));
869                for (Map<?, ?> feature : items(input.get("featureCompleteness"))) {
870                    List<Object> list = features.get(feature.get("feature"));
871                    if (list == null) {
872                        list = new ArrayList<Object>();
873                        features.put(feature.get("feature"), list);
874                    }
875                    list.add(copy(i, feature, false));
876                }
877            }
878            drain();
879            for (Map.Entry<Long, String> entry : kindOverride.entrySet()) {
880                out.get("endpoint").get(entry.getKey()).put("kind", entry.getValue());
881            }
882            doc.put("declaredLevel", level);
883            doc.put("modules", new ArrayList<Object>(modules));
884            Map<String, Object> header = new LinkedHashMap<String, Object>();
885            header.put("producer", producer.toJson());
886            Map<?, ?> header0 = (Map<?, ?>) first.get("header");
887            header.put("registryVersion", header0.get("registryVersion"));
888            header.put("sourceEmbedding", header0.get("sourceEmbedding"));
889            if (createdAt != null) {
890                header.put("createdAt", createdAt);
891            }
892            doc.put("header", header);
893            for (String[] collection : COLLECTIONS) {
894                // the root candidate sets are present only when an Occurrence of a Fact names one (spec 4.3, rule 1)
895                if (!GRAPH_KINDS.contains(collection[0])
896                        && !("rootCandidateSet".equals(collection[0]) && out.get(collection[0]).isEmpty())) {
897                    doc.put(collection[1], new ArrayList<Object>(out.get(collection[0]).values()));
898                }
899            }
900            for (String member : DocumentIndex.MODULE_MEMBERS) {
901                if (!out.get("module:" + member).isEmpty()) {
902                    doc.put(member, new ArrayList<Object>(out.get("module:" + member).values()));
903                }
904            }
905            doc.put("analysisCompleteness", analysis);
906            doc.put("servingCompleteness", mergedServing(serving));
907            if (!features.isEmpty()) {
908                List<Object> list = new ArrayList<Object>();
909                for (List<Object> entries : features.values()) {
910                    list.add(mergedFeature(entries));
911                }
912                doc.put("featureCompleteness", list);
913            }
914            if (modules.contains("explain")) {
915                doc.put("explainGraph", graph());
916            }
917            dropRepeats(doc);
918            return Document.fromJson(doc);
919        }
920
921        /**
922         * Equal objects without an identity are one object of the analysis, so two elements of a SET can become
923         * equal once their refs are renumbered (two equal diagnostics of one statement, two equal FactCandidates
924         * among the inputs of one target field); the SET keeps such an element once. Two elements with an equal
925         * sort key and different content stay, and canonical JSON refuses them.
926         */
927        @SuppressWarnings("unchecked")
928        private void dropRepeats(Map<String, Object> doc) {
929            Deque<Object[]> stack = new ArrayDeque<Object[]>();
930            stack.push(new Object[] {doc, ArrayRegistry.standard().root()});
931            while (!stack.isEmpty()) {
932                Object[] top = stack.pop();
933                ArrayRegistry.Position position = (ArrayRegistry.Position) top[1];
934                if (top[0] instanceof Map<?, ?>) {
935                    for (Map.Entry<?, ?> entry : ((Map<?, ?>) top[0]).entrySet()) {
936                        if (!"extensions".equals(entry.getKey())) {
937                            stack.push(new Object[] {entry.getValue(),
938                                    position == null ? null : position.member(String.valueOf(entry.getKey()))});
939                        }
940                    }
941                } else if (top[0] instanceof List<?>) {
942                    List<Object> list = (List<Object>) top[0];
943                    if (position != null && position.registration() != null
944                            && position.registration().getOrder() == ArrayRegistry.Order.SET) {
945                        Set<String> seen = new HashSet<String>();
946                        for (Iterator<Object> it = list.iterator(); it.hasNext();) {
947                            if (!seen.add(CanonicalJson.jcsText(it.next()))) {
948                                it.remove();
949                            }
950                        }
951                    }
952                    ArrayRegistry.Position element = position == null ? null : position.element();
953                    for (Object item : list) {
954                        stack.push(new Object[] {item, element});
955                    }
956                }
957            }
958        }
959
960        /** Equal objects stay as they are; otherwise TRUNCATED unless all are COMPLETE, the truncations unioned. */
961        @SuppressWarnings("unchecked")
962        private Map<String, Object> mergedServing(List<Object> objects) {
963            List<Object> distinct = distinct(objects);
964            if (distinct.size() == 1) {
965                return (Map<String, Object>) distinct.get(0);
966            }
967            Map<String, Object> merged = new LinkedHashMap<String, Object>();
968            boolean complete = true;
969            List<Object> truncations = new ArrayList<Object>();
970            for (Object object : distinct) {
971                Map<String, Object> serving = (Map<String, Object>) object;
972                rejectExtensions(serving, "servingCompleteness");
973                complete &= "COMPLETE".equals(serving.get("status"));
974                if (serving.get("truncations") instanceof List<?>) {
975                    truncations.addAll((List<Object>) serving.get("truncations"));
976                }
977            }
978            merged.put("status", complete ? "COMPLETE" : "TRUNCATED");
979            if (!truncations.isEmpty()) {
980                merged.put("truncations", distinct(truncations));
981            }
982            return merged;
983        }
984
985        /** Equal entries stay as they are; otherwise the status is combined and the diagnostics unioned. */
986        @SuppressWarnings("unchecked")
987        private Map<String, Object> mergedFeature(List<Object> objects) {
988            List<Object> distinct = distinct(objects);
989            if (distinct.size() == 1) {
990                return (Map<String, Object>) distinct.get(0);
991            }
992            Map<String, Object> merged = new LinkedHashMap<String, Object>();
993            Object status = null;
994            Set<Object> diagnostics = new LinkedHashSet<Object>();
995            for (Object object : distinct) {
996                Map<String, Object> entry = (Map<String, Object>) object;
997                rejectExtensions(entry, "featureCompleteness");
998                merged.put("feature", entry.get("feature"));
999                status = combined(status, entry.get("status"));
1000                if (entry.get("diagnosticRefs") instanceof List<?>) {
1001                    diagnostics.addAll((List<Object>) entry.get("diagnosticRefs"));
1002                }
1003            }
1004            merged.put("status", status);
1005            if (!diagnostics.isEmpty()) {
1006                merged.put("diagnosticRefs", new ArrayList<Object>(diagnostics));
1007            }
1008            return merged;
1009        }
1010
1011        private void rejectExtensions(Map<String, Object> object, String what) {
1012            if (object.containsKey("extensions")) {
1013                throw new MergeException(what + " objects with extensions differ between inputs and cannot be combined");
1014            }
1015        }
1016
1017        /** How much of a statement's graph an input has: COMPLETE above PARTIAL above anything else. */
1018        private int graphRank(Object completeness) {
1019            return "COMPLETE".equals(completeness) ? 2 : "PARTIAL".equals(completeness) ? 1 : 0;
1020        }
1021
1022        private List<Object> distinct(List<Object> values) {
1023            Map<String, Object> seen = new LinkedHashMap<String, Object>();
1024            for (Object value : values) {
1025                seen.put(CanonicalJson.jcsText(value), value);
1026            }
1027            return new ArrayList<Object>(seen.values());
1028        }
1029
1030        private Map<String, Object> graph() {
1031            Map<String, Object> graph = new LinkedHashMap<String, Object>();
1032            boolean allComplete = true;
1033            for (Object[] completeness : statementGraph.values()) {
1034                allComplete &= "COMPLETE".equals(completeness[0]);
1035            }
1036            String byDefault = allComplete ? "COMPLETE" : "PARTIAL";
1037            graph.put("completeness", byDefault);
1038            List<Object> listed = new ArrayList<Object>();
1039            for (Map.Entry<Long, Object[]> entry : statementGraph.entrySet()) {
1040                Object[] value = entry.getValue();
1041                if (value[1] != null && (!byDefault.equals(value[0]) || ((Map<?, ?>) value[1]).get("diagnosticRefs") != null)) {
1042                    // only a statement that differs from the graph's completeness is listed (fold-v1, Section 2)
1043                    @SuppressWarnings("unchecked")
1044                    Map<String, Object> copied = (Map<String, Object>) value[1];
1045                    copied.put("statementRef", entry.getKey());
1046                    listed.add(copied);
1047                } else if (!byDefault.equals(value[0])) {
1048                    Map<String, Object> item = new LinkedHashMap<String, Object>();
1049                    item.put("statementRef", entry.getKey());
1050                    item.put("completeness", value[0]);
1051                    listed.add(item);
1052                }
1053            }
1054            if (!listed.isEmpty()) {
1055                graph.put("statementCompleteness", listed);
1056            }
1057            graph.put("nodes", new ArrayList<Object>(out.get("node").values()));
1058            graph.put("microEdges", new ArrayList<Object>(out.get("microEdge").values()));
1059            if (!out.get("candidateSet").isEmpty()) {
1060                graph.put("candidateSets", new ArrayList<Object>(out.get("candidateSet").values()));
1061            }
1062            graph.put("supportPaths", new ArrayList<Object>(out.get("supportPath").values()));
1063            return graph;
1064        }
1065    }
1066}