dlin-cli 0.1.1__tar.gz → 0.1.2a1__tar.gz
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.
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/Cargo.lock +8 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/Cargo.toml +2 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/PKG-INFO +4 -2
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/README.md +3 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/cli.rs +7 -9
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/builder.rs +360 -10
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/impact.rs +2 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/input.rs +4 -2
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/main.rs +34 -15
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/manifest.rs +14 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/yaml_schema.rs +63 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/html.rs +2 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/json.rs +2 -1
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/integration_test.rs +232 -5
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/.github/workflows/check.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/.github/workflows/publish-crate.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/.github/workflows/publish-pypi.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/.github/workflows/release.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/.gitignore +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/CHANGELOG.md +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/LICENSE +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/assets/tui-demo.gif +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/dist-workspace.toml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/pyproject.toml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/error.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/filter.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/mod.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_bfs_pseudoendpoint.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_endpoints_fan_out.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_endpoints_leaf_model.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_multiple_focus_models.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_no_source_exposure.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__collapse_snapshot_preserve_focus.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__snapshot_transitive_node_type_filter.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__snapshot_transitive_select_filter.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/snapshots/dlin__graph__filter__tests__snapshot_transitive_select_with_node_type.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/graph/types.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/lib.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/cache.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/columns.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/discovery.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/jinja.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/mod.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/project.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/parser/sql.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/ascii.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/dot.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/impact.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/layout.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/list.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/mermaid.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/mod.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/plain.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__group_by_node_type.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_all_edge_types.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_direction_tb.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_direction_tb_grouped.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_group_by_directory.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_lineage.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__dot__tests__snapshot_transitive_edges.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__html__tests__snapshot_html_json.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__impact__tests__snapshot_impact_json.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__impact__tests__snapshot_impact_json_with_sql.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__impact__tests__snapshot_impact_text.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__impact__tests__snapshot_impact_text_with_sql.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__json__tests__snapshot_json_with_sql.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__json__tests__snapshot_lineage.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__json__tests__snapshot_node_metadata.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__list__tests__snapshot_list_json.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__list__tests__snapshot_list_plain.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__group_by_node_type.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__mixed_direct_and_transitive_edges.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__show_columns_escapes_quotes.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__show_columns_lineage.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__show_columns_single_model.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__show_columns_with_collapse.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__show_columns_with_grouping.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__snapshot_direction_tb.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__snapshot_direction_tb_grouped.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__snapshot_group_by_directory.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__snapshot_lineage.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__mermaid__tests__transitive_edge_rendering.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__plain__tests__snapshot_plain.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__summary__tests__snapshot_summary_json.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/snapshots/dlin__render__summary__tests__snapshot_summary_text.snap +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/summary.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/src/render/svg.rs +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/.dlin_cache/.gitignore +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/dbt_project.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/macros/order_totals.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/marts/combined_orders.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/marts/customers.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/marts/order_summary.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/marts/orders.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/marts/schema.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/schema.yml +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/stg_customers.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/stg_online_orders.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/stg_orders.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/stg_payments.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/models/staging/stg_retail_orders.sql +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/seeds/countries.csv +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/target/manifest.json +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/target/run_results.json +0 -0
- {dlin_cli-0.1.1 → dlin_cli-0.1.2a1}/tests/fixtures/simple_project/tests/assert_orders_positive_amount.sql +0 -0
|
@@ -231,7 +231,7 @@ checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
|
|
|
231
231
|
|
|
232
232
|
[[package]]
|
|
233
233
|
name = "dlin"
|
|
234
|
-
version = "0.1.1"
|
|
234
|
+
version = "0.1.2-alpha.1"
|
|
235
235
|
dependencies = [
|
|
236
236
|
"anyhow",
|
|
237
237
|
"clap",
|
|
@@ -241,6 +241,7 @@ dependencies = [
|
|
|
241
241
|
"insta",
|
|
242
242
|
"libc",
|
|
243
243
|
"minijinja",
|
|
244
|
+
"path-slash",
|
|
244
245
|
"petgraph",
|
|
245
246
|
"rayon",
|
|
246
247
|
"regex",
|
|
@@ -576,6 +577,12 @@ dependencies = [
|
|
|
576
577
|
"windows-link",
|
|
577
578
|
]
|
|
578
579
|
|
|
580
|
+
[[package]]
|
|
581
|
+
name = "path-slash"
|
|
582
|
+
version = "0.2.1"
|
|
583
|
+
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
584
|
+
checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
|
|
585
|
+
|
|
579
586
|
[[package]]
|
|
580
587
|
name = "petgraph"
|
|
581
588
|
version = "0.6.5"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "dlin"
|
|
3
|
-
version = "0.1.1"
|
|
3
|
+
version = "0.1.2-alpha.1"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
description = "A fast CLI tool for dbt model lineage analysis"
|
|
6
6
|
license = "MIT"
|
|
@@ -29,6 +29,7 @@ indexmap = "2"
|
|
|
29
29
|
minijinja = "2"
|
|
30
30
|
rayon = "1"
|
|
31
31
|
globset = "0.4"
|
|
32
|
+
path-slash = "0.2.1"
|
|
32
33
|
|
|
33
34
|
[target.'cfg(unix)'.dependencies]
|
|
34
35
|
libc = "0.2"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dlin-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2a1
|
|
4
4
|
Classifier: Development Status :: 4 - Beta
|
|
5
5
|
Classifier: Environment :: Console
|
|
6
6
|
Classifier: Intended Audience :: Developers
|
|
@@ -24,6 +24,7 @@ Project-URL: Repository, https://github.com/eitsupi/dlin
|
|
|
24
24
|
|
|
25
25
|
[](https://crates.io/crates/dlin)
|
|
26
26
|
[](https://pypi.org/project/dlin-cli/)
|
|
27
|
+
[](https://deepwiki.com/eitsupi/dlin)
|
|
27
28
|
|
|
28
29
|
dbt lineage analysis CLI that parses SQL files directly. No `dbt compile`, no Python, no `manifest.json`.
|
|
29
30
|
|
|
@@ -291,7 +292,7 @@ dlin graph --node-type model,source # filter by node type
|
|
|
291
292
|
|
|
292
293
|
dlin aims to work without `dbt compile`. By default it parses SQL files directly, but it can also leverage a pre-compiled `manifest.json` for additional accuracy when one is available.
|
|
293
294
|
|
|
294
|
-
**SQL parsing (default)**: extracts `ref()` and `source()` from SQL via regex + Jinja template evaluation. No Python or dbt needed.
|
|
295
|
+
**SQL parsing (default)**: extracts `ref()` and `source()` from SQL via regex + Jinja template evaluation. No Python or dbt needed. Generic tests (`not_null`, `unique`, `relationships`, etc.) are inferred from YAML schema declarations.
|
|
295
296
|
|
|
296
297
|
**Manifest mode** (`--source manifest`): reads a pre-compiled `manifest.json` for full accuracy with complex Jinja logic.
|
|
297
298
|
|
|
@@ -300,6 +301,7 @@ dlin aims to work without `dbt compile`. By default it parses SQL files directly
|
|
|
300
301
|
- `var()` resolves from `dbt_project.yml` only (`--vars` CLI overrides not supported)
|
|
301
302
|
- Runtime context (`target.type`, `env_var()`) is not evaluated
|
|
302
303
|
- Conditional Jinja branches use default values; non-default paths may be missed
|
|
304
|
+
- Generic test IDs are dlin-specific (e.g. `test.not_null.orders.order_id`) and do not match dbt's naming; use manifest mode when exact test IDs matter
|
|
303
305
|
|
|
304
306
|
When these limitations matter, use `--source manifest`.
|
|
305
307
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://crates.io/crates/dlin)
|
|
4
4
|
[](https://pypi.org/project/dlin-cli/)
|
|
5
|
+
[](https://deepwiki.com/eitsupi/dlin)
|
|
5
6
|
|
|
6
7
|
dbt lineage analysis CLI that parses SQL files directly. No `dbt compile`, no Python, no `manifest.json`.
|
|
7
8
|
|
|
@@ -269,7 +270,7 @@ dlin graph --node-type model,source # filter by node type
|
|
|
269
270
|
|
|
270
271
|
dlin aims to work without `dbt compile`. By default it parses SQL files directly, but it can also leverage a pre-compiled `manifest.json` for additional accuracy when one is available.
|
|
271
272
|
|
|
272
|
-
**SQL parsing (default)**: extracts `ref()` and `source()` from SQL via regex + Jinja template evaluation. No Python or dbt needed.
|
|
273
|
+
**SQL parsing (default)**: extracts `ref()` and `source()` from SQL via regex + Jinja template evaluation. No Python or dbt needed. Generic tests (`not_null`, `unique`, `relationships`, etc.) are inferred from YAML schema declarations.
|
|
273
274
|
|
|
274
275
|
**Manifest mode** (`--source manifest`): reads a pre-compiled `manifest.json` for full accuracy with complex Jinja logic.
|
|
275
276
|
|
|
@@ -278,6 +279,7 @@ dlin aims to work without `dbt compile`. By default it parses SQL files directly
|
|
|
278
279
|
- `var()` resolves from `dbt_project.yml` only (`--vars` CLI overrides not supported)
|
|
279
280
|
- Runtime context (`target.type`, `env_var()`) is not evaluated
|
|
280
281
|
- Conditional Jinja branches use default values; non-default paths may be missed
|
|
282
|
+
- Generic test IDs are dlin-specific (e.g. `test.not_null.orders.order_id`) and do not match dbt's naming; use manifest mode when exact test IDs matter
|
|
281
283
|
|
|
282
284
|
When these limitations matter, use `--source manifest`.
|
|
283
285
|
|
|
@@ -15,8 +15,8 @@ or HTML.
|
|
|
15
15
|
Data sources:
|
|
16
16
|
sql (default) Parse SQL files via regex + minijinja. No Python or dbt required.
|
|
17
17
|
Detects ref() and source() calls in SQL, plus exposures from YAML.
|
|
18
|
-
|
|
19
|
-
— use manifest mode for
|
|
18
|
+
Generic tests (not_null, unique, etc.) are inferred from YAML declarations
|
|
19
|
+
with dlin-specific IDs — use manifest mode for exact dependency resolution.
|
|
20
20
|
manifest Read a pre-compiled manifest.json for full accuracy. Requires
|
|
21
21
|
`dbt compile` (or `dbt run`/`dbt build`) to have been run first.
|
|
22
22
|
Use `dlin check-manifest` to verify freshness before querying.
|
|
@@ -262,9 +262,8 @@ pub struct GraphArgs {
|
|
|
262
262
|
/// Filter output by node type (comma-separated). Default: all types.
|
|
263
263
|
/// Available types: model, source, seed, snapshot, test, exposure.
|
|
264
264
|
///
|
|
265
|
-
///
|
|
266
|
-
///
|
|
267
|
-
/// Use --source manifest for full test coverage.
|
|
265
|
+
/// NOTE: In sql mode, generic tests are inferred from YAML declarations
|
|
266
|
+
/// with dlin-specific IDs. Use --source manifest for exact dependency resolution.
|
|
268
267
|
#[arg(long = "node-type", value_delimiter = ',')]
|
|
269
268
|
pub node_types: Option<Vec<String>>,
|
|
270
269
|
|
|
@@ -633,9 +632,8 @@ pub struct ListArgs {
|
|
|
633
632
|
/// Filter output by node type (comma-separated). Default: all types.
|
|
634
633
|
/// Available types: model, source, seed, snapshot, test, exposure.
|
|
635
634
|
///
|
|
636
|
-
///
|
|
637
|
-
///
|
|
638
|
-
/// Use --source manifest for full test coverage.
|
|
635
|
+
/// NOTE: In sql mode, generic tests are inferred from YAML declarations
|
|
636
|
+
/// with dlin-specific IDs. Use --source manifest for exact dependency resolution.
|
|
639
637
|
#[arg(long = "node-type", value_delimiter = ',')]
|
|
640
638
|
pub node_types: Option<Vec<String>>,
|
|
641
639
|
|
|
@@ -688,7 +686,7 @@ impl OutputFormat {
|
|
|
688
686
|
|
|
689
687
|
#[derive(Debug, Clone, PartialEq, Eq, clap::ValueEnum)]
|
|
690
688
|
pub enum SourceType {
|
|
691
|
-
/// Parse SQL files directly — no dbt/Python required. Exposures are detected from YAML
|
|
689
|
+
/// Parse SQL files directly — no dbt/Python required. Exposures and generic tests are detected from YAML with dlin-specific IDs; use manifest mode for exact test dependency resolution
|
|
692
690
|
Sql,
|
|
693
691
|
/// Use dbt manifest.json — full accuracy, requires prior `dbt compile`
|
|
694
692
|
Manifest,
|
|
@@ -14,7 +14,7 @@ use crate::parser::jinja::JinjaExtraction;
|
|
|
14
14
|
use crate::parser::sql::{
|
|
15
15
|
RefCall, SourceCall, extract_all_with_vars, extract_refs_and_sources_with_vars,
|
|
16
16
|
};
|
|
17
|
-
use crate::parser::yaml_schema::{ExposureDefinition, parse_schema_file};
|
|
17
|
+
use crate::parser::yaml_schema::{ExposureDefinition, SchemaFile, parse_schema_file};
|
|
18
18
|
|
|
19
19
|
/// Read all macro SQL files, filter out unparseable ones, and return a
|
|
20
20
|
/// pre-built prefix string for prepending to model templates.
|
|
@@ -171,16 +171,32 @@ struct YamlModelMeta {
|
|
|
171
171
|
columns: Vec<String>,
|
|
172
172
|
}
|
|
173
173
|
|
|
174
|
-
///
|
|
174
|
+
/// Result of parsing YAML schema files.
|
|
175
|
+
/// The third element pairs each `SchemaFile` with the relative path of the
|
|
176
|
+
/// YAML file it was parsed from (used to populate `file_path` on test nodes).
|
|
177
|
+
type YamlResult = (
|
|
178
|
+
HashMap<String, YamlModelMeta>,
|
|
179
|
+
Vec<ExposureDefinition>,
|
|
180
|
+
Vec<(SchemaFile, PathBuf)>,
|
|
181
|
+
);
|
|
182
|
+
|
|
183
|
+
/// Parse YAML schema files: create source nodes, collect model metadata, exposures,
|
|
184
|
+
/// and parsed schemas (for generic test extraction).
|
|
175
185
|
fn process_yaml_files(
|
|
176
186
|
gb: &mut GraphBuilder,
|
|
177
187
|
files: &DiscoveredFiles,
|
|
178
188
|
project_dir: &Path,
|
|
179
|
-
) -> Result<
|
|
189
|
+
) -> Result<YamlResult> {
|
|
180
190
|
let mut model_meta: HashMap<String, YamlModelMeta> = HashMap::new();
|
|
181
191
|
let mut exposures: Vec<ExposureDefinition> = Vec::new();
|
|
192
|
+
let mut schemas: Vec<(SchemaFile, PathBuf)> = Vec::new();
|
|
182
193
|
|
|
183
|
-
|
|
194
|
+
// Sort YAML paths so that duplicate-test-ID suffixes (_2, _3, …) are
|
|
195
|
+
// deterministic across filesystems/OSes.
|
|
196
|
+
let mut sorted_yaml_files = files.yaml_files.clone();
|
|
197
|
+
sorted_yaml_files.sort();
|
|
198
|
+
|
|
199
|
+
for yaml_path in &sorted_yaml_files {
|
|
184
200
|
let content = read_file(yaml_path)?;
|
|
185
201
|
let schema = match parse_schema_file(&content, Some(yaml_path.as_path())) {
|
|
186
202
|
Ok(s) => s,
|
|
@@ -207,10 +223,15 @@ fn process_yaml_files(
|
|
|
207
223
|
model_meta.insert(model_def.name.clone(), meta);
|
|
208
224
|
}
|
|
209
225
|
|
|
210
|
-
exposures.extend(schema.exposures.
|
|
226
|
+
exposures.extend(schema.exposures.iter().cloned());
|
|
227
|
+
let relative_path = yaml_path
|
|
228
|
+
.strip_prefix(project_dir)
|
|
229
|
+
.unwrap_or(yaml_path)
|
|
230
|
+
.to_path_buf();
|
|
231
|
+
schemas.push((schema, relative_path));
|
|
211
232
|
}
|
|
212
233
|
|
|
213
|
-
Ok((model_meta, exposures))
|
|
234
|
+
Ok((model_meta, exposures, schemas))
|
|
214
235
|
}
|
|
215
236
|
|
|
216
237
|
/// Cached extraction result for a model SQL file (refs and sources).
|
|
@@ -442,10 +463,19 @@ fn process_sql_edges(
|
|
|
442
463
|
(&owned.0, &owned.1)
|
|
443
464
|
};
|
|
444
465
|
|
|
466
|
+
// Use EdgeType::Test when the target node is a test, so all test
|
|
467
|
+
// relationships render with consistent edge labels/styles.
|
|
468
|
+
let is_test = *file_type == "test";
|
|
469
|
+
|
|
445
470
|
for ref_call in refs {
|
|
446
471
|
let dep_idx = gb.get_or_create_phantom_ref(&ref_call.name, sql_path);
|
|
472
|
+
let edge_type = if is_test {
|
|
473
|
+
EdgeType::Test
|
|
474
|
+
} else {
|
|
475
|
+
EdgeType::Ref
|
|
476
|
+
};
|
|
447
477
|
gb.graph
|
|
448
|
-
.add_edge(dep_idx, current_idx, EdgeData::direct(
|
|
478
|
+
.add_edge(dep_idx, current_idx, EdgeData::direct(edge_type));
|
|
449
479
|
}
|
|
450
480
|
|
|
451
481
|
for source_call in sources {
|
|
@@ -454,8 +484,13 @@ fn process_sql_edges(
|
|
|
454
484
|
&source_call.table_name,
|
|
455
485
|
sql_path,
|
|
456
486
|
);
|
|
487
|
+
let edge_type = if is_test {
|
|
488
|
+
EdgeType::Test
|
|
489
|
+
} else {
|
|
490
|
+
EdgeType::Source
|
|
491
|
+
};
|
|
457
492
|
gb.graph
|
|
458
|
-
.add_edge(source_idx, current_idx, EdgeData::direct(
|
|
493
|
+
.add_edge(source_idx, current_idx, EdgeData::direct(edge_type));
|
|
459
494
|
}
|
|
460
495
|
}
|
|
461
496
|
|
|
@@ -499,6 +534,132 @@ fn process_exposures(gb: &mut GraphBuilder, exposures: &[ExposureDefinition]) {
|
|
|
499
534
|
}
|
|
500
535
|
}
|
|
501
536
|
|
|
537
|
+
/// Deduplicate a candidate unique_id by appending `_2`, `_3`, … if it already
|
|
538
|
+
/// exists in the node map. Returns `(unique_id, suffix)` where `suffix` is
|
|
539
|
+
/// `None` when no deduplication was needed, or `Some("_2")` etc. when it was.
|
|
540
|
+
/// Callers can append the suffix to labels so they stay distinct too.
|
|
541
|
+
fn dedup_unique_id(
|
|
542
|
+
candidate: &str,
|
|
543
|
+
node_map: &HashMap<String, NodeIndex>,
|
|
544
|
+
) -> (String, Option<String>) {
|
|
545
|
+
if !node_map.contains_key(candidate) {
|
|
546
|
+
return (candidate.to_string(), None);
|
|
547
|
+
}
|
|
548
|
+
let mut n = 2u32;
|
|
549
|
+
loop {
|
|
550
|
+
let suffix = format!("_{}", n);
|
|
551
|
+
let suffixed = format!("{}{}", candidate, suffix);
|
|
552
|
+
if !node_map.contains_key(&suffixed) {
|
|
553
|
+
return (suffixed, Some(suffix));
|
|
554
|
+
}
|
|
555
|
+
n += 1;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/// Add a generic test node to the graph and connect it to the parent.
|
|
560
|
+
fn add_generic_test_node(
|
|
561
|
+
gb: &mut GraphBuilder,
|
|
562
|
+
parent_idx: NodeIndex,
|
|
563
|
+
unique_id: String,
|
|
564
|
+
label: String,
|
|
565
|
+
file_path: Option<PathBuf>,
|
|
566
|
+
) {
|
|
567
|
+
let idx = gb.add_node(NodeData {
|
|
568
|
+
unique_id,
|
|
569
|
+
label,
|
|
570
|
+
node_type: NodeType::Test,
|
|
571
|
+
file_path,
|
|
572
|
+
description: None,
|
|
573
|
+
materialization: None,
|
|
574
|
+
tags: vec![],
|
|
575
|
+
columns: vec![],
|
|
576
|
+
exposure: None,
|
|
577
|
+
});
|
|
578
|
+
gb.graph
|
|
579
|
+
.add_edge(parent_idx, idx, EdgeData::direct(EdgeType::Test));
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// Create test nodes for YAML-declared generic tests (not_null, unique, etc.)
|
|
583
|
+
/// and connect them to their parent model/source nodes.
|
|
584
|
+
fn process_generic_tests(gb: &mut GraphBuilder, schemas: &[(SchemaFile, PathBuf)]) {
|
|
585
|
+
for (schema, yaml_path) in schemas {
|
|
586
|
+
let file_path = Some(yaml_path.clone());
|
|
587
|
+
|
|
588
|
+
// Model-level generic tests
|
|
589
|
+
for model_def in &schema.models {
|
|
590
|
+
let parent_id = format!("model.{}", model_def.name);
|
|
591
|
+
let parent_idx = match gb.node_map.get(&parent_id) {
|
|
592
|
+
Some(&idx) => idx,
|
|
593
|
+
None => continue,
|
|
594
|
+
};
|
|
595
|
+
|
|
596
|
+
// Model-level tests (not attached to a column)
|
|
597
|
+
for test_def in &model_def.tests {
|
|
598
|
+
let test_name = match test_def.test_name() {
|
|
599
|
+
Some(name) => name,
|
|
600
|
+
None => continue,
|
|
601
|
+
};
|
|
602
|
+
let candidate = format!("test.{}.{}", test_name, model_def.name);
|
|
603
|
+
let (unique_id, suffix) = dedup_unique_id(&candidate, &gb.node_map);
|
|
604
|
+
let mut label = format!("{}_{}", test_name, model_def.name);
|
|
605
|
+
if let Some(s) = suffix {
|
|
606
|
+
label.push_str(&s);
|
|
607
|
+
}
|
|
608
|
+
add_generic_test_node(gb, parent_idx, unique_id, label, file_path.clone());
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// Column-level tests
|
|
612
|
+
for col in &model_def.columns {
|
|
613
|
+
for test_def in &col.tests {
|
|
614
|
+
let test_name = match test_def.test_name() {
|
|
615
|
+
Some(name) => name,
|
|
616
|
+
None => continue,
|
|
617
|
+
};
|
|
618
|
+
let candidate = format!("test.{}.{}.{}", test_name, model_def.name, col.name);
|
|
619
|
+
let (unique_id, suffix) = dedup_unique_id(&candidate, &gb.node_map);
|
|
620
|
+
let mut label = format!("{}_{}_{}", test_name, model_def.name, col.name);
|
|
621
|
+
if let Some(s) = suffix {
|
|
622
|
+
label.push_str(&s);
|
|
623
|
+
}
|
|
624
|
+
add_generic_test_node(gb, parent_idx, unique_id, label, file_path.clone());
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// Source-level generic tests (column-level only)
|
|
630
|
+
for source_def in &schema.sources {
|
|
631
|
+
for table in &source_def.tables {
|
|
632
|
+
let parent_id = format!("source.{}.{}", source_def.name, table.name);
|
|
633
|
+
let parent_idx = match gb.node_map.get(&parent_id) {
|
|
634
|
+
Some(&idx) => idx,
|
|
635
|
+
None => continue,
|
|
636
|
+
};
|
|
637
|
+
for col in &table.columns {
|
|
638
|
+
for test_def in &col.tests {
|
|
639
|
+
let test_name = match test_def.test_name() {
|
|
640
|
+
Some(name) => name,
|
|
641
|
+
None => continue,
|
|
642
|
+
};
|
|
643
|
+
let candidate = format!(
|
|
644
|
+
"test.{}.{}.{}.{}",
|
|
645
|
+
test_name, source_def.name, table.name, col.name
|
|
646
|
+
);
|
|
647
|
+
let (unique_id, suffix) = dedup_unique_id(&candidate, &gb.node_map);
|
|
648
|
+
let mut label = format!(
|
|
649
|
+
"{}_{}_{}_{}",
|
|
650
|
+
test_name, source_def.name, table.name, col.name
|
|
651
|
+
);
|
|
652
|
+
if let Some(s) = suffix {
|
|
653
|
+
label.push_str(&s);
|
|
654
|
+
}
|
|
655
|
+
add_generic_test_node(gb, parent_idx, unique_id, label, file_path.clone());
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
|
|
502
663
|
/// Build the lineage graph from discovered files.
|
|
503
664
|
/// If `cache_dir` is provided, it is used as the cache directory;
|
|
504
665
|
/// otherwise the cache is stored under `<project_dir>/.dlin_cache/`.
|
|
@@ -523,7 +684,7 @@ pub fn build_graph(
|
|
|
523
684
|
cache::ExtractionCache::load(project_dir, ¯o_prefix, vars, cache_dir)
|
|
524
685
|
};
|
|
525
686
|
|
|
526
|
-
let (model_meta, exposures) = process_yaml_files(&mut gb, files, project_dir)?;
|
|
687
|
+
let (model_meta, exposures, schemas) = process_yaml_files(&mut gb, files, project_dir)?;
|
|
527
688
|
let extraction_cache = process_model_files(
|
|
528
689
|
&mut gb,
|
|
529
690
|
files,
|
|
@@ -556,6 +717,7 @@ pub fn build_graph(
|
|
|
556
717
|
vars,
|
|
557
718
|
)?;
|
|
558
719
|
process_exposures(&mut gb, &exposures);
|
|
720
|
+
process_generic_tests(&mut gb, &schemas);
|
|
559
721
|
|
|
560
722
|
disk_cache.save();
|
|
561
723
|
|
|
@@ -827,8 +989,13 @@ models:
|
|
|
827
989
|
let graph = build_graph(&project_dir, &files, None, true, false, &HashMap::new()).unwrap();
|
|
828
990
|
// model + test = 2 nodes
|
|
829
991
|
assert_eq!(graph.node_count(), 2);
|
|
830
|
-
//
|
|
992
|
+
// test edge: stg_orders → assert_positive
|
|
831
993
|
assert_eq!(graph.edge_count(), 1);
|
|
994
|
+
|
|
995
|
+
// Singular SQL tests should use EdgeType::Test
|
|
996
|
+
use petgraph::visit::IntoEdgeReferences;
|
|
997
|
+
let edge = graph.edge_references().next().unwrap();
|
|
998
|
+
assert_eq!(edge.weight().edge_type, EdgeType::Test);
|
|
832
999
|
}
|
|
833
1000
|
|
|
834
1001
|
#[test]
|
|
@@ -1261,4 +1428,187 @@ models:
|
|
|
1261
1428
|
assert!(graph.contains_edge(electronics, combined));
|
|
1262
1429
|
assert!(graph.contains_edge(clothing, combined));
|
|
1263
1430
|
}
|
|
1431
|
+
|
|
1432
|
+
#[test]
|
|
1433
|
+
fn test_generic_tests_from_yaml() {
|
|
1434
|
+
let tmp = tempfile::tempdir().unwrap();
|
|
1435
|
+
let project_dir = tmp.path().to_path_buf();
|
|
1436
|
+
let models_dir = project_dir.join("models");
|
|
1437
|
+
fs::create_dir_all(&models_dir).unwrap();
|
|
1438
|
+
|
|
1439
|
+
fs::write(project_dir.join("dbt_project.yml"), "name: test_proj\n").unwrap();
|
|
1440
|
+
fs::write(models_dir.join("orders.sql"), "SELECT 1 AS order_id").unwrap();
|
|
1441
|
+
|
|
1442
|
+
// Schema with generic tests on columns
|
|
1443
|
+
fs::write(
|
|
1444
|
+
models_dir.join("schema.yml"),
|
|
1445
|
+
r#"
|
|
1446
|
+
sources:
|
|
1447
|
+
- name: raw
|
|
1448
|
+
tables:
|
|
1449
|
+
- name: events
|
|
1450
|
+
columns:
|
|
1451
|
+
- name: event_id
|
|
1452
|
+
data_tests:
|
|
1453
|
+
- not_null
|
|
1454
|
+
models:
|
|
1455
|
+
- name: orders
|
|
1456
|
+
data_tests:
|
|
1457
|
+
- dbt_utils.expression_is_true:
|
|
1458
|
+
expression: "a = b"
|
|
1459
|
+
- dbt_utils.expression_is_true:
|
|
1460
|
+
expression: "c = d"
|
|
1461
|
+
columns:
|
|
1462
|
+
- name: order_id
|
|
1463
|
+
data_tests:
|
|
1464
|
+
- not_null
|
|
1465
|
+
- unique
|
|
1466
|
+
"#,
|
|
1467
|
+
)
|
|
1468
|
+
.unwrap();
|
|
1469
|
+
|
|
1470
|
+
let files = DiscoveredFiles {
|
|
1471
|
+
model_sql_files: vec![project_dir.join("models/orders.sql")],
|
|
1472
|
+
yaml_files: vec![project_dir.join("models/schema.yml")],
|
|
1473
|
+
..Default::default()
|
|
1474
|
+
};
|
|
1475
|
+
|
|
1476
|
+
let graph = build_graph(&project_dir, &files, None, true, false, &HashMap::new()).unwrap();
|
|
1477
|
+
|
|
1478
|
+
// 1 model + 1 source + 2 column tests + 1 source test + 2 model-level tests = 7
|
|
1479
|
+
assert_eq!(graph.node_count(), 7);
|
|
1480
|
+
|
|
1481
|
+
let test_nodes: Vec<_> = graph
|
|
1482
|
+
.node_indices()
|
|
1483
|
+
.filter(|&i| graph[i].node_type == NodeType::Test)
|
|
1484
|
+
.collect();
|
|
1485
|
+
assert_eq!(test_nodes.len(), 5);
|
|
1486
|
+
|
|
1487
|
+
// Verify test unique_ids
|
|
1488
|
+
let mut test_ids: Vec<&str> = test_nodes
|
|
1489
|
+
.iter()
|
|
1490
|
+
.map(|&i| graph[i].unique_id.as_str())
|
|
1491
|
+
.collect();
|
|
1492
|
+
test_ids.sort();
|
|
1493
|
+
assert!(test_ids.contains(&"test.not_null.orders.order_id"));
|
|
1494
|
+
assert!(test_ids.contains(&"test.unique.orders.order_id"));
|
|
1495
|
+
assert!(test_ids.contains(&"test.not_null.raw.events.event_id"));
|
|
1496
|
+
// Model-level tests: first gets base ID, second gets _2 suffix
|
|
1497
|
+
assert!(test_ids.contains(&"test.dbt_utils.expression_is_true.orders"));
|
|
1498
|
+
assert!(test_ids.contains(&"test.dbt_utils.expression_is_true.orders_2"));
|
|
1499
|
+
|
|
1500
|
+
// All test edges from model (2 column + 2 model-level = 4)
|
|
1501
|
+
let model_idx = graph
|
|
1502
|
+
.node_indices()
|
|
1503
|
+
.find(|&i| graph[i].unique_id == "model.orders")
|
|
1504
|
+
.unwrap();
|
|
1505
|
+
let source_idx = graph
|
|
1506
|
+
.node_indices()
|
|
1507
|
+
.find(|&i| graph[i].unique_id == "source.raw.events")
|
|
1508
|
+
.unwrap();
|
|
1509
|
+
|
|
1510
|
+
let model_test_edges = graph
|
|
1511
|
+
.edges_directed(model_idx, petgraph::Direction::Outgoing)
|
|
1512
|
+
.filter(|e| e.weight().edge_type == EdgeType::Test)
|
|
1513
|
+
.count();
|
|
1514
|
+
assert_eq!(model_test_edges, 4);
|
|
1515
|
+
|
|
1516
|
+
let source_test_edges = graph
|
|
1517
|
+
.edges_directed(source_idx, petgraph::Direction::Outgoing)
|
|
1518
|
+
.filter(|e| e.weight().edge_type == EdgeType::Test)
|
|
1519
|
+
.count();
|
|
1520
|
+
assert_eq!(source_test_edges, 1);
|
|
1521
|
+
|
|
1522
|
+
// All generic test nodes should have file_path pointing to the YAML file
|
|
1523
|
+
for &ti in &test_nodes {
|
|
1524
|
+
assert_eq!(
|
|
1525
|
+
graph[ti].file_path.as_deref(),
|
|
1526
|
+
Some(std::path::Path::new("models/schema.yml")),
|
|
1527
|
+
"test node '{}' should have file_path",
|
|
1528
|
+
graph[ti].unique_id,
|
|
1529
|
+
);
|
|
1530
|
+
}
|
|
1531
|
+
|
|
1532
|
+
// Deduped test labels must also be distinct (suffix applied to label)
|
|
1533
|
+
let mut test_labels: Vec<&str> = test_nodes
|
|
1534
|
+
.iter()
|
|
1535
|
+
.map(|&i| graph[i].label.as_str())
|
|
1536
|
+
.collect();
|
|
1537
|
+
test_labels.sort();
|
|
1538
|
+
let deduped_len = test_labels.len();
|
|
1539
|
+
test_labels.dedup();
|
|
1540
|
+
assert_eq!(
|
|
1541
|
+
test_labels.len(),
|
|
1542
|
+
deduped_len,
|
|
1543
|
+
"All test labels should be unique"
|
|
1544
|
+
);
|
|
1545
|
+
// Verify the deduped model-level test labels
|
|
1546
|
+
assert!(test_labels.contains(&"dbt_utils.expression_is_true_orders"));
|
|
1547
|
+
assert!(test_labels.contains(&"dbt_utils.expression_is_true_orders_2"));
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
#[test]
|
|
1551
|
+
fn test_generic_test_ids_deterministic_across_yaml_order() {
|
|
1552
|
+
// Duplicate test names across two YAML files should produce the same
|
|
1553
|
+
// suffixed IDs regardless of the order the files are passed in.
|
|
1554
|
+
let tmp = tempfile::tempdir().unwrap();
|
|
1555
|
+
let project_dir = tmp.path().to_path_buf();
|
|
1556
|
+
let models_dir = project_dir.join("models");
|
|
1557
|
+
let sub_dir = models_dir.join("sub");
|
|
1558
|
+
fs::create_dir_all(&sub_dir).unwrap();
|
|
1559
|
+
|
|
1560
|
+
fs::write(models_dir.join("orders.sql"), "SELECT 1 AS order_id").unwrap();
|
|
1561
|
+
|
|
1562
|
+
// Two YAML files that both declare a not_null test on orders.order_id
|
|
1563
|
+
let yaml_a = models_dir.join("a_schema.yml");
|
|
1564
|
+
let yaml_b = sub_dir.join("b_schema.yml");
|
|
1565
|
+
let yaml_content = r#"
|
|
1566
|
+
models:
|
|
1567
|
+
- name: orders
|
|
1568
|
+
columns:
|
|
1569
|
+
- name: order_id
|
|
1570
|
+
data_tests:
|
|
1571
|
+
- not_null
|
|
1572
|
+
"#;
|
|
1573
|
+
fs::write(&yaml_a, yaml_content).unwrap();
|
|
1574
|
+
fs::write(&yaml_b, yaml_content).unwrap();
|
|
1575
|
+
|
|
1576
|
+
// Build with files in forward order
|
|
1577
|
+
let files_fwd = DiscoveredFiles {
|
|
1578
|
+
model_sql_files: vec![project_dir.join("models/orders.sql")],
|
|
1579
|
+
yaml_files: vec![yaml_a.clone(), yaml_b.clone()],
|
|
1580
|
+
..Default::default()
|
|
1581
|
+
};
|
|
1582
|
+
let graph_fwd =
|
|
1583
|
+
build_graph(&project_dir, &files_fwd, None, true, false, &HashMap::new()).unwrap();
|
|
1584
|
+
|
|
1585
|
+
// Build with files in reverse order
|
|
1586
|
+
let files_rev = DiscoveredFiles {
|
|
1587
|
+
model_sql_files: vec![project_dir.join("models/orders.sql")],
|
|
1588
|
+
yaml_files: vec![yaml_b, yaml_a],
|
|
1589
|
+
..Default::default()
|
|
1590
|
+
};
|
|
1591
|
+
let graph_rev =
|
|
1592
|
+
build_graph(&project_dir, &files_rev, None, true, false, &HashMap::new()).unwrap();
|
|
1593
|
+
|
|
1594
|
+
// Both should produce the same set of test unique_ids
|
|
1595
|
+
let mut ids_fwd: Vec<String> = graph_fwd
|
|
1596
|
+
.node_indices()
|
|
1597
|
+
.filter(|&i| graph_fwd[i].node_type == NodeType::Test)
|
|
1598
|
+
.map(|i| graph_fwd[i].unique_id.clone())
|
|
1599
|
+
.collect();
|
|
1600
|
+
ids_fwd.sort();
|
|
1601
|
+
|
|
1602
|
+
let mut ids_rev: Vec<String> = graph_rev
|
|
1603
|
+
.node_indices()
|
|
1604
|
+
.filter(|&i| graph_rev[i].node_type == NodeType::Test)
|
|
1605
|
+
.map(|i| graph_rev[i].unique_id.clone())
|
|
1606
|
+
.collect();
|
|
1607
|
+
ids_rev.sort();
|
|
1608
|
+
|
|
1609
|
+
assert_eq!(ids_fwd, ids_rev);
|
|
1610
|
+
assert_eq!(ids_fwd.len(), 2);
|
|
1611
|
+
assert!(ids_fwd.contains(&"test.not_null.orders.order_id".to_string()));
|
|
1612
|
+
assert!(ids_fwd.contains(&"test.not_null.orders.order_id_2".to_string()));
|
|
1613
|
+
}
|
|
1264
1614
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
use std::collections::{HashMap, HashSet, VecDeque};
|
|
2
2
|
|
|
3
|
+
use path_slash::PathExt as _;
|
|
3
4
|
use petgraph::Direction;
|
|
4
5
|
use petgraph::stable_graph::NodeIndex;
|
|
5
6
|
use petgraph::visit::EdgeRef;
|
|
@@ -132,7 +133,7 @@ pub fn compute_impact(graph: &LineageGraph, source_idx: NodeIndex) -> ImpactRepo
|
|
|
132
133
|
file_path: node
|
|
133
134
|
.file_path
|
|
134
135
|
.as_ref()
|
|
135
|
-
.map(|p| p.
|
|
136
|
+
.map(|p| p.to_slash_lossy().into_owned()),
|
|
136
137
|
severity,
|
|
137
138
|
distance: next_distance,
|
|
138
139
|
sql_content: None,
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
use std::io::{self, BufRead, IsTerminal};
|
|
2
2
|
use std::path::{Path, PathBuf};
|
|
3
3
|
|
|
4
|
+
use path_slash::PathExt as _;
|
|
5
|
+
|
|
4
6
|
use crate::graph::types::LineageGraph;
|
|
5
7
|
use crate::parser::project::ResolvedPaths;
|
|
6
8
|
use crate::parser::yaml_schema;
|
|
@@ -232,13 +234,13 @@ fn resolve_sql_to_label(
|
|
|
232
234
|
let project_dir = normalize_path(project_dir);
|
|
233
235
|
let relative = abs_path.strip_prefix(&project_dir).ok()?;
|
|
234
236
|
// Normalize to forward slashes once (loop-invariant) for Windows compatibility
|
|
235
|
-
let rel_str = relative.
|
|
237
|
+
let rel_str = relative.to_slash_lossy();
|
|
236
238
|
|
|
237
239
|
graph.node_indices().find_map(|idx| {
|
|
238
240
|
let node = &graph[idx];
|
|
239
241
|
match &node.file_path {
|
|
240
242
|
Some(node_path) => {
|
|
241
|
-
let node_str = node_path.
|
|
243
|
+
let node_str = node_path.to_slash_lossy();
|
|
242
244
|
if node_str == rel_str {
|
|
243
245
|
Some(node.label.clone())
|
|
244
246
|
} else {
|