Skip to content

Commit 4bbc978

Browse files
authored
Merge pull request #1749 from DeusData/feat/sql-table-view-lineage
feat(sql): first-class Table/View nodes and FROM/JOIN lineage edges
2 parents 46ae198 + 7ba84e3 commit 4bbc978

19 files changed

Lines changed: 452 additions & 85 deletions

‎internal/cbm/cbm.h‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,4 +753,17 @@ void cbm_extract_k8s(CBMExtractCtx *ctx);
753753
// `label` may be NULL (returns false). Defined in helpers.c.
754754
bool cbm_label_is_type_like(const char *label);
755755

756+
// True for data-relation labels (Table, View — SQL DDL). Relations resolve as
757+
// lineage targets only: registry members, but never type-like and never valid
758+
// CALLS/THROWS/READS/WRITES targets. `label` may be NULL. Defined in helpers.c.
759+
bool cbm_label_is_relation(const char *label);
760+
761+
// True for labels admitted to the cross-file name registry: Function, Method,
762+
// every type-like container, Variable, Field, and the relation labels. Single
763+
// source of truth for registry seeding — the full (pass_definitions.c),
764+
// parallel (pass_parallel.c) and incremental (pipeline_incremental.c) pipelines
765+
// all seed through this predicate so their registries never diverge.
766+
// `label` may be NULL (returns false). Defined in helpers.c.
767+
bool cbm_label_is_registry_symbol(const char *label);
768+
756769
#endif // CBM_H

‎internal/cbm/extract_defs.c‎

Lines changed: 84 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -275,12 +275,31 @@ static TSNode resolve_ocaml_func_name(TSNode node) {
275275
return null_node;
276276
}
277277

278-
// SQL: resolve create_function name from object_reference→identifier or direct identifier.
278+
// Last identifier (DFS pre-order) under `node`. For a schema-qualified
279+
// object_reference (schema.table) this is the table name; the schema prefix is
280+
// ignored. Leaves *found false and returns `best` unchanged if none is present.
281+
static TSNode sql_last_identifier(TSNode node, TSNode best, bool *found) {
282+
if (strcmp(ts_node_type(node), "identifier") == 0) {
283+
best = node;
284+
*found = true;
285+
}
286+
uint32_t cc = ts_node_child_count(node);
287+
for (uint32_t i = 0; i < cc; i++) {
288+
best = sql_last_identifier(ts_node_child(node, i), best, found);
289+
}
290+
return best;
291+
}
292+
293+
// SQL: resolve create_function / create_table / create_view name. The name sits
294+
// on an object_reference; for a schema-qualified name (schema.table) take the
295+
// last identifier (the table), not the first (the schema).
279296
static TSNode resolve_sql_func_name(TSNode node) {
280297
TSNode obj_ref = cbm_find_child_by_kind(node, "object_reference");
281298
if (!ts_node_is_null(obj_ref)) {
282-
TSNode id = cbm_find_child_by_kind(obj_ref, "identifier");
283-
if (!ts_node_is_null(id)) {
299+
bool found = false;
300+
TSNode empty = {0};
301+
TSNode id = sql_last_identifier(obj_ref, empty, &found);
302+
if (found) {
284303
return id;
285304
}
286305
}
@@ -4024,13 +4043,75 @@ static bool extract_config_class_def(CBMExtractCtx *ctx, TSNode node, const char
40244043
return true;
40254044
}
40264045

4046+
// Collect FROM/JOIN table references (tree-sitter-sql `relation` nodes) anywhere
4047+
// under `node` and emit them as usages scoped to enclosing_qn. pass_usages then
4048+
// resolves each ref_name to the referenced Table/View def and creates a USAGE
4049+
// lineage edge (e.g. a view -> the tables it selects from). Emitting them here
4050+
// (rather than via the generic identifier walker) sets the correct enclosing
4051+
// scope and bypasses the is_definition_name suppression that drops them.
4052+
static void collect_sql_relation_usages(CBMExtractCtx *ctx, TSNode node, const char *enclosing_qn) {
4053+
if (strcmp(ts_node_type(node), "relation") == 0) {
4054+
TSNode nm = resolve_sql_func_name(node); // object_reference -> identifier
4055+
if (!ts_node_is_null(nm)) {
4056+
char *tname = cbm_node_text(ctx->arena, nm, ctx->source);
4057+
if (tname && tname[0]) {
4058+
CBMUsage usage = {0};
4059+
usage.ref_name = tname;
4060+
usage.enclosing_func_qn = enclosing_qn;
4061+
cbm_usages_push(&ctx->result->usages, ctx->arena, usage);
4062+
}
4063+
}
4064+
}
4065+
uint32_t n = ts_node_child_count(node);
4066+
for (uint32_t i = 0; i < n; i++) {
4067+
collect_sql_relation_usages(ctx, ts_node_child(node, i), enclosing_qn);
4068+
}
4069+
}
4070+
4071+
// Handle SQL DDL relation defs: CREATE TABLE / VIEW / MATERIALIZED VIEW become
4072+
// first-class Table/View nodes rather than generic Variable nodes. The relation
4073+
// name sits on an object_reference child (the same shape create_function uses),
4074+
// so resolve_sql_func_name locates it. Also emits FROM/JOIN dependencies as
4075+
// usages so lineage edges form. Returns true if handled.
4076+
static bool extract_sql_ddl_class_def(CBMExtractCtx *ctx, TSNode node, const char *kind) {
4077+
if (ctx->language != CBM_LANG_SQL) {
4078+
return false;
4079+
}
4080+
const char *label;
4081+
if (strcmp(kind, "create_table") == 0) {
4082+
label = "Table";
4083+
} else if (strcmp(kind, "create_view") == 0 || strcmp(kind, "create_materialized_view") == 0) {
4084+
label = "View";
4085+
} else {
4086+
return false;
4087+
}
4088+
TSNode name_node = resolve_sql_func_name(node);
4089+
if (ts_node_is_null(name_node)) {
4090+
return false;
4091+
}
4092+
char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
4093+
if (!name || !name[0]) {
4094+
return false;
4095+
}
4096+
push_simple_class_def(ctx, node, name, label);
4097+
// Must match push_simple_class_def's QN exactly (qn_safe_segment included)
4098+
// or pass_usages cannot find the enclosing def for the lineage source.
4099+
const char *qn =
4100+
cbm_fqn_compute(ctx->arena, ctx->project, ctx->rel_path, qn_safe_segment(ctx->arena, name));
4101+
collect_sql_relation_usages(ctx, node, qn);
4102+
return true;
4103+
}
4104+
40274105
static void extract_class_def(CBMExtractCtx *ctx, TSNode node, const CBMLangSpec *spec) {
40284106
CBMArena *a = ctx->arena;
40294107
const char *kind = ts_node_type(node);
40304108

40314109
if (extract_config_class_def(ctx, node, kind)) {
40324110
return;
40334111
}
4112+
if (extract_sql_ddl_class_def(ctx, node, kind)) {
4113+
return;
4114+
}
40344115

40354116
TSNode name_node = ts_node_child_by_field_name(node, TS_FIELD("name"));
40364117
// ObjC: class name is first identifier child

‎internal/cbm/helpers.c‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,31 @@ bool cbm_label_is_type_like(const char *label) {
166166
strcmp(label, "Type") == 0 || strcmp(label, "Trait") == 0;
167167
}
168168

169+
// True when `label` names a data relation (SQL CREATE TABLE / CREATE VIEW).
170+
// Relations live in the registry so FROM/JOIN lineage can resolve, but they are
171+
// deliberately NOT type-like: they must never satisfy inheritance, impl-receiver,
172+
// semantic-type, or LSP-registrar lookups, and resolver fallbacks treat them as
173+
// lineage-only targets (see cbm_label_is_registry_symbol call sites).
174+
bool cbm_label_is_relation(const char *label) {
175+
if (!label) {
176+
return false;
177+
}
178+
return strcmp(label, "Table") == 0 || strcmp(label, "View") == 0;
179+
}
180+
181+
// True when `label` belongs in the cross-file name registry (see cbm.h). Single
182+
// source of truth for every registry-seeding site — full, parallel and
183+
// incremental pipelines MUST admit the same set or an incremental re-resolve
184+
// diverges from a clean full reindex.
185+
bool cbm_label_is_registry_symbol(const char *label) {
186+
if (!label) {
187+
return false;
188+
}
189+
return strcmp(label, "Function") == 0 || strcmp(label, "Method") == 0 ||
190+
cbm_label_is_type_like(label) || strcmp(label, "Variable") == 0 ||
191+
strcmp(label, "Field") == 0 || cbm_label_is_relation(label);
192+
}
193+
169194
bool cbm_is_keyword(const char *name, CBMLanguage lang) {
170195
if (!name || !name[0]) {
171196
return true;

‎internal/cbm/lang_specs.c‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -656,13 +656,17 @@ static const char *hcl_call_types[] = {"function_call", NULL};
656656
static const char *hcl_var_types[] = {"attribute", NULL};
657657

658658
// ==================== SQL ====================
659-
static const char *sql_func_types[] = {"create_function", "function_declaration", NULL};
659+
static const char *sql_func_types[] = {"create_function", "function_declaration",
660+
"create_procedure", NULL};
660661
static const char *sql_field_types[] = {"column_definition", NULL};
661-
static const char *sql_class_types[] = {"custom_type", NULL};
662+
// create_table/create_view route through the class-def path where
663+
// extract_sql_ddl_class_def turns them into first-class Table/View nodes
664+
// (previously they were generic Variable nodes via sql_var_types).
665+
static const char *sql_class_types[] = {"custom_type", "create_table", "create_view",
666+
"create_materialized_view", NULL};
662667
static const char *sql_module_types[] = {"program", NULL};
663668
static const char *sql_call_types[] = {"invocation", NULL};
664669
static const char *sql_branch_types[] = {"if_statement", "case_expression", NULL};
665-
static const char *sql_var_types[] = {"create_table", "create_view", NULL};
666670

667671
// ==================== DOCKERFILE ====================
668672
static const char *dockerfile_module_types[] = {"source_file", NULL};
@@ -1840,7 +1844,7 @@ static const CBMLangSpec lang_specs[CBM_LANG_COUNT] = {
18401844
// CBM_LANG_SQL
18411845
[CBM_LANG_SQL] = {CBM_LANG_SQL, sql_func_types, sql_class_types, sql_field_types,
18421846
sql_module_types, sql_call_types, empty_types, empty_types, sql_branch_types,
1843-
sql_var_types, empty_types, empty_types, NULL, empty_types, NULL, NULL,
1847+
empty_types, empty_types, empty_types, NULL, empty_types, NULL, NULL,
18441848
tree_sitter_sql, NULL},
18451849

18461850
// CBM_LANG_DOCKERFILE

‎src/foundation/constants.h‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,5 +108,8 @@ enum { SKIP_ONE = 1, PAIR_LEN = 2 };
108108
#define CBM_SQL_TYPE_LIKE_LABELS "'Class','Struct','Interface','Enum','Type','Trait'"
109109
#define CBM_SQL_CALLABLE_LABELS "'Function','Method'"
110110
#define CBM_SQL_CALLABLE_OR_TYPE_LABELS CBM_SQL_CALLABLE_LABELS "," CBM_SQL_TYPE_LIKE_LABELS
111+
/* SQL mirror of cbm_label_is_relation() (Table/View — data-lineage nodes),
112+
* pinned by tests/test_store_nodes.c the same way as the sets above. */
113+
#define CBM_SQL_RELATION_LABELS "'Table','View'"
111114

112115
#endif /* CBM_CONSTANTS_H */

‎src/mcp/mcp.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3017,6 +3017,9 @@ static char *bm25_search(cbm_store_t *store, const char *project, const char *qu
30173017
" - CASE WHEN n.label IN ('Function','Method') THEN 10.0 "
30183018
" WHEN n.label = 'Route' THEN 8.0 "
30193019
" WHEN n.label IN (" CBM_SQL_TYPE_LIKE_LABELS ") THEN 5.0 "
3020+
/* Relations rank with the type tier: a table IS the schema container
3021+
* a data question is looking for (findability-first). */
3022+
" WHEN n.label IN (" CBM_SQL_RELATION_LABELS ") THEN 5.0 "
30203023
" ELSE 0.0 END) AS rank "
30213024
"FROM ("
30223025
" SELECT rowid, bm25(nodes_fts) AS base_rank"

‎src/pipeline/lsp_surface.c‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include <stdlib.h>
2222
#include <string.h>
2323

24+
#include "cbm.h" /* cbm_label_is_relation — reg-only surface membership */
2425
#include "foundation/log.h"
2526
#include "foundation/sha256.h"
2627
#include "yyjson/yyjson.h"
@@ -30,12 +31,13 @@ enum { SURFACE_CODEC_VERSION = 1 };
3031
/* Labels the incremental name registry serves that pxc_map_label does NOT
3132
* carry into the CBMLSPDef set. Their (name, qn, label) triple must still
3233
* participate in the surface hash, or renaming one would slip past the
33-
* early cutoff while stale references to it survive in dependent files.
34-
* KEEP IN SYNC with pxc_map_label (pass_lsp_cross.c) and
35-
* incr_label_is_registry_symbol (pipeline_incremental.c); the codec unit
36-
* test cross-checks the three. */
34+
* early cutoff while stale references to it survive in dependent files —
35+
* for Table/View that means a renamed table keeping stale FROM/JOIN lineage
36+
* edges from dependent SQL files. KEEP IN SYNC with pxc_map_label
37+
* (pass_lsp_cross.c) and incr_label_is_registry_symbol
38+
* (pipeline_incremental.c); the codec unit test cross-checks the three. */
3739
static bool surface_reg_only_label(const char *label) {
38-
return label && strcmp(label, "Field") == 0;
40+
return label && (strcmp(label, "Field") == 0 || cbm_label_is_relation(label));
3941
}
4042

4143
static void add_str_or_null(yyjson_mut_doc *doc, yyjson_mut_val *obj, const char *key,

‎src/pipeline/pass_definitions.c‎

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -327,18 +327,12 @@ static void process_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *def, const
327327
int64_t node_id = cbm_gbuf_upsert_node(
328328
ctx->gbuf, def->label ? def->label : "Function", def->name, def->qualified_name,
329329
def->file_path ? def->file_path : rel, (int)def->start_line, (int)def->end_line, props);
330-
/* Register callable symbols + every type-like container (Class/Struct/
331-
* Interface/Enum/Type/Trait). Type-like defs must be in the registry so
332-
* `class Foo : IBar` (INHERITS), `impl Trait for S` (IMPLEMENTS), and method/
333-
* field resolution can reach them — Struct included so Rust/Go/Swift/D structs
334-
* resolve as type targets just as a Class did. Variable/Field defs are also
335-
* registered so pass_usages.c can resolve READS/WRITES accesses (rw->var_name)
336-
* to a Variable/Field node QN.
337-
* KEEP IN SYNC with pass_parallel.c and pipeline_incremental.c's seed sets. */
338-
if (node_id > 0 && def->label &&
339-
(strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
340-
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
341-
strcmp(def->label, "Field") == 0)) {
330+
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
331+
* (helpers.c): callables + type-like containers (INHERITS/IMPLEMENTS/method/
332+
* field resolution), Variable/Field (READS/WRITES resolution), and Table/View
333+
* (SQL FROM/JOIN lineage). pass_parallel.c and pipeline_incremental.c seed
334+
* through the same predicate, so the three registries cannot diverge. */
335+
if (node_id > 0 && cbm_label_is_registry_symbol(def->label)) {
342336
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
343337
}
344338
char *file_qn = cbm_pipeline_fqn_compute(ctx->project_name, rel, "__file__");

‎src/pipeline/pass_parallel.c‎

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,14 +1200,9 @@ static int register_and_link_def(cbm_pipeline_ctx_t *ctx, const CBMDefinition *d
12001200
if (!def->name || !def->qualified_name || !def->label) {
12011201
return 0;
12021202
}
1203-
/* Register callable symbols + every type-like container (Class/Struct/
1204-
* Interface/Enum/Type/Trait) — see pass_definitions.c for rationale. Struct
1205-
* included so Rust/Go/Swift/D structs resolve as type targets. Variable/Field
1206-
* defs are registered too so READS/WRITES can resolve.
1207-
* KEEP IN SYNC with pass_definitions.c and pipeline_incremental.c. */
1208-
if (strcmp(def->label, "Function") == 0 || strcmp(def->label, "Method") == 0 ||
1209-
cbm_label_is_type_like(def->label) || strcmp(def->label, "Variable") == 0 ||
1210-
strcmp(def->label, "Field") == 0) {
1203+
/* Registry membership is defined ONCE by cbm_label_is_registry_symbol
1204+
* (helpers.c) — see pass_definitions.c for the per-label rationale. */
1205+
if (cbm_label_is_registry_symbol(def->label)) {
12111206
cbm_registry_add(ctx->registry, def->name, def->qualified_name, def->label);
12121207
(*reg_entries)++;
12131208
}
@@ -2626,8 +2621,17 @@ static void resolve_file_usages(resolve_ctx_t *rc, resolve_worker_state_t *ws,
26262621
if (semantic_reference) {
26272622
continue;
26282623
}
2629-
cbm_resolution_t res = cbm_registry_resolve(rc->registry, usage->ref_name, module_qn,
2630-
imp_keys, imp_vals, imp_count);
2624+
/* SQL usages are FROM/JOIN lineage refs and may bind Table/View
2625+
* targets (cbm_registry_resolve_lineage); every other language
2626+
* resolves through the default variant, whose central relation
2627+
* veto keeps same-named code identifiers out of the lineage layer.
2628+
* Must mirror the sequential twin (pass_usages.c) exactly. */
2629+
cbm_resolution_t res =
2630+
(lang == CBM_LANG_SQL)
2631+
? cbm_registry_resolve_lineage(rc->registry, usage->ref_name, module_qn,
2632+
imp_keys, imp_vals, imp_count)
2633+
: cbm_registry_resolve(rc->registry, usage->ref_name, module_qn, imp_keys,
2634+
imp_vals, imp_count);
26312635
if (!res.qualified_name || res.qualified_name[0] == '\0') {
26322636
continue;
26332637
}

‎src/pipeline/pass_usages.c‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -192,8 +192,16 @@ static int resolve_usage_edges(cbm_pipeline_ctx_t *ctx, const CBMFileResult *res
192192
if (semantic_reference) {
193193
continue;
194194
}
195-
cbm_resolution_t res = cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn,
196-
imp_keys, imp_vals, imp_count);
195+
/* SQL usages are FROM/JOIN lineage refs and may bind Table/View
196+
* targets (cbm_registry_resolve_lineage); every other language
197+
* resolves through the default variant, whose central relation
198+
* veto keeps same-named code identifiers out of the lineage layer. */
199+
cbm_resolution_t res =
200+
(lang == CBM_LANG_SQL)
201+
? cbm_registry_resolve_lineage(ctx->registry, usage->ref_name, module_qn,
202+
imp_keys, imp_vals, imp_count)
203+
: cbm_registry_resolve(ctx->registry, usage->ref_name, module_qn, imp_keys,
204+
imp_vals, imp_count);
197205
if (!res.qualified_name || res.qualified_name[0] == '\0') {
198206
continue;
199207
}

0 commit comments

Comments
 (0)