pytrilogy 0.3.149__cp313-cp313-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.
Files changed (207) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.149.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.149.dist-info/RECORD +207 -0
  6. pytrilogy-0.3.149.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.149.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +27 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +119 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +454 -0
  31. trilogy/core/env_processor.py +239 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1240 -0
  36. trilogy/core/graph_models.py +142 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2670 -0
  40. trilogy/core/models/build.py +2603 -0
  41. trilogy/core/models/build_environment.py +165 -0
  42. trilogy/core/models/core.py +506 -0
  43. trilogy/core/models/datasource.py +436 -0
  44. trilogy/core/models/environment.py +756 -0
  45. trilogy/core/models/execute.py +1213 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +270 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +207 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +695 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +846 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +522 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +604 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1432 -0
  112. trilogy/dialect/bigquery.py +314 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +159 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +397 -0
  117. trilogy/dialect/enums.py +151 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/__init__.py +17 -0
  127. trilogy/execution/config.py +119 -0
  128. trilogy/execution/state/__init__.py +0 -0
  129. trilogy/execution/state/exceptions.py +26 -0
  130. trilogy/execution/state/file_state_store.py +0 -0
  131. trilogy/execution/state/sqllite_state_store.py +0 -0
  132. trilogy/execution/state/state_store.py +406 -0
  133. trilogy/executor.py +692 -0
  134. trilogy/hooks/__init__.py +4 -0
  135. trilogy/hooks/base_hook.py +40 -0
  136. trilogy/hooks/graph_hook.py +135 -0
  137. trilogy/hooks/query_debugger.py +166 -0
  138. trilogy/metadata/__init__.py +0 -0
  139. trilogy/parser.py +10 -0
  140. trilogy/parsing/README.md +21 -0
  141. trilogy/parsing/__init__.py +0 -0
  142. trilogy/parsing/common.py +1069 -0
  143. trilogy/parsing/config.py +5 -0
  144. trilogy/parsing/exceptions.py +8 -0
  145. trilogy/parsing/helpers.py +1 -0
  146. trilogy/parsing/parse_engine.py +2876 -0
  147. trilogy/parsing/render.py +775 -0
  148. trilogy/parsing/trilogy.lark +546 -0
  149. trilogy/py.typed +0 -0
  150. trilogy/render.py +45 -0
  151. trilogy/scripts/README.md +9 -0
  152. trilogy/scripts/__init__.py +0 -0
  153. trilogy/scripts/agent.py +41 -0
  154. trilogy/scripts/agent_info.py +306 -0
  155. trilogy/scripts/common.py +432 -0
  156. trilogy/scripts/dependency/Cargo.lock +617 -0
  157. trilogy/scripts/dependency/Cargo.toml +39 -0
  158. trilogy/scripts/dependency/README.md +131 -0
  159. trilogy/scripts/dependency/build.sh +25 -0
  160. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  161. trilogy/scripts/dependency/src/lib.rs +16 -0
  162. trilogy/scripts/dependency/src/main.rs +770 -0
  163. trilogy/scripts/dependency/src/parser.rs +435 -0
  164. trilogy/scripts/dependency/src/preql.pest +208 -0
  165. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  166. trilogy/scripts/dependency/src/resolver.rs +716 -0
  167. trilogy/scripts/dependency/tests/base.preql +3 -0
  168. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  169. trilogy/scripts/dependency/tests/customer.preql +6 -0
  170. trilogy/scripts/dependency/tests/main.preql +9 -0
  171. trilogy/scripts/dependency/tests/orders.preql +7 -0
  172. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  173. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  174. trilogy/scripts/dependency.py +323 -0
  175. trilogy/scripts/display.py +555 -0
  176. trilogy/scripts/environment.py +59 -0
  177. trilogy/scripts/fmt.py +32 -0
  178. trilogy/scripts/ingest.py +487 -0
  179. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  180. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  181. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  182. trilogy/scripts/ingest_helpers/typing.py +161 -0
  183. trilogy/scripts/init.py +105 -0
  184. trilogy/scripts/parallel_execution.py +762 -0
  185. trilogy/scripts/plan.py +189 -0
  186. trilogy/scripts/refresh.py +161 -0
  187. trilogy/scripts/run.py +79 -0
  188. trilogy/scripts/serve.py +202 -0
  189. trilogy/scripts/serve_helpers/__init__.py +41 -0
  190. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  191. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  192. trilogy/scripts/serve_helpers/models.py +38 -0
  193. trilogy/scripts/single_execution.py +131 -0
  194. trilogy/scripts/testing.py +143 -0
  195. trilogy/scripts/trilogy.py +75 -0
  196. trilogy/std/__init__.py +0 -0
  197. trilogy/std/color.preql +3 -0
  198. trilogy/std/date.preql +13 -0
  199. trilogy/std/display.preql +18 -0
  200. trilogy/std/geography.preql +22 -0
  201. trilogy/std/metric.preql +15 -0
  202. trilogy/std/money.preql +67 -0
  203. trilogy/std/net.preql +14 -0
  204. trilogy/std/ranking.preql +7 -0
  205. trilogy/std/report.preql +5 -0
  206. trilogy/std/semantic.preql +6 -0
  207. 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
+ }