We are currently solving the following issue within our repository. Here is the issue text:
--- BEGIN ISSUE ---
Partial parsing: duplicate resource error
### Describe the bug
When the `--partial-parse` flag is enable, dbt can fail with an errant compilation error:

```
$ dbt --partial-parse compile
Running with dbt=0.15.0
Encountered an error:
Compilation Error
  dbt found two resources with the name "adwords_ad_performance_adapter". Since these resources have the same name,
  dbt will be unable to find the correct resource when ref("adwords_ad_performance_adapter") is used. To fix this,
  change the name of one of these resources:
  - model.adwords.adwords_ad_performance_adapter (models/router/adapter/criteria/adwords_ad_performance_adapter.sql)
  - model.adwords.adwords_ad_performance_adapter (models/router/adapter/criteria/adwords_ad_performance_adapter.sql)
```

dbt is reporting the same model twice. Interestingly, there actually _are_ two instances of models named `adwords_ad_performance_adapter` in the [adwords package](https://github.com/fishtown-analytics/adwords):
 - https://github.com/fishtown-analytics/adwords/blob/master/models/router/adapter/criteria/adwords_ad_performance_adapter.sql
 - https://github.com/fishtown-analytics/adwords/blob/master/models/router/adapter/url/adwords_ad_performance_adapter.sql

These models are conditionally enabled using a variable, `adapter_value`, defined in the `dbt_project.yml` file.

It's not yet clear to me if dbt is failing because it's finding both of these models and reporting an errant error message, or if dbt really is picking up the same model twice somehow.

### Steps To Reproduce

Add the following to `packages.yml`:
```
packages:
  - package: fishtown-analytics/adwords
    version: 0.2.9
```

Add the following to `dbt_project.yml`:
```
models:
    adwords:
        vars:
            adapter_value: criteria
```

Create a model called `adwords_criteria_performance.sql` with the contents:
```
select 1 as id
```

```
# Succeeds
$ dbt compile

# Fails with duplicate model error
$ dbt --partial-parse compile
```

### Expected behavior
dbt should compile and run this project successfully.

**The output of `dbt --version`:**
```
dbt v0.15.0
```

--- END ISSUE ---

Below are some code segments, each from a relevant file. One or more of these files may contain bugs.
--- BEGIN FILES ---
Path: `core/dbt/parser/results.py`
Content:
```
1 from dataclasses import dataclass, field
2 from typing import TypeVar, MutableMapping, Mapping, Union, List
3 
4 from hologram import JsonSchemaMixin
5 
6 from dbt.contracts.graph.manifest import SourceFile, RemoteFile, FileHash
7 from dbt.contracts.graph.parsed import (
8     ParsedNode, HasUniqueID, ParsedMacro, ParsedDocumentation, ParsedNodePatch,
9     ParsedSourceDefinition, ParsedAnalysisNode, ParsedHookNode, ParsedRPCNode,
10     ParsedModelNode, ParsedSeedNode, ParsedTestNode, ParsedSnapshotNode,
11 )
12 from dbt.contracts.util import Writable, Replaceable
13 from dbt.exceptions import (
14     raise_duplicate_resource_name, raise_duplicate_patch_name,
15     CompilationException, InternalException
16 )
17 from dbt.version import __version__
18 
19 
20 # Parsers can return anything as long as it's a unique ID
21 ParsedValueType = TypeVar('ParsedValueType', bound=HasUniqueID)
22 
23 
24 def _check_duplicates(
25     value: HasUniqueID, src: Mapping[str, HasUniqueID]
26 ):
27     if value.unique_id in src:
28         raise_duplicate_resource_name(value, src[value.unique_id])
29 
30 
31 ManifestNodes = Union[
32     ParsedAnalysisNode,
33     ParsedHookNode,
34     ParsedModelNode,
35     ParsedSeedNode,
36     ParsedTestNode,
37     ParsedSnapshotNode,
38     ParsedRPCNode,
39 ]
40 
41 
42 def dict_field():
43     return field(default_factory=dict)
44 
45 
46 @dataclass
47 class ParseResult(JsonSchemaMixin, Writable, Replaceable):
48     vars_hash: FileHash
49     profile_hash: FileHash
50     project_hashes: MutableMapping[str, FileHash]
51     nodes: MutableMapping[str, ManifestNodes] = dict_field()
52     sources: MutableMapping[str, ParsedSourceDefinition] = dict_field()
53     docs: MutableMapping[str, ParsedDocumentation] = dict_field()
54     macros: MutableMapping[str, ParsedMacro] = dict_field()
55     patches: MutableMapping[str, ParsedNodePatch] = dict_field()
56     files: MutableMapping[str, SourceFile] = dict_field()
57     disabled: MutableMapping[str, List[ParsedNode]] = dict_field()
58     dbt_version: str = __version__
59 
60     def get_file(self, source_file: SourceFile) -> SourceFile:
61         key = source_file.search_key
62         if key is None:
63             return source_file
64         if key not in self.files:
65             self.files[key] = source_file
66         return self.files[key]
67 
68     def add_source(
69         self, source_file: SourceFile, node: ParsedSourceDefinition
70     ):
71         # nodes can't be overwritten!
72         _check_duplicates(node, self.sources)
73         self.sources[node.unique_id] = node
74         self.get_file(source_file).sources.append(node.unique_id)
75 
76     def add_node(self, source_file: SourceFile, node: ManifestNodes):
77         # nodes can't be overwritten!
78         _check_duplicates(node, self.nodes)
79         self.nodes[node.unique_id] = node
80         self.get_file(source_file).nodes.append(node.unique_id)
81 
82     def add_disabled(self, source_file: SourceFile, node: ParsedNode):
83         if node.unique_id in self.disabled:
84             self.disabled[node.unique_id].append(node)
85         else:
86             self.disabled[node.unique_id] = [node]
87         self.get_file(source_file).nodes.append(node.unique_id)
88 
89     def add_macro(self, source_file: SourceFile, macro: ParsedMacro):
90         # macros can be overwritten (should they be?)
91         self.macros[macro.unique_id] = macro
92         self.get_file(source_file).macros.append(macro.unique_id)
93 
94     def add_doc(self, source_file: SourceFile, doc: ParsedDocumentation):
95         # Docs also can be overwritten (should they be?)
96         self.docs[doc.unique_id] = doc
97         self.get_file(source_file).docs.append(doc.unique_id)
98 
99     def add_patch(self, source_file: SourceFile, patch: ParsedNodePatch):
100         # matches can't be overwritten
101         if patch.name in self.patches:
102             raise_duplicate_patch_name(patch.name, patch,
103                                        self.patches[patch.name])
104         self.patches[patch.name] = patch
105         self.get_file(source_file).patches.append(patch.name)
106 
107     def _get_disabled(
108         self, unique_id: str, match_file: SourceFile
109     ) -> List[ParsedNode]:
110         if unique_id not in self.disabled:
111             raise InternalException(
112                 'called _get_disabled with id={}, but it does not exist'
113                 .format(unique_id)
114             )
115         return [
116             n for n in self.disabled[unique_id]
117             if n.original_file_path == match_file.path.original_file_path
118         ]
119 
120     def sanitized_update(
121         self, source_file: SourceFile, old_result: 'ParseResult',
122     ) -> bool:
123         """Perform a santized update. If the file can't be updated, invalidate
124         it and return false.
125         """
126         if isinstance(source_file.path, RemoteFile):
127             return False
128 
129         old_file = old_result.get_file(source_file)
130         for doc_id in old_file.docs:
131             doc = _expect_value(doc_id, old_result.docs, old_file, "docs")
132             self.add_doc(source_file, doc)
133 
134         for macro_id in old_file.macros:
135             macro = _expect_value(
136                 macro_id, old_result.macros, old_file, "macros"
137             )
138             self.add_macro(source_file, macro)
139 
140         for source_id in old_file.sources:
141             source = _expect_value(
142                 source_id, old_result.sources, old_file, "sources"
143             )
144             self.add_source(source_file, source)
145 
146         # because we know this is how we _parsed_ the node, we can safely
147         # assume if it's disabled it was done by the project or file, and
148         # we can keep our old data
149         for node_id in old_file.nodes:
150             if node_id in old_result.nodes:
151                 node = old_result.nodes[node_id]
152                 self.add_node(source_file, node)
153             elif node_id in old_result.disabled:
154                 matches = old_result._get_disabled(node_id, source_file)
155                 for match in matches:
156                     self.add_disabled(source_file, match)
157             else:
158                 raise CompilationException(
159                     'Expected to find "{}" in cached "manifest.nodes" or '
160                     '"manifest.disabled" based on cached file information: {}!'
161                     .format(node_id, old_file)
162                 )
163 
164         for name in old_file.patches:
165             patch = _expect_value(
166                 name, old_result.patches, old_file, "patches"
167             )
168             self.add_patch(source_file, patch)
169 
170         return True
171 
172     def has_file(self, source_file: SourceFile) -> bool:
173         key = source_file.search_key
174         if key is None:
175             return False
176         if key not in self.files:
177             return False
178         my_checksum = self.files[key].checksum
179         return my_checksum == source_file.checksum
180 
181     @classmethod
182     def rpc(cls):
183         # ugh!
184         return cls(FileHash.empty(), FileHash.empty(), {})
185 
186 
187 T = TypeVar('T')
188 
189 
190 def _expect_value(
191     key: str, src: Mapping[str, T], old_file: SourceFile, name: str
192 ) -> T:
193     if key not in src:
194         raise CompilationException(
195             'Expected to find "{}" in cached "result.{}" based '
196             'on cached file information: {}!'
197             .format(key, name, old_file)
198         )
199     return src[key]
200
```
--- END FILES ---

Please first localize the bug based on the issue statement, and then generate a patch according to the `git diff` format fenced by three backticks.

Here is an example:

```diff
diff --git a/examples/server_async.py b/examples/server_async.py
--- a/examples/server_async.py
+++ b/examples/server_async.py
@@ -313,4 +313,4 @@
 
 
 if __name__ == "__main__":
-    asyncio.run(run_async_server("."), debug=True)
+    asyncio.run(run_async_server(), debug=True)
diff --git a/examples/server_sync.py b/examples/server_sync.py
--- a/examples/server_sync.py
+++ b/examples/server_sync.py
@@ -313,5 +313,5 @@
 
 
 if __name__ == "__main__":
-    server = run_sync_server(".")
+    server = run_sync_server()
     server.shutdown()

```

diff --git a/core/dbt/parser/results.py b/core/dbt/parser/results.py
--- a/core/dbt/parser/results.py
+++ b/core/dbt/parser/results.py
@@ -117,6 +117,37 @@
             if n.original_file_path == match_file.path.original_file_path
         ]
 
+    def _process_node(
+        self,
+        node_id: str,
+        source_file: SourceFile,
+        old_file: SourceFile,
+        old_result: 'ParseResult',
+    ) -> None:
+        """Nodes are a special kind of complicated - there can be multiple
+        with the same name, as long as all but one are disabled.
+        """
+        source_path = source_file.path.original_file_path
+        found: bool = False
+        if node_id in old_result.nodes:
+            old_node = old_result.nodes[node_id]
+            if old_node.original_file_path == source_path:
+                self.add_node(source_file, old_node)
+                found = True
+
+        if node_id in old_result.disabled:
+            matches = old_result._get_disabled(node_id, source_file)
+            for match in matches:
+                self.add_disabled(source_file, match)
+                found = True
+
+        if not found:
+            raise CompilationException(
+                'Expected to find "{}" in cached "manifest.nodes" or '
+                '"manifest.disabled" based on cached file information: {}!'
+                .format(node_id, old_file)
+            )
+
     def sanitized_update(
         self, source_file: SourceFile, old_result: 'ParseResult',
     ) -> bool:
@@ -146,20 +177,10 @@
         # because we know this is how we _parsed_ the node, we can safely
         # assume if it's disabled it was done by the project or file, and
         # we can keep our old data
+        # the node ID could be in old_result.disabled AND in old_result.nodes.
+        # In that case, we have to make sure the path also matches.
         for node_id in old_file.nodes:
-            if node_id in old_result.nodes:
-                node = old_result.nodes[node_id]
-                self.add_node(source_file, node)
-            elif node_id in old_result.disabled:
-                matches = old_result._get_disabled(node_id, source_file)
-                for match in matches:
-                    self.add_disabled(source_file, match)
-            else:
-                raise CompilationException(
-                    'Expected to find "{}" in cached "manifest.nodes" or '
-                    '"manifest.disabled" based on cached file information: {}!'
-                    .format(node_id, old_file)
-                )
+            self._process_node(node_id, source_file, old_file, old_result)
 
         for name in old_file.patches:
             patch = _expect_value(

{"golden_diff": "diff --git a/core/dbt/parser/results.py b/core/dbt/parser/results.py\n--- a/core/dbt/parser/results.py\n+++ b/core/dbt/parser/results.py\n@@ -117,6 +117,37 @@\n             if n.original_file_path == match_file.path.original_file_path\n         ]\n \n+    def _process_node(\n+        self,\n+        node_id: str,\n+        source_file: SourceFile,\n+        old_file: SourceFile,\n+        old_result: 'ParseResult',\n+    ) -> None:\n+        \"\"\"Nodes are a special kind of complicated - there can be multiple\n+        with the same name, as long as all but one are disabled.\n+        \"\"\"\n+        source_path = source_file.path.original_file_path\n+        found: bool = False\n+        if node_id in old_result.nodes:\n+            old_node = old_result.nodes[node_id]\n+            if old_node.original_file_path == source_path:\n+                self.add_node(source_file, old_node)\n+                found = True\n+\n+        if node_id in old_result.disabled:\n+            matches = old_result._get_disabled(node_id, source_file)\n+            for match in matches:\n+                self.add_disabled(source_file, match)\n+                found = True\n+\n+        if not found:\n+            raise CompilationException(\n+                'Expected to find \"{}\" in cached \"manifest.nodes\" or '\n+                '\"manifest.disabled\" based on cached file information: {}!'\n+                .format(node_id, old_file)\n+            )\n+\n     def sanitized_update(\n         self, source_file: SourceFile, old_result: 'ParseResult',\n     ) -> bool:\n@@ -146,20 +177,10 @@\n         # because we know this is how we _parsed_ the node, we can safely\n         # assume if it's disabled it was done by the project or file, and\n         # we can keep our old data\n+        # the node ID could be in old_result.disabled AND in old_result.nodes.\n+        # In that case, we have to make sure the path also matches.\n         for node_id in old_file.nodes:\n-            if node_id in old_result.nodes:\n-                node = old_result.nodes[node_id]\n-                self.add_node(source_file, node)\n-            elif node_id in old_result.disabled:\n-                matches = old_result._get_disabled(node_id, source_file)\n-                for match in matches:\n-                    self.add_disabled(source_file, match)\n-            else:\n-                raise CompilationException(\n-                    'Expected to find \"{}\" in cached \"manifest.nodes\" or '\n-                    '\"manifest.disabled\" based on cached file information: {}!'\n-                    .format(node_id, old_file)\n-                )\n+            self._process_node(node_id, source_file, old_file, old_result)\n \n         for name in old_file.patches:\n             patch = _expect_value(\n", "issue": "Partial parsing: duplicate resource error\n### Describe the bug\r\nWhen the `--partial-parse` flag is enable, dbt can fail with an errant compilation error:\r\n\r\n```\r\n$ dbt --partial-parse compile\r\nRunning with dbt=0.15.0\r\nEncountered an error:\r\nCompilation Error\r\n  dbt found two resources with the name \"adwords_ad_performance_adapter\". Since these resources have the same name,\r\n  dbt will be unable to find the correct resource when ref(\"adwords_ad_performance_adapter\") is used. To fix this,\r\n  change the name of one of these resources:\r\n  - model.adwords.adwords_ad_performance_adapter (models/router/adapter/criteria/adwords_ad_performance_adapter.sql)\r\n  - model.adwords.adwords_ad_performance_adapter (models/router/adapter/criteria/adwords_ad_performance_adapter.sql)\r\n```\r\n\r\ndbt is reporting the same model twice. Interestingly, there actually _are_ two instances of models named `adwords_ad_performance_adapter` in the [adwords package](https://github.com/fishtown-analytics/adwords):\r\n - https://github.com/fishtown-analytics/adwords/blob/master/models/router/adapter/criteria/adwords_ad_performance_adapter.sql\r\n - https://github.com/fishtown-analytics/adwords/blob/master/models/router/adapter/url/adwords_ad_performance_adapter.sql\r\n\r\nThese models are conditionally enabled using a variable, `adapter_value`, defined in the `dbt_project.yml` file.\r\n\r\nIt's not yet clear to me if dbt is failing because it's finding both of these models and reporting an errant error message, or if dbt really is picking up the same model twice somehow.\r\n\r\n### Steps To Reproduce\r\n\r\nAdd the following to `packages.yml`:\r\n```\r\npackages:\r\n  - package: fishtown-analytics/adwords\r\n    version: 0.2.9\r\n```\r\n\r\nAdd the following to `dbt_project.yml`:\r\n```\r\nmodels:\r\n    adwords:\r\n        vars:\r\n            adapter_value: criteria\r\n```\r\n\r\nCreate a model called `adwords_criteria_performance.sql` with the contents:\r\n```\r\nselect 1 as id\r\n```\r\n\r\n```\r\n# Succeeds\r\n$ dbt compile\r\n\r\n# Fails with duplicate model error\r\n$ dbt --partial-parse compile\r\n```\r\n\r\n### Expected behavior\r\ndbt should compile and run this project successfully.\r\n\r\n**The output of `dbt --version`:**\r\n```\r\ndbt v0.15.0\r\n```\n", "before_files": [{"content": "from dataclasses import dataclass, field\nfrom typing import TypeVar, MutableMapping, Mapping, Union, List\n\nfrom hologram import JsonSchemaMixin\n\nfrom dbt.contracts.graph.manifest import SourceFile, RemoteFile, FileHash\nfrom dbt.contracts.graph.parsed import (\n    ParsedNode, HasUniqueID, ParsedMacro, ParsedDocumentation, ParsedNodePatch,\n    ParsedSourceDefinition, ParsedAnalysisNode, ParsedHookNode, ParsedRPCNode,\n    ParsedModelNode, ParsedSeedNode, ParsedTestNode, ParsedSnapshotNode,\n)\nfrom dbt.contracts.util import Writable, Replaceable\nfrom dbt.exceptions import (\n    raise_duplicate_resource_name, raise_duplicate_patch_name,\n    CompilationException, InternalException\n)\nfrom dbt.version import __version__\n\n\n# Parsers can return anything as long as it's a unique ID\nParsedValueType = TypeVar('ParsedValueType', bound=HasUniqueID)\n\n\ndef _check_duplicates(\n    value: HasUniqueID, src: Mapping[str, HasUniqueID]\n):\n    if value.unique_id in src:\n        raise_duplicate_resource_name(value, src[value.unique_id])\n\n\nManifestNodes = Union[\n    ParsedAnalysisNode,\n    ParsedHookNode,\n    ParsedModelNode,\n    ParsedSeedNode,\n    ParsedTestNode,\n    ParsedSnapshotNode,\n    ParsedRPCNode,\n]\n\n\ndef dict_field():\n    return field(default_factory=dict)\n\n\n@dataclass\nclass ParseResult(JsonSchemaMixin, Writable, Replaceable):\n    vars_hash: FileHash\n    profile_hash: FileHash\n    project_hashes: MutableMapping[str, FileHash]\n    nodes: MutableMapping[str, ManifestNodes] = dict_field()\n    sources: MutableMapping[str, ParsedSourceDefinition] = dict_field()\n    docs: MutableMapping[str, ParsedDocumentation] = dict_field()\n    macros: MutableMapping[str, ParsedMacro] = dict_field()\n    patches: MutableMapping[str, ParsedNodePatch] = dict_field()\n    files: MutableMapping[str, SourceFile] = dict_field()\n    disabled: MutableMapping[str, List[ParsedNode]] = dict_field()\n    dbt_version: str = __version__\n\n    def get_file(self, source_file: SourceFile) -> SourceFile:\n        key = source_file.search_key\n        if key is None:\n            return source_file\n        if key not in self.files:\n            self.files[key] = source_file\n        return self.files[key]\n\n    def add_source(\n        self, source_file: SourceFile, node: ParsedSourceDefinition\n    ):\n        # nodes can't be overwritten!\n        _check_duplicates(node, self.sources)\n        self.sources[node.unique_id] = node\n        self.get_file(source_file).sources.append(node.unique_id)\n\n    def add_node(self, source_file: SourceFile, node: ManifestNodes):\n        # nodes can't be overwritten!\n        _check_duplicates(node, self.nodes)\n        self.nodes[node.unique_id] = node\n        self.get_file(source_file).nodes.append(node.unique_id)\n\n    def add_disabled(self, source_file: SourceFile, node: ParsedNode):\n        if node.unique_id in self.disabled:\n            self.disabled[node.unique_id].append(node)\n        else:\n            self.disabled[node.unique_id] = [node]\n        self.get_file(source_file).nodes.append(node.unique_id)\n\n    def add_macro(self, source_file: SourceFile, macro: ParsedMacro):\n        # macros can be overwritten (should they be?)\n        self.macros[macro.unique_id] = macro\n        self.get_file(source_file).macros.append(macro.unique_id)\n\n    def add_doc(self, source_file: SourceFile, doc: ParsedDocumentation):\n        # Docs also can be overwritten (should they be?)\n        self.docs[doc.unique_id] = doc\n        self.get_file(source_file).docs.append(doc.unique_id)\n\n    def add_patch(self, source_file: SourceFile, patch: ParsedNodePatch):\n        # matches can't be overwritten\n        if patch.name in self.patches:\n            raise_duplicate_patch_name(patch.name, patch,\n                                       self.patches[patch.name])\n        self.patches[patch.name] = patch\n        self.get_file(source_file).patches.append(patch.name)\n\n    def _get_disabled(\n        self, unique_id: str, match_file: SourceFile\n    ) -> List[ParsedNode]:\n        if unique_id not in self.disabled:\n            raise InternalException(\n                'called _get_disabled with id={}, but it does not exist'\n                .format(unique_id)\n            )\n        return [\n            n for n in self.disabled[unique_id]\n            if n.original_file_path == match_file.path.original_file_path\n        ]\n\n    def sanitized_update(\n        self, source_file: SourceFile, old_result: 'ParseResult',\n    ) -> bool:\n        \"\"\"Perform a santized update. If the file can't be updated, invalidate\n        it and return false.\n        \"\"\"\n        if isinstance(source_file.path, RemoteFile):\n            return False\n\n        old_file = old_result.get_file(source_file)\n        for doc_id in old_file.docs:\n            doc = _expect_value(doc_id, old_result.docs, old_file, \"docs\")\n            self.add_doc(source_file, doc)\n\n        for macro_id in old_file.macros:\n            macro = _expect_value(\n                macro_id, old_result.macros, old_file, \"macros\"\n            )\n            self.add_macro(source_file, macro)\n\n        for source_id in old_file.sources:\n            source = _expect_value(\n                source_id, old_result.sources, old_file, \"sources\"\n            )\n            self.add_source(source_file, source)\n\n        # because we know this is how we _parsed_ the node, we can safely\n        # assume if it's disabled it was done by the project or file, and\n        # we can keep our old data\n        for node_id in old_file.nodes:\n            if node_id in old_result.nodes:\n                node = old_result.nodes[node_id]\n                self.add_node(source_file, node)\n            elif node_id in old_result.disabled:\n                matches = old_result._get_disabled(node_id, source_file)\n                for match in matches:\n                    self.add_disabled(source_file, match)\n            else:\n                raise CompilationException(\n                    'Expected to find \"{}\" in cached \"manifest.nodes\" or '\n                    '\"manifest.disabled\" based on cached file information: {}!'\n                    .format(node_id, old_file)\n                )\n\n        for name in old_file.patches:\n            patch = _expect_value(\n                name, old_result.patches, old_file, \"patches\"\n            )\n            self.add_patch(source_file, patch)\n\n        return True\n\n    def has_file(self, source_file: SourceFile) -> bool:\n        key = source_file.search_key\n        if key is None:\n            return False\n        if key not in self.files:\n            return False\n        my_checksum = self.files[key].checksum\n        return my_checksum == source_file.checksum\n\n    @classmethod\n    def rpc(cls):\n        # ugh!\n        return cls(FileHash.empty(), FileHash.empty(), {})\n\n\nT = TypeVar('T')\n\n\ndef _expect_value(\n    key: str, src: Mapping[str, T], old_file: SourceFile, name: str\n) -> T:\n    if key not in src:\n        raise CompilationException(\n            'Expected to find \"{}\" in cached \"result.{}\" based '\n            'on cached file information: {}!'\n            .format(key, name, old_file)\n        )\n    return src[key]\n", "path": "core/dbt/parser/results.py"}], "after_files": [{"content": "from dataclasses import dataclass, field\nfrom typing import TypeVar, MutableMapping, Mapping, Union, List\n\nfrom hologram import JsonSchemaMixin\n\nfrom dbt.contracts.graph.manifest import SourceFile, RemoteFile, FileHash\nfrom dbt.contracts.graph.parsed import (\n    ParsedNode, HasUniqueID, ParsedMacro, ParsedDocumentation, ParsedNodePatch,\n    ParsedSourceDefinition, ParsedAnalysisNode, ParsedHookNode, ParsedRPCNode,\n    ParsedModelNode, ParsedSeedNode, ParsedTestNode, ParsedSnapshotNode,\n)\nfrom dbt.contracts.util import Writable, Replaceable\nfrom dbt.exceptions import (\n    raise_duplicate_resource_name, raise_duplicate_patch_name,\n    CompilationException, InternalException\n)\nfrom dbt.version import __version__\n\n\n# Parsers can return anything as long as it's a unique ID\nParsedValueType = TypeVar('ParsedValueType', bound=HasUniqueID)\n\n\ndef _check_duplicates(\n    value: HasUniqueID, src: Mapping[str, HasUniqueID]\n):\n    if value.unique_id in src:\n        raise_duplicate_resource_name(value, src[value.unique_id])\n\n\nManifestNodes = Union[\n    ParsedAnalysisNode,\n    ParsedHookNode,\n    ParsedModelNode,\n    ParsedSeedNode,\n    ParsedTestNode,\n    ParsedSnapshotNode,\n    ParsedRPCNode,\n]\n\n\ndef dict_field():\n    return field(default_factory=dict)\n\n\n@dataclass\nclass ParseResult(JsonSchemaMixin, Writable, Replaceable):\n    vars_hash: FileHash\n    profile_hash: FileHash\n    project_hashes: MutableMapping[str, FileHash]\n    nodes: MutableMapping[str, ManifestNodes] = dict_field()\n    sources: MutableMapping[str, ParsedSourceDefinition] = dict_field()\n    docs: MutableMapping[str, ParsedDocumentation] = dict_field()\n    macros: MutableMapping[str, ParsedMacro] = dict_field()\n    patches: MutableMapping[str, ParsedNodePatch] = dict_field()\n    files: MutableMapping[str, SourceFile] = dict_field()\n    disabled: MutableMapping[str, List[ParsedNode]] = dict_field()\n    dbt_version: str = __version__\n\n    def get_file(self, source_file: SourceFile) -> SourceFile:\n        key = source_file.search_key\n        if key is None:\n            return source_file\n        if key not in self.files:\n            self.files[key] = source_file\n        return self.files[key]\n\n    def add_source(\n        self, source_file: SourceFile, node: ParsedSourceDefinition\n    ):\n        # nodes can't be overwritten!\n        _check_duplicates(node, self.sources)\n        self.sources[node.unique_id] = node\n        self.get_file(source_file).sources.append(node.unique_id)\n\n    def add_node(self, source_file: SourceFile, node: ManifestNodes):\n        # nodes can't be overwritten!\n        _check_duplicates(node, self.nodes)\n        self.nodes[node.unique_id] = node\n        self.get_file(source_file).nodes.append(node.unique_id)\n\n    def add_disabled(self, source_file: SourceFile, node: ParsedNode):\n        if node.unique_id in self.disabled:\n            self.disabled[node.unique_id].append(node)\n        else:\n            self.disabled[node.unique_id] = [node]\n        self.get_file(source_file).nodes.append(node.unique_id)\n\n    def add_macro(self, source_file: SourceFile, macro: ParsedMacro):\n        # macros can be overwritten (should they be?)\n        self.macros[macro.unique_id] = macro\n        self.get_file(source_file).macros.append(macro.unique_id)\n\n    def add_doc(self, source_file: SourceFile, doc: ParsedDocumentation):\n        # Docs also can be overwritten (should they be?)\n        self.docs[doc.unique_id] = doc\n        self.get_file(source_file).docs.append(doc.unique_id)\n\n    def add_patch(self, source_file: SourceFile, patch: ParsedNodePatch):\n        # matches can't be overwritten\n        if patch.name in self.patches:\n            raise_duplicate_patch_name(patch.name, patch,\n                                       self.patches[patch.name])\n        self.patches[patch.name] = patch\n        self.get_file(source_file).patches.append(patch.name)\n\n    def _get_disabled(\n        self, unique_id: str, match_file: SourceFile\n    ) -> List[ParsedNode]:\n        if unique_id not in self.disabled:\n            raise InternalException(\n                'called _get_disabled with id={}, but it does not exist'\n                .format(unique_id)\n            )\n        return [\n            n for n in self.disabled[unique_id]\n            if n.original_file_path == match_file.path.original_file_path\n        ]\n\n    def _process_node(\n        self,\n        node_id: str,\n        source_file: SourceFile,\n        old_file: SourceFile,\n        old_result: 'ParseResult',\n    ) -> None:\n        \"\"\"Nodes are a special kind of complicated - there can be multiple\n        with the same name, as long as all but one are disabled.\n        \"\"\"\n        source_path = source_file.path.original_file_path\n        found: bool = False\n        if node_id in old_result.nodes:\n            old_node = old_result.nodes[node_id]\n            if old_node.original_file_path == source_path:\n                self.add_node(source_file, old_node)\n                found = True\n\n        if node_id in old_result.disabled:\n            matches = old_result._get_disabled(node_id, source_file)\n            for match in matches:\n                self.add_disabled(source_file, match)\n                found = True\n\n        if not found:\n            raise CompilationException(\n                'Expected to find \"{}\" in cached \"manifest.nodes\" or '\n                '\"manifest.disabled\" based on cached file information: {}!'\n                .format(node_id, old_file)\n            )\n\n    def sanitized_update(\n        self, source_file: SourceFile, old_result: 'ParseResult',\n    ) -> bool:\n        \"\"\"Perform a santized update. If the file can't be updated, invalidate\n        it and return false.\n        \"\"\"\n        if isinstance(source_file.path, RemoteFile):\n            return False\n\n        old_file = old_result.get_file(source_file)\n        for doc_id in old_file.docs:\n            doc = _expect_value(doc_id, old_result.docs, old_file, \"docs\")\n            self.add_doc(source_file, doc)\n\n        for macro_id in old_file.macros:\n            macro = _expect_value(\n                macro_id, old_result.macros, old_file, \"macros\"\n            )\n            self.add_macro(source_file, macro)\n\n        for source_id in old_file.sources:\n            source = _expect_value(\n                source_id, old_result.sources, old_file, \"sources\"\n            )\n            self.add_source(source_file, source)\n\n        # because we know this is how we _parsed_ the node, we can safely\n        # assume if it's disabled it was done by the project or file, and\n        # we can keep our old data\n        # the node ID could be in old_result.disabled AND in old_result.nodes.\n        # In that case, we have to make sure the path also matches.\n        for node_id in old_file.nodes:\n            self._process_node(node_id, source_file, old_file, old_result)\n\n        for name in old_file.patches:\n            patch = _expect_value(\n                name, old_result.patches, old_file, \"patches\"\n            )\n            self.add_patch(source_file, patch)\n\n        return True\n\n    def has_file(self, source_file: SourceFile) -> bool:\n        key = source_file.search_key\n        if key is None:\n            return False\n        if key not in self.files:\n            return False\n        my_checksum = self.files[key].checksum\n        return my_checksum == source_file.checksum\n\n    @classmethod\n    def rpc(cls):\n        # ugh!\n        return cls(FileHash.empty(), FileHash.empty(), {})\n\n\nT = TypeVar('T')\n\n\ndef _expect_value(\n    key: str, src: Mapping[str, T], old_file: SourceFile, name: str\n) -> T:\n    if key not in src:\n        raise CompilationException(\n            'Expected to find \"{}\" in cached \"result.{}\" based '\n            'on cached file information: {}!'\n            .format(key, name, old_file)\n        )\n    return src[key]\n", "path": "core/dbt/parser/results.py"}]}