pytrilogy 0.3.138__cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.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.cpython-311-x86_64-linux-gnu.so +0 -0
- pytrilogy-0.3.138.dist-info/METADATA +525 -0
- pytrilogy-0.3.138.dist-info/RECORD +182 -0
- pytrilogy-0.3.138.dist-info/WHEEL +5 -0
- pytrilogy-0.3.138.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.138.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +9 -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 +87 -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 +143 -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 +2672 -0
- trilogy/core/models/build.py +2521 -0
- trilogy/core/models/build_environment.py +180 -0
- trilogy/core/models/core.py +494 -0
- trilogy/core/models/datasource.py +322 -0
- trilogy/core/models/environment.py +748 -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 +517 -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 +106 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1359 -0
- trilogy/dialect/bigquery.py +256 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +144 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +177 -0
- trilogy/dialect/enums.py +147 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +91 -0
- trilogy/dialect/presto.py +104 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +90 -0
- trilogy/dialect/sql_server.py +92 -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 +750 -0
- trilogy/parsing/trilogy.lark +540 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +42 -0
- trilogy/scripts/README.md +7 -0
- trilogy/scripts/__init__.py +0 -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 +162 -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 +289 -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 +460 -0
- trilogy/scripts/environment.py +46 -0
- trilogy/scripts/parallel_execution.py +483 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/trilogy.py +772 -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,770 @@
|
|
|
1
|
+
use clap::{Parser, Subcommand, ValueEnum};
|
|
2
|
+
use preql_import_resolver::{parse_file, ImportResolver, ParsedFile};
|
|
3
|
+
use serde::Serialize;
|
|
4
|
+
use std::collections::{BTreeMap, HashMap};
|
|
5
|
+
use std::path::PathBuf;
|
|
6
|
+
use std::process::ExitCode;
|
|
7
|
+
|
|
8
|
+
#[derive(Parser)]
|
|
9
|
+
#[command(name = "preql-import-resolver")]
|
|
10
|
+
#[command(author, version, about = "Parse PreQL files and resolve import/datasource dependencies")]
|
|
11
|
+
struct Cli {
|
|
12
|
+
#[command(subcommand)]
|
|
13
|
+
command: Commands,
|
|
14
|
+
/// Output format
|
|
15
|
+
#[arg(long, short, default_value = "json", global = true)]
|
|
16
|
+
format: OutputFormat,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
#[derive(Subcommand)]
|
|
20
|
+
enum Commands {
|
|
21
|
+
/// Parse a file or directory and list imports, datasources, and persist statements
|
|
22
|
+
Parse {
|
|
23
|
+
/// Path to a PreQL file or directory containing PreQL files
|
|
24
|
+
path: PathBuf,
|
|
25
|
+
/// Recursively search directories
|
|
26
|
+
#[arg(long, short)]
|
|
27
|
+
recursive: bool,
|
|
28
|
+
/// Only show direct imports (don't resolve transitive dependencies)
|
|
29
|
+
#[arg(long)]
|
|
30
|
+
direct_only: bool,
|
|
31
|
+
},
|
|
32
|
+
/// Resolve all dependencies from a root file or directory with datasource-aware ordering
|
|
33
|
+
Resolve {
|
|
34
|
+
/// Path to a PreQL file or directory containing PreQL files
|
|
35
|
+
path: PathBuf,
|
|
36
|
+
/// Only output the dependency order (list of paths)
|
|
37
|
+
#[arg(long)]
|
|
38
|
+
order_only: bool,
|
|
39
|
+
/// Recursively search directories
|
|
40
|
+
#[arg(long, short)]
|
|
41
|
+
recursive: bool,
|
|
42
|
+
},
|
|
43
|
+
/// Analyze datasources in a file or directory
|
|
44
|
+
Datasources {
|
|
45
|
+
/// Path to a PreQL file or directory
|
|
46
|
+
path: PathBuf,
|
|
47
|
+
/// Recursively search directories
|
|
48
|
+
#[arg(long, short)]
|
|
49
|
+
recursive: bool,
|
|
50
|
+
},
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
#[derive(Copy, Clone, PartialEq, Eq, ValueEnum)]
|
|
54
|
+
enum OutputFormat {
|
|
55
|
+
Json,
|
|
56
|
+
Pretty,
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
#[derive(Serialize)]
|
|
60
|
+
struct ParseOutput {
|
|
61
|
+
file: PathBuf,
|
|
62
|
+
imports: Vec<ImportOutput>,
|
|
63
|
+
datasources: Vec<DatasourceOutput>,
|
|
64
|
+
persists: Vec<PersistOutput>,
|
|
65
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
66
|
+
resolved_dependencies: Option<Vec<PathBuf>>,
|
|
67
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
68
|
+
warnings: Vec<String>,
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
#[derive(Serialize)]
|
|
72
|
+
struct DirectoryParseOutput {
|
|
73
|
+
files: BTreeMap<PathBuf, FileParseResult>,
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
#[derive(Serialize)]
|
|
77
|
+
#[serde(untagged)]
|
|
78
|
+
enum FileParseResult {
|
|
79
|
+
Success {
|
|
80
|
+
imports: Vec<ImportOutput>,
|
|
81
|
+
datasources: Vec<DatasourceOutput>,
|
|
82
|
+
persists: Vec<PersistOutput>,
|
|
83
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
84
|
+
resolved_dependencies: Option<Vec<PathBuf>>,
|
|
85
|
+
#[serde(skip_serializing_if = "Vec::is_empty")]
|
|
86
|
+
warnings: Vec<String>,
|
|
87
|
+
},
|
|
88
|
+
Error {
|
|
89
|
+
error: String,
|
|
90
|
+
},
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
#[derive(Serialize)]
|
|
94
|
+
struct ImportOutput {
|
|
95
|
+
raw_path: String,
|
|
96
|
+
alias: Option<String>,
|
|
97
|
+
is_stdlib: bool,
|
|
98
|
+
parent_dirs: usize,
|
|
99
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
100
|
+
resolved_path: Option<PathBuf>,
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
#[derive(Serialize)]
|
|
104
|
+
struct DatasourceOutput {
|
|
105
|
+
name: String,
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
#[derive(Serialize)]
|
|
109
|
+
struct PersistOutput {
|
|
110
|
+
mode: String,
|
|
111
|
+
target_datasource: String,
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#[derive(Serialize)]
|
|
115
|
+
struct DatasourceAnalysis {
|
|
116
|
+
/// All datasources found with their declaring files
|
|
117
|
+
declarations: BTreeMap<String, PathBuf>,
|
|
118
|
+
/// Datasources and the files that update them
|
|
119
|
+
updaters: BTreeMap<String, Vec<PathBuf>>,
|
|
120
|
+
/// Files that depend on each datasource (through imports)
|
|
121
|
+
dependents: BTreeMap<String, Vec<PathBuf>>,
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
#[derive(Serialize)]
|
|
125
|
+
struct ErrorOutput {
|
|
126
|
+
error: String,
|
|
127
|
+
#[serde(skip_serializing_if = "Option::is_none")]
|
|
128
|
+
file: Option<PathBuf>,
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
fn main() -> ExitCode {
|
|
132
|
+
let cli = Cli::parse();
|
|
133
|
+
|
|
134
|
+
let result = match &cli.command {
|
|
135
|
+
Commands::Parse {
|
|
136
|
+
path,
|
|
137
|
+
recursive,
|
|
138
|
+
direct_only,
|
|
139
|
+
} => handle_parse(path, *recursive, *direct_only, cli.format),
|
|
140
|
+
Commands::Resolve { path, order_only, recursive } => handle_resolve(path, *order_only, *recursive, cli.format),
|
|
141
|
+
Commands::Datasources { path, recursive } => {
|
|
142
|
+
handle_datasources(path, *recursive, cli.format)
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
match result {
|
|
147
|
+
Ok(_) => ExitCode::SUCCESS,
|
|
148
|
+
Err(e) => {
|
|
149
|
+
let error_output = ErrorOutput {
|
|
150
|
+
error: e.to_string(),
|
|
151
|
+
file: None,
|
|
152
|
+
};
|
|
153
|
+
match cli.format {
|
|
154
|
+
OutputFormat::Json => {
|
|
155
|
+
eprintln!("{}", serde_json::to_string(&error_output).unwrap());
|
|
156
|
+
}
|
|
157
|
+
OutputFormat::Pretty => {
|
|
158
|
+
eprintln!("{}", serde_json::to_string_pretty(&error_output).unwrap());
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
ExitCode::FAILURE
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
fn handle_parse(
|
|
167
|
+
path: &PathBuf,
|
|
168
|
+
recursive: bool,
|
|
169
|
+
direct_only: bool,
|
|
170
|
+
format: OutputFormat,
|
|
171
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
172
|
+
if path.is_file() {
|
|
173
|
+
handle_parse_file(path, direct_only, format)
|
|
174
|
+
} else if path.is_dir() {
|
|
175
|
+
handle_parse_directory(path, recursive, direct_only, format)
|
|
176
|
+
} else {
|
|
177
|
+
Err(format!("Path does not exist or is not accessible: {}", path.display()).into())
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
fn handle_parse_file(
|
|
182
|
+
file: &PathBuf,
|
|
183
|
+
direct_only: bool,
|
|
184
|
+
format: OutputFormat,
|
|
185
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
186
|
+
let content = std::fs::read_to_string(file)?;
|
|
187
|
+
let parsed = parse_file(&content)?;
|
|
188
|
+
|
|
189
|
+
let (resolved_dependencies, warnings) = if direct_only {
|
|
190
|
+
(None, Vec::new())
|
|
191
|
+
} else {
|
|
192
|
+
let mut resolver = ImportResolver::new();
|
|
193
|
+
match resolver.resolve(file) {
|
|
194
|
+
Ok(graph) => {
|
|
195
|
+
let deps: Vec<PathBuf> = graph
|
|
196
|
+
.order
|
|
197
|
+
.into_iter()
|
|
198
|
+
.filter(|p| p != &graph.root)
|
|
199
|
+
.collect();
|
|
200
|
+
(
|
|
201
|
+
if deps.is_empty() { None } else { Some(deps) },
|
|
202
|
+
graph.warnings,
|
|
203
|
+
)
|
|
204
|
+
}
|
|
205
|
+
Err(e) => (None, vec![format!("Dependency resolution failed: {}", e)]),
|
|
206
|
+
}
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
let import_outputs: Vec<ImportOutput> = if direct_only {
|
|
210
|
+
parsed
|
|
211
|
+
.imports
|
|
212
|
+
.into_iter()
|
|
213
|
+
.map(|i| ImportOutput {
|
|
214
|
+
raw_path: i.raw_path,
|
|
215
|
+
alias: i.alias,
|
|
216
|
+
is_stdlib: i.is_stdlib,
|
|
217
|
+
parent_dirs: i.parent_dirs,
|
|
218
|
+
resolved_path: None,
|
|
219
|
+
})
|
|
220
|
+
.collect()
|
|
221
|
+
} else {
|
|
222
|
+
let file_dir = file.parent().unwrap_or(std::path::Path::new("."));
|
|
223
|
+
parsed
|
|
224
|
+
.imports
|
|
225
|
+
.into_iter()
|
|
226
|
+
.map(|i| {
|
|
227
|
+
let resolved_path = if i.is_stdlib {
|
|
228
|
+
None
|
|
229
|
+
} else {
|
|
230
|
+
i.resolve(file_dir).and_then(|p| {
|
|
231
|
+
if p.exists() {
|
|
232
|
+
std::fs::canonicalize(&p).ok()
|
|
233
|
+
} else {
|
|
234
|
+
None
|
|
235
|
+
}
|
|
236
|
+
})
|
|
237
|
+
};
|
|
238
|
+
ImportOutput {
|
|
239
|
+
raw_path: i.raw_path,
|
|
240
|
+
alias: i.alias,
|
|
241
|
+
is_stdlib: i.is_stdlib,
|
|
242
|
+
parent_dirs: i.parent_dirs,
|
|
243
|
+
resolved_path,
|
|
244
|
+
}
|
|
245
|
+
})
|
|
246
|
+
.collect()
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
let datasource_outputs: Vec<DatasourceOutput> = parsed
|
|
250
|
+
.datasources
|
|
251
|
+
.into_iter()
|
|
252
|
+
.map(|d| DatasourceOutput { name: d.name })
|
|
253
|
+
.collect();
|
|
254
|
+
|
|
255
|
+
let persist_outputs: Vec<PersistOutput> = parsed
|
|
256
|
+
.persists
|
|
257
|
+
.into_iter()
|
|
258
|
+
.map(|p| PersistOutput {
|
|
259
|
+
mode: p.mode.to_string(),
|
|
260
|
+
target_datasource: p.target_datasource,
|
|
261
|
+
})
|
|
262
|
+
.collect();
|
|
263
|
+
|
|
264
|
+
let output = ParseOutput {
|
|
265
|
+
file: file.clone(),
|
|
266
|
+
imports: import_outputs,
|
|
267
|
+
datasources: datasource_outputs,
|
|
268
|
+
persists: persist_outputs,
|
|
269
|
+
resolved_dependencies,
|
|
270
|
+
warnings,
|
|
271
|
+
};
|
|
272
|
+
|
|
273
|
+
match format {
|
|
274
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
|
|
275
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&output)?),
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
Ok(())
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
fn handle_parse_directory(
|
|
282
|
+
dir: &PathBuf,
|
|
283
|
+
recursive: bool,
|
|
284
|
+
direct_only: bool,
|
|
285
|
+
format: OutputFormat,
|
|
286
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
287
|
+
let files = collect_preql_files(dir, recursive)?;
|
|
288
|
+
let mut results: BTreeMap<PathBuf, FileParseResult> = BTreeMap::new();
|
|
289
|
+
|
|
290
|
+
for file in files {
|
|
291
|
+
let result = match std::fs::read_to_string(&file) {
|
|
292
|
+
Ok(content) => match parse_file(&content) {
|
|
293
|
+
Ok(parsed) => {
|
|
294
|
+
let (resolved_dependencies, warnings) = if direct_only {
|
|
295
|
+
(None, Vec::new())
|
|
296
|
+
} else {
|
|
297
|
+
let mut resolver = ImportResolver::new();
|
|
298
|
+
match resolver.resolve(&file) {
|
|
299
|
+
Ok(graph) => {
|
|
300
|
+
let deps: Vec<PathBuf> = graph
|
|
301
|
+
.order
|
|
302
|
+
.into_iter()
|
|
303
|
+
.filter(|p| p != &graph.root)
|
|
304
|
+
.collect();
|
|
305
|
+
(
|
|
306
|
+
if deps.is_empty() { None } else { Some(deps) },
|
|
307
|
+
graph.warnings,
|
|
308
|
+
)
|
|
309
|
+
}
|
|
310
|
+
Err(e) => (None, vec![format!("Dependency resolution failed: {}", e)]),
|
|
311
|
+
}
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
let import_outputs: Vec<ImportOutput> = if direct_only {
|
|
315
|
+
parsed
|
|
316
|
+
.imports
|
|
317
|
+
.into_iter()
|
|
318
|
+
.map(|i| ImportOutput {
|
|
319
|
+
raw_path: i.raw_path,
|
|
320
|
+
alias: i.alias,
|
|
321
|
+
is_stdlib: i.is_stdlib,
|
|
322
|
+
parent_dirs: i.parent_dirs,
|
|
323
|
+
resolved_path: None,
|
|
324
|
+
})
|
|
325
|
+
.collect()
|
|
326
|
+
} else {
|
|
327
|
+
let file_dir = file.parent().unwrap_or(std::path::Path::new("."));
|
|
328
|
+
parsed
|
|
329
|
+
.imports
|
|
330
|
+
.into_iter()
|
|
331
|
+
.map(|i| {
|
|
332
|
+
let resolved_path = if i.is_stdlib {
|
|
333
|
+
None
|
|
334
|
+
} else {
|
|
335
|
+
i.resolve(file_dir).and_then(|p| {
|
|
336
|
+
if p.exists() {
|
|
337
|
+
std::fs::canonicalize(&p).ok()
|
|
338
|
+
} else {
|
|
339
|
+
None
|
|
340
|
+
}
|
|
341
|
+
})
|
|
342
|
+
};
|
|
343
|
+
ImportOutput {
|
|
344
|
+
raw_path: i.raw_path,
|
|
345
|
+
alias: i.alias,
|
|
346
|
+
is_stdlib: i.is_stdlib,
|
|
347
|
+
parent_dirs: i.parent_dirs,
|
|
348
|
+
resolved_path,
|
|
349
|
+
}
|
|
350
|
+
})
|
|
351
|
+
.collect()
|
|
352
|
+
};
|
|
353
|
+
|
|
354
|
+
let datasource_outputs: Vec<DatasourceOutput> = parsed
|
|
355
|
+
.datasources
|
|
356
|
+
.into_iter()
|
|
357
|
+
.map(|d| DatasourceOutput { name: d.name })
|
|
358
|
+
.collect();
|
|
359
|
+
|
|
360
|
+
let persist_outputs: Vec<PersistOutput> = parsed
|
|
361
|
+
.persists
|
|
362
|
+
.into_iter()
|
|
363
|
+
.map(|p| PersistOutput {
|
|
364
|
+
mode: p.mode.to_string(),
|
|
365
|
+
target_datasource: p.target_datasource,
|
|
366
|
+
})
|
|
367
|
+
.collect();
|
|
368
|
+
|
|
369
|
+
FileParseResult::Success {
|
|
370
|
+
imports: import_outputs,
|
|
371
|
+
datasources: datasource_outputs,
|
|
372
|
+
persists: persist_outputs,
|
|
373
|
+
resolved_dependencies,
|
|
374
|
+
warnings,
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
Err(e) => FileParseResult::Error {
|
|
378
|
+
error: e.to_string(),
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
Err(e) => FileParseResult::Error {
|
|
382
|
+
error: e.to_string(),
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
let relative_path = file
|
|
387
|
+
.strip_prefix(dir)
|
|
388
|
+
.map(|p| p.to_path_buf())
|
|
389
|
+
.unwrap_or(file);
|
|
390
|
+
results.insert(relative_path, result);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
let output = DirectoryParseOutput { files: results };
|
|
394
|
+
|
|
395
|
+
match format {
|
|
396
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
|
|
397
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&output)?),
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
Ok(())
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
fn handle_datasources(
|
|
404
|
+
path: &PathBuf,
|
|
405
|
+
recursive: bool,
|
|
406
|
+
format: OutputFormat,
|
|
407
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
408
|
+
let files = if path.is_file() {
|
|
409
|
+
vec![path.clone()]
|
|
410
|
+
} else if path.is_dir() {
|
|
411
|
+
collect_preql_files(path, recursive)?
|
|
412
|
+
} else {
|
|
413
|
+
return Err(format!("Path does not exist: {}", path.display()).into());
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
let mut declarations: BTreeMap<String, PathBuf> = BTreeMap::new();
|
|
417
|
+
let mut updaters: BTreeMap<String, Vec<PathBuf>> = BTreeMap::new();
|
|
418
|
+
let mut all_files_parsed: Vec<(PathBuf, ParsedFile)> = Vec::new();
|
|
419
|
+
|
|
420
|
+
// First pass: collect all declarations and updaters
|
|
421
|
+
for file in &files {
|
|
422
|
+
let content = std::fs::read_to_string(file)?;
|
|
423
|
+
if let Ok(parsed) = parse_file(&content) {
|
|
424
|
+
for ds in &parsed.datasources {
|
|
425
|
+
declarations.insert(ds.name.clone(), file.clone());
|
|
426
|
+
}
|
|
427
|
+
for persist in &parsed.persists {
|
|
428
|
+
updaters
|
|
429
|
+
.entry(persist.target_datasource.clone())
|
|
430
|
+
.or_default()
|
|
431
|
+
.push(file.clone());
|
|
432
|
+
}
|
|
433
|
+
all_files_parsed.push((file.clone(), parsed));
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
// Second pass: find dependents (files that import files containing datasources)
|
|
438
|
+
let mut dependents: BTreeMap<String, Vec<PathBuf>> = BTreeMap::new();
|
|
439
|
+
|
|
440
|
+
for (file, parsed) in &all_files_parsed {
|
|
441
|
+
let file_dir = file.parent().unwrap_or(std::path::Path::new("."));
|
|
442
|
+
|
|
443
|
+
for import in &parsed.imports {
|
|
444
|
+
if import.is_stdlib {
|
|
445
|
+
continue;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
if let Some(resolved) = import.resolve(file_dir) {
|
|
449
|
+
if resolved.exists() {
|
|
450
|
+
if let Ok(canonical) = std::fs::canonicalize(&resolved) {
|
|
451
|
+
// Check what datasources are declared in the imported file
|
|
452
|
+
for (ds_name, declaring_file) in &declarations {
|
|
453
|
+
if let Ok(decl_canonical) = std::fs::canonicalize(declaring_file) {
|
|
454
|
+
if canonical == decl_canonical {
|
|
455
|
+
dependents
|
|
456
|
+
.entry(ds_name.clone())
|
|
457
|
+
.or_default()
|
|
458
|
+
.push(file.clone());
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
let analysis = DatasourceAnalysis {
|
|
469
|
+
declarations,
|
|
470
|
+
updaters,
|
|
471
|
+
dependents,
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
match format {
|
|
475
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&analysis)?),
|
|
476
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&analysis)?),
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
Ok(())
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
fn collect_preql_files(dir: &PathBuf, recursive: bool) -> Result<Vec<PathBuf>, std::io::Error> {
|
|
483
|
+
let mut files = Vec::new();
|
|
484
|
+
|
|
485
|
+
if recursive {
|
|
486
|
+
collect_preql_files_recursive(dir, &mut files)?;
|
|
487
|
+
} else {
|
|
488
|
+
for entry in std::fs::read_dir(dir)? {
|
|
489
|
+
let entry = entry?;
|
|
490
|
+
let path = entry.path();
|
|
491
|
+
if path.is_file() && is_preql_file(&path) {
|
|
492
|
+
files.push(path);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
files.sort();
|
|
498
|
+
Ok(files)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
fn collect_preql_files_recursive(
|
|
502
|
+
dir: &PathBuf,
|
|
503
|
+
files: &mut Vec<PathBuf>,
|
|
504
|
+
) -> Result<(), std::io::Error> {
|
|
505
|
+
for entry in std::fs::read_dir(dir)? {
|
|
506
|
+
let entry = entry?;
|
|
507
|
+
let path = entry.path();
|
|
508
|
+
|
|
509
|
+
if path.is_dir() {
|
|
510
|
+
collect_preql_files_recursive(&path, files)?;
|
|
511
|
+
} else if path.is_file() && is_preql_file(&path) {
|
|
512
|
+
files.push(path);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
Ok(())
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fn is_preql_file(path: &PathBuf) -> bool {
|
|
520
|
+
path.extension().is_some_and(|ext| ext == "preql")
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
fn handle_resolve(
|
|
524
|
+
path: &PathBuf,
|
|
525
|
+
order_only: bool,
|
|
526
|
+
recursive: bool,
|
|
527
|
+
format: OutputFormat,
|
|
528
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
529
|
+
if path.is_file() {
|
|
530
|
+
handle_resolve_file(path, order_only, format)
|
|
531
|
+
} else if path.is_dir() {
|
|
532
|
+
handle_resolve_directory(path, order_only, recursive, format)
|
|
533
|
+
} else {
|
|
534
|
+
Err(format!("Path does not exist or is not accessible: {}", path.display()).into())
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
fn handle_resolve_file(
|
|
539
|
+
file: &PathBuf,
|
|
540
|
+
order_only: bool,
|
|
541
|
+
format: OutputFormat,
|
|
542
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
543
|
+
let mut resolver = ImportResolver::new();
|
|
544
|
+
let graph = resolver.resolve(file)?;
|
|
545
|
+
|
|
546
|
+
if order_only {
|
|
547
|
+
let paths: Vec<&PathBuf> = graph.order.iter().collect();
|
|
548
|
+
match format {
|
|
549
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&paths)?),
|
|
550
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&paths)?),
|
|
551
|
+
}
|
|
552
|
+
} else {
|
|
553
|
+
match format {
|
|
554
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&graph)?),
|
|
555
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&graph)?),
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
Ok(())
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
fn handle_resolve_directory(
|
|
563
|
+
dir: &PathBuf,
|
|
564
|
+
order_only: bool,
|
|
565
|
+
recursive: bool,
|
|
566
|
+
format: OutputFormat,
|
|
567
|
+
) -> Result<(), Box<dyn std::error::Error>> {
|
|
568
|
+
use std::collections::HashSet;
|
|
569
|
+
|
|
570
|
+
let files = collect_preql_files(dir, recursive)?;
|
|
571
|
+
|
|
572
|
+
if files.is_empty() {
|
|
573
|
+
return Err(format!("No .preql files found in {}", dir.display()).into());
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
#[derive(Serialize, Clone)]
|
|
577
|
+
struct FileInfo {
|
|
578
|
+
path: PathBuf,
|
|
579
|
+
imports: Vec<String>,
|
|
580
|
+
datasources: Vec<String>,
|
|
581
|
+
persists: Vec<PersistInfo>,
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
#[derive(Serialize, Clone)]
|
|
585
|
+
struct PersistInfo {
|
|
586
|
+
mode: String,
|
|
587
|
+
target: String,
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
#[derive(Serialize, Clone)]
|
|
591
|
+
struct Edge {
|
|
592
|
+
from: PathBuf,
|
|
593
|
+
to: PathBuf,
|
|
594
|
+
reason: EdgeReason,
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
#[derive(Serialize, Clone)]
|
|
598
|
+
#[serde(tag = "type")]
|
|
599
|
+
enum EdgeReason {
|
|
600
|
+
#[serde(rename = "declare_before_use")]
|
|
601
|
+
DeclareBeforeUse { datasource: String },
|
|
602
|
+
#[serde(rename = "persist_before_declare")]
|
|
603
|
+
PersistBeforeDeclare { datasource: String },
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
#[derive(Serialize)]
|
|
607
|
+
struct GraphOutput {
|
|
608
|
+
directory: PathBuf,
|
|
609
|
+
files: Vec<PathBuf>,
|
|
610
|
+
edges: Vec<Edge>,
|
|
611
|
+
warnings: Vec<String>,
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
let mut files_info: HashMap<PathBuf, FileInfo> = HashMap::new();
|
|
615
|
+
let mut all_imports: HashMap<PathBuf, Vec<PathBuf>> = HashMap::new();
|
|
616
|
+
let mut warnings: Vec<String> = Vec::new();
|
|
617
|
+
let mut edges: Vec<Edge> = Vec::new();
|
|
618
|
+
|
|
619
|
+
// First pass: parse all files and collect info
|
|
620
|
+
for file in &files {
|
|
621
|
+
let canonical = match std::fs::canonicalize(file) {
|
|
622
|
+
Ok(c) => c,
|
|
623
|
+
Err(e) => {
|
|
624
|
+
warnings.push(format!("Failed to canonicalize {}: {}", file.display(), e));
|
|
625
|
+
continue;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
let content = std::fs::read_to_string(file)?;
|
|
630
|
+
match parse_file(&content) {
|
|
631
|
+
Ok(parsed) => {
|
|
632
|
+
let mut resolved_imports: Vec<PathBuf> = Vec::new();
|
|
633
|
+
|
|
634
|
+
// Resolve imports
|
|
635
|
+
let file_dir = file.parent().unwrap_or(std::path::Path::new("."));
|
|
636
|
+
for import in &parsed.imports {
|
|
637
|
+
if import.is_stdlib {
|
|
638
|
+
continue;
|
|
639
|
+
}
|
|
640
|
+
if let Some(resolved) = import.resolve(file_dir) {
|
|
641
|
+
if resolved.exists() {
|
|
642
|
+
if let Ok(resolved_canonical) = std::fs::canonicalize(&resolved) {
|
|
643
|
+
resolved_imports.push(resolved_canonical);
|
|
644
|
+
}
|
|
645
|
+
} else {
|
|
646
|
+
warnings.push(format!(
|
|
647
|
+
"Import '{}' in {} resolved to non-existent file: {}",
|
|
648
|
+
import.raw_path,
|
|
649
|
+
file.display(),
|
|
650
|
+
resolved.display()
|
|
651
|
+
));
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Collect datasource names
|
|
657
|
+
let datasource_names: Vec<String> = parsed
|
|
658
|
+
.datasources
|
|
659
|
+
.iter()
|
|
660
|
+
.map(|d| d.name.clone())
|
|
661
|
+
.collect();
|
|
662
|
+
|
|
663
|
+
// Collect persist info
|
|
664
|
+
let persist_infos: Vec<PersistInfo> = parsed
|
|
665
|
+
.persists
|
|
666
|
+
.iter()
|
|
667
|
+
.map(|p| PersistInfo {
|
|
668
|
+
mode: p.mode.to_string(),
|
|
669
|
+
target: p.target_datasource.clone(),
|
|
670
|
+
})
|
|
671
|
+
.collect();
|
|
672
|
+
|
|
673
|
+
let import_names: Vec<String> = parsed
|
|
674
|
+
.imports
|
|
675
|
+
.iter()
|
|
676
|
+
.map(|i| i.raw_path.clone())
|
|
677
|
+
.collect();
|
|
678
|
+
|
|
679
|
+
all_imports.insert(canonical.clone(), resolved_imports);
|
|
680
|
+
files_info.insert(
|
|
681
|
+
canonical.clone(),
|
|
682
|
+
FileInfo {
|
|
683
|
+
path: canonical,
|
|
684
|
+
imports: import_names,
|
|
685
|
+
datasources: datasource_names,
|
|
686
|
+
persists: persist_infos,
|
|
687
|
+
},
|
|
688
|
+
);
|
|
689
|
+
}
|
|
690
|
+
Err(e) => {
|
|
691
|
+
warnings.push(format!("Failed to parse {}: {}", file.display(), e));
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
let known_files: HashSet<PathBuf> = files_info.keys().cloned().collect();
|
|
697
|
+
|
|
698
|
+
// Build edges based on direct imports only
|
|
699
|
+
for (file, imports) in &all_imports {
|
|
700
|
+
// Get datasources that this file persists to
|
|
701
|
+
let persisted_datasources: HashSet<String> = files_info
|
|
702
|
+
.get(file)
|
|
703
|
+
.map(|info| info.persists.iter().map(|p| p.target.clone()).collect())
|
|
704
|
+
.unwrap_or_default();
|
|
705
|
+
|
|
706
|
+
for resolved_path in imports {
|
|
707
|
+
if !known_files.contains(resolved_path) {
|
|
708
|
+
continue;
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
// Get datasources declared in the imported file
|
|
712
|
+
let imported_datasources: Vec<String> = files_info
|
|
713
|
+
.get(resolved_path)
|
|
714
|
+
.map(|info| info.datasources.clone())
|
|
715
|
+
.unwrap_or_default();
|
|
716
|
+
|
|
717
|
+
for ds_name in imported_datasources {
|
|
718
|
+
if persisted_datasources.contains(&ds_name) {
|
|
719
|
+
// Case 2: file imports then updates datasource
|
|
720
|
+
// -> file must run before imported file (to update before re-declare)
|
|
721
|
+
edges.push(Edge {
|
|
722
|
+
from: file.clone(),
|
|
723
|
+
to: resolved_path.clone(),
|
|
724
|
+
reason: EdgeReason::PersistBeforeDeclare {
|
|
725
|
+
datasource: ds_name,
|
|
726
|
+
},
|
|
727
|
+
});
|
|
728
|
+
} else {
|
|
729
|
+
// Case 1: file imports datasource (read-only)
|
|
730
|
+
// -> imported file must run before this file
|
|
731
|
+
edges.push(Edge {
|
|
732
|
+
from: resolved_path.clone(),
|
|
733
|
+
to: file.clone(),
|
|
734
|
+
reason: EdgeReason::DeclareBeforeUse { datasource: ds_name },
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Deduplicate edges (same from/to/reason type)
|
|
742
|
+
edges.sort_by(|a, b| (&a.from, &a.to).cmp(&(&b.from, &b.to)));
|
|
743
|
+
edges.dedup_by(|a, b| {
|
|
744
|
+
a.from == b.from
|
|
745
|
+
&& a.to == b.to
|
|
746
|
+
&& std::mem::discriminant(&a.reason) == std::mem::discriminant(&b.reason)
|
|
747
|
+
});
|
|
748
|
+
|
|
749
|
+
if order_only {
|
|
750
|
+
let file_list: Vec<&PathBuf> = files_info.keys().collect();
|
|
751
|
+
match format {
|
|
752
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&file_list)?),
|
|
753
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&file_list)?),
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
let output = GraphOutput {
|
|
757
|
+
directory: dir.clone(),
|
|
758
|
+
files: files_info.keys().cloned().collect(),
|
|
759
|
+
edges,
|
|
760
|
+
warnings,
|
|
761
|
+
};
|
|
762
|
+
|
|
763
|
+
match format {
|
|
764
|
+
OutputFormat::Json => println!("{}", serde_json::to_string(&output)?),
|
|
765
|
+
OutputFormat::Pretty => println!("{}", serde_json::to_string_pretty(&output)?),
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
Ok(())
|
|
770
|
+
}
|