From 9e73ae201cb11e0d52078580b0e4876120223fdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Gu=C3=AD=C3=B1ez?= Date: Thu, 22 Oct 2026 23:06:23 -0300 Subject: [PATCH 1/2] compiler: keep sql_table column types that match the column name A column's label defaults to its name, so compileSQLTable blanked the type whenever the two matched, including when the label was set explicitly (date: date). Record explicit labels and only blank the type when there is none. Fixes #1960 --- ci/release/changelogs/next.md | 2 + d2compiler/compile.go | 10 +- d2compiler/compile_test.go | 20 + .../sql_column_type_matches_name.exp.json | 480 ++++++++++++++++++ 4 files changed, 509 insertions(+), 3 deletions(-) create mode 100644 testdata/d2compiler/TestCompile/sql_column_type_matches_name.exp.json diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index d291ced933..c88500cda0 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -51,6 +51,8 @@ - Limit wildcard edge connections and selectors to bounded expansion work, returning a clear error before excessive fanout consumes resources. [#2926](https://github.com/d2lang/d2/pull/2926) + - Keep `sql_table` column types that match the column name, such as + `date: date`, instead of rendering an empty type. - decoding and assets: - Cap decompressed URL-encoded D2 input at 16 MiB. [#2902](https://github.com/d2lang/d2/pull/2902) - Bound image references, locators, fetched and decoded bytes, cached data, and diff --git a/d2compiler/compile.go b/d2compiler/compile.go index a871692b87..993193abf8 100644 --- a/d2compiler/compile.go +++ b/d2compiler/compile.go @@ -421,6 +421,7 @@ type compiler struct { err *d2parser.ParseError activeClassMaps map[*d2ir.Map]struct{} classMapsByRoot map[*d2ir.Map]map[string]*d2ir.Map + explicitLabels map[*d2graph.Attributes]struct{} ctx context.Context fatalErr error } @@ -793,6 +794,10 @@ func (c *compiler) compileLabel(attrs *d2graph.Attributes, f d2ir.Node) { attrs.Label.Value = scalar.ScalarString() } attrs.Label.MapKey = f.LastPrimaryKey() + if c.explicitLabels == nil { + c.explicitLabels = make(map[*d2graph.Attributes]struct{}) + } + c.explicitLabels[attrs] = struct{}{} } func (c *compiler) compilePosition(attrs *d2graph.Attributes, f *d2ir.Field) { @@ -1464,9 +1469,8 @@ func (c *compiler) compileSQLTable(obj *d2graph.Object) { return } typ := col.Label.Value - if typ == col.IDVal { - // Not great, AST should easily allow specifying alternate primary field - // as an explicit label should change the name. + // A column without an explicit label defaults it to its ID, which is not a type. + if _, ok := c.explicitLabels[&col.Attributes]; !ok && typ == col.IDVal { typ = "" } d2Col := d2target.SQLColumn{ diff --git a/d2compiler/compile_test.go b/d2compiler/compile_test.go index 0be7759ced..23bd1ca462 100644 --- a/d2compiler/compile_test.go +++ b/d2compiler/compile_test.go @@ -2857,6 +2857,26 @@ x.y -> a.b: { assert.String(t, `Is()`, g.Objects[0].SQLTable.Columns[1].Name.Label) }, }, + { + name: "sql_column_type_matches_name", + + text: `time: { + shape: sql_table + id + created {constraint: primary_key} + date: date + key: key + key {constraint: unique} +} +`, + assertions: func(t *testing.T, g *d2graph.Graph) { + cols := g.Objects[0].SQLTable.Columns + assert.String(t, ``, cols[0].Type.Label) + assert.String(t, ``, cols[1].Type.Label) + assert.String(t, `date`, cols[2].Type.Label) + assert.String(t, `key`, cols[3].Type.Label) + }, + }, { name: "nested_sql", diff --git a/testdata/d2compiler/TestCompile/sql_column_type_matches_name.exp.json b/testdata/d2compiler/TestCompile/sql_column_type_matches_name.exp.json new file mode 100644 index 0000000000..68b59206c8 --- /dev/null +++ b/testdata/d2compiler/TestCompile/sql_column_type_matches_name.exp.json @@ -0,0 +1,480 @@ +{ + "graph": { + "name": "", + "isFolderOnly": false, + "ast": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-8:0:121", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-7:1:120", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-0:4:4", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-0:4:4", + "value": [ + { + "string": "time", + "raw_string": "time" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:6:6-7:1:120", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,1:2:10-1:18:26", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,1:2:10-1:7:15", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,1:2:10-1:7:15", + "value": [ + { + "string": "shape", + "raw_string": "shape" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,1:9:17-1:18:26", + "value": [ + { + "string": "sql_table", + "raw_string": "sql_table" + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,2:2:29-2:4:31", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,2:2:29-2:4:31", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,2:2:29-2:4:31", + "value": [ + { + "string": "id", + "raw_string": "id" + } + ] + } + } + ] + }, + "primary": {}, + "value": {} + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:2:34-3:35:67", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:2:34-3:9:41", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:2:34-3:9:41", + "value": [ + { + "string": "created", + "raw_string": "created" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:10:42-3:35:67", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:11:43-3:34:66", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:11:43-3:21:53", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:11:43-3:21:53", + "value": [ + { + "string": "constraint", + "raw_string": "constraint" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,3:23:55-3:34:66", + "value": [ + { + "string": "primary_key", + "raw_string": "primary_key" + } + ] + } + } + } + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,4:2:70-4:12:80", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,4:2:70-4:6:74", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,4:2:70-4:6:74", + "value": [ + { + "string": "date", + "raw_string": "date" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,4:8:76-4:12:80", + "value": [ + { + "string": "date", + "raw_string": "date" + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,5:2:83-5:10:91", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,5:2:83-5:5:86", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,5:2:83-5:5:86", + "value": [ + { + "string": "key", + "raw_string": "key" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,5:7:88-5:10:91", + "value": [ + { + "string": "key", + "raw_string": "key" + } + ] + } + } + } + }, + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:2:94-6:26:118", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:2:94-6:5:97", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:2:94-6:5:97", + "value": [ + { + "string": "key", + "raw_string": "key" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "map": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:6:98-6:26:118", + "nodes": [ + { + "map_key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:7:99-6:25:117", + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:7:99-6:17:109", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:7:99-6:17:109", + "value": [ + { + "string": "constraint", + "raw_string": "constraint" + } + ] + } + } + ] + }, + "primary": {}, + "value": { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,6:19:111-6:25:117", + "value": [ + { + "string": "unique", + "raw_string": "unique" + } + ] + } + } + } + } + ] + } + } + } + } + ] + } + } + } + } + ] + }, + "root": { + "id": "", + "id_val": "", + "attributes": { + "label": { + "value": "" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "iconStyle": {}, + "near_key": null, + "shape": { + "value": "" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + }, + "edges": null, + "objects": [ + { + "id": "time", + "id_val": "time", + "references": [ + { + "key": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-0:4:4", + "path": [ + { + "unquoted_string": { + "range": "d2/testdata/d2compiler/TestCompile/sql_column_type_matches_name.d2,0:0:0-0:4:4", + "value": [ + { + "string": "time", + "raw_string": "time" + } + ] + } + } + ] + }, + "key_path_index": 0, + "map_key_edge_index": -1 + } + ], + "sql_table": { + "columns": [ + { + "name": { + "label": "id", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "type": { + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "created", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "type": { + "label": "", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "constraint": [ + "primary_key" + ], + "reference": "" + }, + { + "name": { + "label": "date", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "type": { + "label": "date", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "constraint": null, + "reference": "" + }, + { + "name": { + "label": "key", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "type": { + "label": "key", + "fontSize": 0, + "fontFamily": "", + "language": "", + "color": "", + "italic": false, + "bold": false, + "underline": false, + "labelWidth": 0, + "labelHeight": 0 + }, + "constraint": [ + "unique" + ], + "reference": "" + } + ] + }, + "attributes": { + "label": { + "value": "time" + }, + "labelDimensions": { + "width": 0, + "height": 0 + }, + "style": {}, + "iconStyle": {}, + "near_key": null, + "shape": { + "value": "sql_table" + }, + "direction": { + "value": "" + }, + "constraint": null + }, + "zIndex": 0 + } + ] + }, + "err": null +} From e961cd93a5a36262b3a5aca2a1e0d40a8758c686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Francisco=20Gu=C3=AD=C3=B1ez?= Date: Thu, 22 Oct 2026 23:06:45 -0300 Subject: [PATCH 2/2] changelog: link #2950 --- ci/release/changelogs/next.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/release/changelogs/next.md b/ci/release/changelogs/next.md index c88500cda0..550f125cd6 100644 --- a/ci/release/changelogs/next.md +++ b/ci/release/changelogs/next.md @@ -52,7 +52,7 @@ returning a clear error before excessive fanout consumes resources. [#2926](https://github.com/d2lang/d2/pull/2926) - Keep `sql_table` column types that match the column name, such as - `date: date`, instead of rendering an empty type. + `date: date`, instead of rendering an empty type. [#2950](https://github.com/d2lang/d2/pull/2950) - decoding and assets: - Cap decompressed URL-encoded D2 input at 16 MiB. [#2902](https://github.com/d2lang/d2/pull/2902) - Bound image references, locators, fetched and decoded bytes, cached data, and