pytrilogy 0.3.142__cp312-cp312-win_amd64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cp312-win_amd64.pyd +0 -0
- pytrilogy-0.3.142.dist-info/METADATA +555 -0
- pytrilogy-0.3.142.dist-info/RECORD +200 -0
- pytrilogy-0.3.142.dist-info/WHEEL +4 -0
- pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +16 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +113 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +443 -0
- trilogy/core/env_processor.py +120 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1227 -0
- trilogy/core/graph_models.py +139 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2669 -0
- trilogy/core/models/build.py +2521 -0
- trilogy/core/models/build_environment.py +180 -0
- trilogy/core/models/core.py +501 -0
- trilogy/core/models/datasource.py +322 -0
- trilogy/core/models/environment.py +751 -0
- trilogy/core/models/execute.py +1177 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +268 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +205 -0
- trilogy/core/processing/node_generators/node_merge_node.py +653 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +748 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +519 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +596 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1392 -0
- trilogy/dialect/bigquery.py +308 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +144 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +231 -0
- trilogy/dialect/enums.py +147 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/config.py +75 -0
- trilogy/executor.py +568 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +139 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2813 -0
- trilogy/parsing/render.py +769 -0
- trilogy/parsing/trilogy.lark +540 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +42 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +303 -0
- trilogy/scripts/common.py +355 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +303 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +512 -0
- trilogy/scripts/environment.py +46 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +471 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +713 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/run.py +63 -0
- trilogy/scripts/serve.py +140 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +119 -0
- trilogy/scripts/trilogy.py +68 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- trilogy/utility.py +34 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# PreQL Import Resolver
|
|
2
|
+
|
|
3
|
+
A Rust-based CLI tool and Python library for parsing PreQL (Trilogy) files and resolving import dependencies with ETL-aware dependency ordering.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- Parse PreQL files to extract imports, datasource declarations, and persist statements
|
|
8
|
+
- Resolve import dependencies transitively
|
|
9
|
+
- Build dependency graphs with ETL-aware ordering:
|
|
10
|
+
- Files that persist (write) to a datasource run before files that declare it, even if they import it.
|
|
11
|
+
- Standard import dependencies (imported files run before importing files)
|
|
12
|
+
|
|
13
|
+
Exit codes:
|
|
14
|
+
- `0`: Success
|
|
15
|
+
- `1`: Error (parse error, file not found, circular dependency, etc.)
|
|
16
|
+
|
|
17
|
+
## CLI Usage
|
|
18
|
+
|
|
19
|
+
### Parse a single file
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
preql-import-resolver parse path/to/file.preql --format pretty
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### Parse a directory
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
preql-import-resolver parse path/to/directory --recursive --format json
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Resolve dependencies
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
preql-import-resolver resolve path/to/file.preql --format pretty
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Analyze datasources
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
preql-import-resolver datasources path/to/directory --recursive
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Python Integration
|
|
44
|
+
|
|
45
|
+
The Rust resolver is integrated into the Python package via PyO3 and maturin.
|
|
46
|
+
|
|
47
|
+
### Building the Python Extension
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd trilogy/scripts/dependency
|
|
51
|
+
maturin develop # For development
|
|
52
|
+
# or
|
|
53
|
+
maturin build --release # For production
|
|
54
|
+
pip install target/wheels/*.whl
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Using in Python
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from trilogy.scripts.dependency import DependencyResolver, ETLDependencyStrategy, create_script_nodes
|
|
61
|
+
from pathlib import Path
|
|
62
|
+
|
|
63
|
+
# Create script nodes from files
|
|
64
|
+
files = [Path("model1.preql"), Path("model2.preql")]
|
|
65
|
+
nodes = create_script_nodes(files)
|
|
66
|
+
|
|
67
|
+
# Use the ETL dependency strategy (backed by Rust)
|
|
68
|
+
resolver = DependencyResolver(strategy=ETLDependencyStrategy())
|
|
69
|
+
graph = resolver.build_graph(nodes)
|
|
70
|
+
|
|
71
|
+
# Get execution order
|
|
72
|
+
import networkx as nx
|
|
73
|
+
execution_order = list(nx.topological_sort(graph))
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
The `ETLDependencyStrategy` uses the Rust-based resolver under the hood for fast, accurate dependency analysis based on:
|
|
77
|
+
- Import statements
|
|
78
|
+
- Datasource declarations
|
|
79
|
+
- Persist statements (append/overwrite/persist)
|
|
80
|
+
|
|
81
|
+
## Development
|
|
82
|
+
|
|
83
|
+
### Running Rust Tests
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
cd trilogy/scripts/dependency
|
|
87
|
+
cargo test
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
All tests include:
|
|
91
|
+
- Unit tests for parser (17 tests covering imports, datasources, persist statements)
|
|
92
|
+
- Unit tests for resolver (5 tests covering dependency resolution logic)
|
|
93
|
+
- Integration tests for CLI (12 tests covering all CLI commands)
|
|
94
|
+
|
|
95
|
+
### Building the CLI
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
cargo build --release
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The binary will be at `target/release/preql-import-resolver` (or `.exe` on Windows).
|
|
102
|
+
|
|
103
|
+
### Building for Python
|
|
104
|
+
|
|
105
|
+
Run maturin from the base of the pytrilogy repo. [not this directory.]
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
# Development mode (installs in current Python environment)
|
|
109
|
+
maturin develop
|
|
110
|
+
|
|
111
|
+
# Production build
|
|
112
|
+
maturin build --release
|
|
113
|
+
|
|
114
|
+
# The wheel will be in target/wheels/
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## Dependency Ordering Rules
|
|
118
|
+
|
|
119
|
+
The resolver implements three key dependency rules:
|
|
120
|
+
|
|
121
|
+
1. **Import Dependencies** : Imported files should run before importing files
|
|
122
|
+
2. **Persist-Before-Declare**: Files that persist to a datasource must run before files that declare it, even if they import that file.
|
|
123
|
+
|
|
124
|
+
### Edge Cases
|
|
125
|
+
|
|
126
|
+
- Case 1: file A imports from file B → B must run before A for all datasources in B
|
|
127
|
+
- Case 2: file A imports from file B, then updates datasource from file B → update takes precedence, so A runs before B.
|
|
128
|
+
|
|
129
|
+
## Grammar Limitations
|
|
130
|
+
|
|
131
|
+
The grammar is currently focused on dependency-relevant constructs (imports, datasources, persist statements). It does not parse all Trilogy syntax. It can be extended in the future.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Build script for preql-import-resolver
|
|
3
|
+
|
|
4
|
+
set -e
|
|
5
|
+
|
|
6
|
+
echo "Building preql-import-resolver..."
|
|
7
|
+
|
|
8
|
+
# Check for Rust
|
|
9
|
+
if ! command -v cargo &> /dev/null; then
|
|
10
|
+
echo "Rust is not installed. Install from https://rustup.rs/"
|
|
11
|
+
exit 1
|
|
12
|
+
fi
|
|
13
|
+
|
|
14
|
+
# Build release binary
|
|
15
|
+
cargo build --release
|
|
16
|
+
|
|
17
|
+
echo ""
|
|
18
|
+
echo "Build successful!"
|
|
19
|
+
echo "Binary location: ./target/release/preql-import-resolver"
|
|
20
|
+
echo ""
|
|
21
|
+
echo "To install globally, run:"
|
|
22
|
+
echo " cargo install --path ."
|
|
23
|
+
echo ""
|
|
24
|
+
echo "To test:"
|
|
25
|
+
echo " ./target/release/preql-import-resolver --help"
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
use crate::parser::parse_file;
|
|
2
|
+
use std::collections::{HashMap, HashSet};
|
|
3
|
+
use std::fs;
|
|
4
|
+
use std::path::PathBuf;
|
|
5
|
+
|
|
6
|
+
#[derive(Debug, Clone)]
|
|
7
|
+
pub struct FileInfo {
|
|
8
|
+
pub path: PathBuf,
|
|
9
|
+
pub datasources: Vec<String>,
|
|
10
|
+
pub persists: Vec<String>,
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
#[derive(Debug, Clone)]
|
|
14
|
+
pub struct DirectoryGraph {
|
|
15
|
+
pub files: HashMap<PathBuf, FileInfo>,
|
|
16
|
+
pub imports: HashMap<PathBuf, Vec<PathBuf>>,
|
|
17
|
+
pub warnings: Vec<String>,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
#[derive(Debug, Clone)]
|
|
21
|
+
pub struct Edge {
|
|
22
|
+
pub from: PathBuf,
|
|
23
|
+
pub to: PathBuf,
|
|
24
|
+
pub reason: EdgeReason,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
#[derive(Debug, Clone)]
|
|
28
|
+
pub enum EdgeReason {
|
|
29
|
+
Import,
|
|
30
|
+
PersistBeforeDeclare { datasource: String },
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/// Process files in a directory, discovering transitive imports
|
|
34
|
+
pub fn process_directory_with_imports(
|
|
35
|
+
initial_files: Vec<PathBuf>,
|
|
36
|
+
) -> Result<DirectoryGraph, String> {
|
|
37
|
+
let mut all_imports: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
|
|
38
|
+
let mut files_info: HashMap<PathBuf, FileInfo> = HashMap::new();
|
|
39
|
+
let mut files_to_process = initial_files;
|
|
40
|
+
let mut processed_files: HashSet<PathBuf> = HashSet::new();
|
|
41
|
+
let mut warnings = Vec::new();
|
|
42
|
+
|
|
43
|
+
while let Some(file) = files_to_process.pop() {
|
|
44
|
+
let canonical = match fs::canonicalize(&file) {
|
|
45
|
+
Ok(c) => c,
|
|
46
|
+
Err(e) => {
|
|
47
|
+
warnings.push(format!("Failed to canonicalize {}: {}", file.display(), e));
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
if processed_files.contains(&canonical) {
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
processed_files.insert(canonical.clone());
|
|
56
|
+
|
|
57
|
+
let content = match fs::read_to_string(&file) {
|
|
58
|
+
Ok(c) => c,
|
|
59
|
+
Err(e) => {
|
|
60
|
+
warnings.push(format!("Failed to read {}: {}", file.display(), e));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
let parsed = match parse_file(&content) {
|
|
66
|
+
Ok(p) => p,
|
|
67
|
+
Err(e) => {
|
|
68
|
+
warnings.push(format!("Failed to parse {}: {}", file.display(), e));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
let mut resolved_imports = Vec::new();
|
|
74
|
+
let file_dir = file.parent().unwrap_or(std::path::Path::new("."));
|
|
75
|
+
|
|
76
|
+
for import in &parsed.imports {
|
|
77
|
+
if import.is_stdlib {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if let Some(resolved) = import.resolve(file_dir) {
|
|
81
|
+
if resolved.exists() {
|
|
82
|
+
if let Ok(resolved_canonical) = fs::canonicalize(&resolved) {
|
|
83
|
+
resolved_imports.push(resolved_canonical.clone());
|
|
84
|
+
if !processed_files.contains(&resolved_canonical) {
|
|
85
|
+
files_to_process.push(resolved_canonical);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let datasources: Vec<String> = parsed.datasources.iter().map(|d| d.name.clone()).collect();
|
|
93
|
+
let persists: Vec<String> = parsed.persists.iter().map(|p| p.target_datasource.clone()).collect();
|
|
94
|
+
|
|
95
|
+
all_imports.insert(canonical.clone(), resolved_imports);
|
|
96
|
+
files_info.insert(
|
|
97
|
+
canonical.clone(),
|
|
98
|
+
FileInfo {
|
|
99
|
+
path: canonical,
|
|
100
|
+
datasources,
|
|
101
|
+
persists,
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
Ok(DirectoryGraph {
|
|
107
|
+
files: files_info,
|
|
108
|
+
imports: all_imports,
|
|
109
|
+
warnings,
|
|
110
|
+
})
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/// Build edges from a directory graph
|
|
114
|
+
pub fn build_edges(graph: &DirectoryGraph) -> Vec<Edge> {
|
|
115
|
+
let mut edges = Vec::new();
|
|
116
|
+
let known_files: HashSet<PathBuf> = graph.files.keys().cloned().collect();
|
|
117
|
+
|
|
118
|
+
// Rule 1: Import dependencies (imported files run before importing files)
|
|
119
|
+
for (file, imports) in &graph.imports {
|
|
120
|
+
for resolved_path in imports {
|
|
121
|
+
if !known_files.contains(resolved_path) {
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
edges.push(Edge {
|
|
125
|
+
from: resolved_path.clone(),
|
|
126
|
+
to: file.clone(),
|
|
127
|
+
reason: EdgeReason::Import,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Rule 2: Persist-before-declare
|
|
133
|
+
// Files that persist to a datasource must run BEFORE files that declare that datasource.
|
|
134
|
+
// This takes precedence over import edges - if the updater imports the declarer,
|
|
135
|
+
// we need to remove that import edge and add the persist-before-declare edge instead.
|
|
136
|
+
for (declarer_path, declarer_info) in &graph.files {
|
|
137
|
+
for ds_name in &declarer_info.datasources {
|
|
138
|
+
for (updater_path, updater_info) in &graph.files {
|
|
139
|
+
if updater_path == declarer_path {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if updater_info.persists.contains(ds_name) {
|
|
144
|
+
edges.push(Edge {
|
|
145
|
+
from: updater_path.clone(),
|
|
146
|
+
to: declarer_path.clone(),
|
|
147
|
+
reason: EdgeReason::PersistBeforeDeclare {
|
|
148
|
+
datasource: ds_name.clone(),
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Rule 3: Remove import edges that conflict with persist-before-declare
|
|
157
|
+
// If file A imports file B, but A also persists to a datasource declared by B,
|
|
158
|
+
// then the import edge (B -> A) conflicts with persist-before-declare (A -> B).
|
|
159
|
+
// In this case, persist-before-declare takes precedence.
|
|
160
|
+
let persist_edges: HashSet<(PathBuf, PathBuf)> = edges
|
|
161
|
+
.iter()
|
|
162
|
+
.filter(|e| matches!(e.reason, EdgeReason::PersistBeforeDeclare { .. }))
|
|
163
|
+
.map(|e| (e.from.clone(), e.to.clone()))
|
|
164
|
+
.collect();
|
|
165
|
+
|
|
166
|
+
edges.retain(|edge| {
|
|
167
|
+
if matches!(edge.reason, EdgeReason::Import) {
|
|
168
|
+
// Check if there's a conflicting persist-before-declare edge in the opposite direction
|
|
169
|
+
let reverse = (edge.to.clone(), edge.from.clone());
|
|
170
|
+
!persist_edges.contains(&reverse)
|
|
171
|
+
} else {
|
|
172
|
+
true
|
|
173
|
+
}
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
edges
|
|
177
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
mod parser;
|
|
2
|
+
mod resolver;
|
|
3
|
+
mod directory_resolver;
|
|
4
|
+
pub mod python_bindings;
|
|
5
|
+
|
|
6
|
+
pub use parser::{
|
|
7
|
+
parse_file, parse_imports, DatasourceDeclaration, ImportStatement, ParseError, ParsedFile,
|
|
8
|
+
PersistMode, PersistStatement,
|
|
9
|
+
};
|
|
10
|
+
pub use resolver::{
|
|
11
|
+
DatasourceInfo, DependencyGraph, FileNode, ImportInfo, ImportResolver, PersistInfo,
|
|
12
|
+
ResolveError,
|
|
13
|
+
};
|
|
14
|
+
pub use directory_resolver::{
|
|
15
|
+
process_directory_with_imports, build_edges, DirectoryGraph, Edge, EdgeReason, FileInfo,
|
|
16
|
+
};
|