a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
328 lines
10 KiB
Go
328 lines
10 KiB
Go
package languages
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
)
|
|
|
|
func nodeByID(nodes []*graph.Node, id string) *graph.Node {
|
|
for _, n := range nodes {
|
|
if n.ID == id {
|
|
return n
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func colNames(nodes []*graph.Node) []string {
|
|
var out []string
|
|
for _, n := range nodes {
|
|
if n.Kind == graph.KindColumn {
|
|
out = append(out, n.Name)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// --- dbt SQL models -------------------------------------------------------
|
|
|
|
func TestDbt_SQLModel_RefSourceConfigColumns(t *testing.T) {
|
|
src := []byte(`{{ config(materialized='table', schema='analytics') }}
|
|
|
|
with base as (
|
|
select * from {{ source('raw', 'events') }}
|
|
)
|
|
select
|
|
e.event_id,
|
|
e.user_id,
|
|
count(*) as event_count
|
|
from base e
|
|
join {{ ref('dim_users') }} u on u.id = e.user_id
|
|
group by 1, 2
|
|
`)
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("models/staging/stg_events.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
model := nodeByID(result.Nodes, "dbt::model::stg_events")
|
|
require.NotNil(t, model, "model node should exist")
|
|
assert.Equal(t, graph.KindTable, model.Kind)
|
|
assert.Equal(t, "stg_events", model.Name)
|
|
assert.Equal(t, "dbt", model.Meta["framework"])
|
|
assert.Equal(t, "dbt_model", model.Meta["sql_type"])
|
|
assert.Equal(t, "model", model.Meta["resource_type"])
|
|
assert.Equal(t, "table", model.Meta["materialized"])
|
|
assert.Equal(t, "analytics", model.Meta["schema"])
|
|
|
|
// Source referenced via source() is materialised as a KindTable.
|
|
srcNode := nodeByID(result.Nodes, "dbt::source::raw.events")
|
|
require.NotNil(t, srcNode, "source node should exist")
|
|
assert.Equal(t, "dbt_source", srcNode.Meta["sql_type"])
|
|
|
|
// Columns from the model's final SELECT projection.
|
|
cols := colNames(result.Nodes)
|
|
assert.ElementsMatch(t, []string{"event_id", "user_id", "event_count"}, cols)
|
|
|
|
memberEdges := edgesOfKind(result.Edges, graph.EdgeMemberOf)
|
|
assert.Len(t, memberEdges, 3)
|
|
for _, me := range memberEdges {
|
|
assert.Equal(t, "dbt::model::stg_events", me.To)
|
|
}
|
|
|
|
// Lineage: one ref() + one source() dependency.
|
|
deps := edgesOfKind(result.Edges, graph.EdgeDependsOn)
|
|
require.Len(t, deps, 2)
|
|
targets := map[string]string{}
|
|
for _, d := range deps {
|
|
targets[d.To], _ = d.Meta["link"].(string)
|
|
}
|
|
assert.Equal(t, "ref", targets["dbt::model::dim_users"])
|
|
assert.Equal(t, "source", targets["dbt::source::raw.events"])
|
|
}
|
|
|
|
func TestDbt_SQLModel_PureSQLByPath(t *testing.T) {
|
|
// A dbt model need not contain any Jinja — path + a query-shaped
|
|
// body is enough to classify it.
|
|
src := []byte("select id, name, email from some_upstream\n")
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("models/marts/dim_users.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
model := nodeByID(result.Nodes, "dbt::model::dim_users")
|
|
require.NotNil(t, model)
|
|
assert.Equal(t, "view", model.Meta["materialized"]) // default
|
|
assert.ElementsMatch(t, []string{"id", "name", "email"}, colNames(result.Nodes))
|
|
}
|
|
|
|
func TestDbt_SQLModel_Snapshot(t *testing.T) {
|
|
src := []byte(`{% snapshot orders_snapshot %}
|
|
{{ config(target_schema='snapshots', unique_key='id', strategy='timestamp') }}
|
|
select id, status, updated_at from {{ source('jaffle', 'orders') }}
|
|
{% endsnapshot %}
|
|
`)
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("snapshots/orders_snapshot.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
model := nodeByID(result.Nodes, "dbt::model::orders_snapshot")
|
|
require.NotNil(t, model)
|
|
assert.Equal(t, "snapshot", model.Meta["resource_type"])
|
|
assert.Equal(t, "snapshot", model.Meta["materialized"])
|
|
assert.ElementsMatch(t, []string{"id", "status", "updated_at"}, colNames(result.Nodes))
|
|
|
|
deps := edgesOfKind(result.Edges, graph.EdgeDependsOn)
|
|
require.Len(t, deps, 1)
|
|
assert.Equal(t, "dbt::source::jaffle.orders", deps[0].To)
|
|
}
|
|
|
|
func TestDbt_SQLModel_TwoArgRef(t *testing.T) {
|
|
src := []byte("select 1 as id from {{ ref('analytics', 'dim_date') }}\n")
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("models/x.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
deps := edgesOfKind(result.Edges, graph.EdgeDependsOn)
|
|
require.Len(t, deps, 1)
|
|
assert.Equal(t, "dbt::model::dim_date", deps[0].To)
|
|
assert.Equal(t, "analytics", deps[0].Meta["ref_package"])
|
|
}
|
|
|
|
// --- SQLMesh SQL models ---------------------------------------------------
|
|
|
|
func TestSQLMesh_Model_BlockPropsAndLineage(t *testing.T) {
|
|
src := []byte(`MODEL (
|
|
name sushi.customers,
|
|
kind FULL,
|
|
cron '@daily',
|
|
grain customer_id,
|
|
owner 'data-team'
|
|
);
|
|
|
|
SELECT
|
|
customer_id,
|
|
cast(zip AS TEXT) AS zip_code,
|
|
status
|
|
FROM sushi.raw_customers
|
|
`)
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("models/customers.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
model := nodeByID(result.Nodes, "sqlmesh::model::sushi.customers")
|
|
require.NotNil(t, model)
|
|
assert.Equal(t, "customers", model.Name)
|
|
assert.Equal(t, "sqlmesh", model.Meta["framework"])
|
|
assert.Equal(t, "sushi", model.Meta["schema"])
|
|
assert.Equal(t, "full", model.Meta["materialized"])
|
|
assert.Equal(t, "@daily", model.Meta["cron"])
|
|
assert.Equal(t, "customer_id", model.Meta["grain"])
|
|
assert.Equal(t, "data-team", model.Meta["owner"])
|
|
|
|
assert.ElementsMatch(t, []string{"customer_id", "zip_code", "status"}, colNames(result.Nodes))
|
|
|
|
deps := edgesOfKind(result.Edges, graph.EdgeDependsOn)
|
|
require.Len(t, deps, 1)
|
|
assert.Equal(t, "sqlmesh::model::sushi.raw_customers", deps[0].To)
|
|
}
|
|
|
|
func TestSQLMesh_Model_ColumnsProperty(t *testing.T) {
|
|
src := []byte(`MODEL (
|
|
name analytics.orders,
|
|
kind INCREMENTAL_BY_TIME_RANGE (
|
|
time_column event_date
|
|
),
|
|
columns (
|
|
order_id INT,
|
|
total DECIMAL(10, 2),
|
|
created_at TIMESTAMP
|
|
)
|
|
);
|
|
|
|
SELECT order_id, total, created_at
|
|
FROM analytics.staging_orders
|
|
WHERE event_date BETWEEN @start_date AND @end_date
|
|
`)
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("models/orders.sql", src)
|
|
require.NoError(t, err)
|
|
|
|
model := nodeByID(result.Nodes, "sqlmesh::model::analytics.orders")
|
|
require.NotNil(t, model)
|
|
assert.Equal(t, "incremental_by_time_range", model.Meta["materialized"])
|
|
|
|
// Explicit columns() property wins; data_type is carried in Meta.
|
|
cols := nodesOfKind(result.Nodes, graph.KindColumn)
|
|
require.Len(t, cols, 3)
|
|
byName := map[string]*graph.Node{}
|
|
for _, c := range cols {
|
|
byName[c.Name] = c
|
|
}
|
|
require.Contains(t, byName, "order_id")
|
|
assert.Equal(t, "INT", byName["order_id"].Meta["data_type"])
|
|
assert.Equal(t, "DECIMAL(10, 2)", byName["total"].Meta["data_type"])
|
|
|
|
deps := edgesOfKind(result.Edges, graph.EdgeDependsOn)
|
|
require.Len(t, deps, 1)
|
|
assert.Equal(t, "sqlmesh::model::analytics.staging_orders", deps[0].To)
|
|
}
|
|
|
|
// --- dbt schema YAML ------------------------------------------------------
|
|
|
|
func TestDbt_SchemaYAML_ModelsAndSources(t *testing.T) {
|
|
src := []byte(`version: 2
|
|
|
|
models:
|
|
- name: stg_events
|
|
description: "Staged event data"
|
|
config:
|
|
materialized: view
|
|
columns:
|
|
- name: event_id
|
|
description: "Primary key"
|
|
data_type: integer
|
|
tests:
|
|
- unique
|
|
- not_null
|
|
- name: user_id
|
|
data_type: integer
|
|
|
|
sources:
|
|
- name: raw
|
|
schema: raw_data
|
|
tables:
|
|
- name: events
|
|
columns:
|
|
- name: id
|
|
- name: payload
|
|
data_type: json
|
|
`)
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("models/staging/schema.yml", src)
|
|
require.NoError(t, err)
|
|
|
|
// Generic top-level-keys fallback must NOT have run.
|
|
assert.Empty(t, nodesOfKind(result.Nodes, graph.KindVariable))
|
|
|
|
model := nodeByID(result.Nodes, "dbt::model::stg_events")
|
|
require.NotNil(t, model)
|
|
assert.Equal(t, graph.KindTable, model.Kind)
|
|
assert.Equal(t, "view", model.Meta["materialized"])
|
|
assert.Equal(t, "Staged event data", model.Meta["description"])
|
|
|
|
eventID := nodeByID(result.Nodes, "dbt::model::stg_events::event_id")
|
|
require.NotNil(t, eventID)
|
|
assert.Equal(t, graph.KindColumn, eventID.Kind)
|
|
assert.Equal(t, "integer", eventID.Meta["data_type"])
|
|
assert.Equal(t, "Primary key", eventID.Meta["description"])
|
|
assert.ElementsMatch(t, []string{"unique", "not_null"}, eventID.Meta["tests"])
|
|
|
|
source := nodeByID(result.Nodes, "dbt::source::raw.events")
|
|
require.NotNil(t, source)
|
|
assert.Equal(t, "dbt_source", source.Meta["sql_type"])
|
|
assert.Equal(t, "raw_data", source.Meta["schema"])
|
|
|
|
payload := nodeByID(result.Nodes, "dbt::source::raw.events::payload")
|
|
require.NotNil(t, payload)
|
|
assert.Equal(t, "json", payload.Meta["data_type"])
|
|
|
|
assert.Len(t, nodesOfKind(result.Nodes, graph.KindColumn), 4)
|
|
assert.Len(t, edgesOfKind(result.Edges, graph.EdgeMemberOf), 4)
|
|
}
|
|
|
|
func TestDbt_SchemaYAML_PlainConfigFallsThrough(t *testing.T) {
|
|
// A plain config YAML must not be mistaken for a dbt schema file —
|
|
// the generic top-level-keys walker should still run.
|
|
src := []byte(`server:
|
|
port: 8080
|
|
database:
|
|
host: localhost
|
|
`)
|
|
e := NewYAMLExtractor()
|
|
result, err := e.Extract("config/app.yml", src)
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, nodesOfKind(result.Nodes, graph.KindTable))
|
|
vars := nodesOfKind(result.Nodes, graph.KindVariable)
|
|
assert.Len(t, vars, 2) // server, database
|
|
}
|
|
|
|
// --- classification -------------------------------------------------------
|
|
|
|
func TestClassifySQLFile(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
path string
|
|
src string
|
|
want string
|
|
}{
|
|
{"sqlmesh model block", "models/a.sql", "MODEL (\n name a\n);\nSELECT 1", "sqlmesh"},
|
|
{"sqlmesh leading comment", "a.sql", "-- header\nMODEL (name a);\nSELECT 1", "sqlmesh"},
|
|
{"dbt ref marker", "x.sql", "select * from {{ ref('y') }}", "dbt"},
|
|
{"dbt config marker", "x.sql", "{{ config(materialized='table') }}\nselect 1", "dbt"},
|
|
{"dbt pure sql by path", "models/dim.sql", "select id from upstream", "dbt"},
|
|
{"plain DDL not dbt", "schema.sql", "CREATE TABLE foo (id int);", ""},
|
|
{"DDL under models dir not dbt", "models/foo.sql", "CREATE TABLE foo (id int);", ""},
|
|
{"plain query outside models", "scratch.sql", "select 1", ""},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
assert.Equal(t, tc.want, classifySQLFile(tc.path, []byte(tc.src)))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSQLExtractor_NonDbtUnaffected(t *testing.T) {
|
|
// Regression guard: a plain DDL file still goes through the generic
|
|
// SQL walk and produces a KindType, not a dbt model node.
|
|
src := []byte("CREATE TABLE users (id INTEGER PRIMARY KEY);")
|
|
e := NewSQLExtractor()
|
|
result, err := e.Extract("db/schema.sql", src)
|
|
require.NoError(t, err)
|
|
assert.Len(t, nodesOfKind(result.Nodes, graph.KindType), 1)
|
|
assert.Empty(t, nodesOfKind(result.Nodes, graph.KindTable))
|
|
}
|