liam icon indicating copy to clipboard operation
liam copied to clipboard

[Cloud Spanner] Table relations are not visualized in generated ERD by @liam-hq/cli (>= 0.5.9).

Open KaoruMuta opened this issue 6 months ago • 12 comments

Self Checks

  • [x] This is only for bug report, if you would like to ask a question, please head to Discussions.
  • [x] I have searched for existing issues search for existing issues, including closed ones.

Version Type

CLI Version (npm package)

Version (only for Self Hosted)

>= 0.5.9

Steps to reproduce

  1. Set up project with Cloud Spanner (emulator is fine)
  2. Run k1low/tbls to output schema.json which has schema information with relationships between tables. a. example command: SPANNER_EMULATOR_HOST=localhost:9010 tbls doc
  3. Run latest liam-hq/cli a. example command: npx @liam-hq/cli erd build --input schema.json --format tbls

Expected Behavior

Table relations are visualized. As of liam-hq/[email protected], it works like the attached screenshot in my playground.

Image

Actual Behavior

Table relations are not visualized like the attached screenshot in my playground.

Image

Additional Context

As far as I investigated, https://github.com/liam-hq/liam/pull/2156 (released as 0.5.9) tried to introduce the new mechanism to visualize relationship without schema.relationships by using foreign key relations. But, Cloud Spanner mostly uses interleave to create parent and children relationships instead of foreign keys. (ref)

Therefore, I think table relations are not visualized for now.

KaoruMuta avatar Jul 07 '25 14:07 KaoruMuta

If I need to provide another information, please let me know. Anyway, I also try to fix this issue.

KaoruMuta avatar Jul 07 '25 15:07 KaoruMuta

@KaoruMuta Thanks for creating the Issue! Probably a degrade of https://github.com/liam-hq/liam/pull/2156 🙏🏻 I am not familiar with Cloud Spanner interleave, but if you were able to view it originally, then interleave must be mapped to the schema.json structure in tbls, and Liam ERD's tbls parser may not be able to retrieve it. https://github.com/k1LoW/tbls/blob/5ad27e7eb1ead7eb2c733dc44a5a1d604fa3ba2b/spec/tbls.schema.json_schema.json

Also, until 0.5.8, was interleave shown as a foreign key in the TableDetail on the Liam ERD? Or was it not displayed in any way? I am wondering how it should be displayed.

Image

Pull Request is welcome! 🚀 If there is anything we can do to help, please call us!

MH4GF avatar Jul 08 '25 05:07 MH4GF

@MH4GF

Thanks for your reply!

was interleave shown as a foreign key in the TableDetail on the Liam ERD?

Yes, it is shown like that.

Image

In detail, schema.json generated by liam under dist directory drops these information. (Difference is attached below)

  • primary, unique field in columns json block
  • relationships json block

Also, I confirmed schema.json structure in tbls between v1.81.0 and latest (v1.86.1). But it has no difference.

  • https://raw.githubusercontent.com/k1LoW/tbls/v1.81.0/spec/tbls.schema.json_schema.json
  • https://raw.githubusercontent.com/k1LoW/tbls/v1.86.1/spec/tbls.schema.json_schema.json

Therefore, schema mapping between tbls and liam was succeeded, and we previously might be able to visualize the table relations by referring to relationships information.

I plan to continue to investigate and try to fix, but you have something to find, please let me know 🙏 Thank you!

schema.json difference under dist directory between 0.5.8 and >= 0.5.9
  • 0.5.8: docs/dist/schema.json
  • >= 0.5.9: docs/dist/schema.latest.json
➜ diff -u docs/dist/schema.json docs/dist/schema.latest.json       
--- docs/dist/schema.json       2025-07-07 23:53:52
+++ docs/dist/schema.latest.json        2025-07-09 08:20:56
@@ -10,8 +10,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "comment_id": {
@@ -20,8 +18,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "user_id": {
@@ -30,8 +26,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "content": {
@@ -40,8 +34,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "created_at": {
@@ -50,8 +42,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -77,8 +67,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "email": {
@@ -87,8 +75,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "username": {
@@ -97,8 +83,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "created_at": {
@@ -107,8 +91,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "updated_at": {
@@ -117,8 +99,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -144,8 +124,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "attachment_id": {
@@ -154,8 +132,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "user_id": {
@@ -164,8 +140,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "filename": {
@@ -174,8 +148,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "file_size": {
@@ -184,8 +156,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "mime_type": {
@@ -194,8 +164,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "storage_path": {
@@ -204,8 +172,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "uploaded_at": {
@@ -214,8 +180,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -241,8 +205,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "category_id": {
@@ -251,8 +213,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "name": {
@@ -261,8 +221,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "color": {
@@ -271,8 +229,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": false
         },
         "created_at": {
@@ -281,8 +237,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -308,8 +262,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "name": {
@@ -318,8 +270,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "created_at": {
@@ -328,8 +278,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -355,8 +303,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "user_id": {
@@ -365,8 +311,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "category_id": {
@@ -375,8 +319,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": false
         },
         "title": {
@@ -385,8 +327,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "description": {
@@ -395,8 +335,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": false
         },
         "status": {
@@ -405,8 +343,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "priority": {
@@ -415,8 +351,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "due_date": {
@@ -425,8 +359,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": false
         },
         "created_at": {
@@ -435,8 +367,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "updated_at": {
@@ -445,8 +375,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "completed_at": {
@@ -455,8 +383,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": false
         }
       },
@@ -507,8 +433,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "tag_id": {
@@ -517,8 +441,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         },
         "created_at": {
@@ -527,8 +449,6 @@
           "default": null,
           "check": null,
           "comment": null,
-          "primary": false,
-          "unique": false,
           "notNull": true
         }
       },
@@ -544,48 +464,5 @@
       },
       "constraints": {}
     }
-  },
-  "relationships": {
-    "todos_todo_id_to_comments_todo_id": {
-      "name": "todos_todo_id_to_comments_todo_id",
-      "primaryTableName": "todos",
-      "primaryColumnName": "todo_id",
-      "foreignTableName": "comments",
-      "foreignColumnName": "todo_id",
-      "cardinality": "ONE_TO_ONE",
-      "updateConstraint": "NO_ACTION",
-      "deleteConstraint": "CASCADE"
-    },
-    "todos_todo_id_to_attachments_todo_id": {
-      "name": "todos_todo_id_to_attachments_todo_id",
-      "primaryTableName": "todos",
-      "primaryColumnName": "todo_id",
-      "foreignTableName": "attachments",
-      "foreignColumnName": "todo_id",
-      "cardinality": "ONE_TO_ONE",
-      "updateConstraint": "NO_ACTION",
-      "deleteConstraint": "CASCADE"
-    },
-    "users_user_id_to_categories_user_id": {
-      "name": "users_user_id_to_categories_user_id",
-      "primaryTableName": "users",
-      "primaryColumnName": "user_id",
-      "foreignTableName": "categories",
-      "foreignColumnName": "user_id",
-      "cardinality": "ONE_TO_ONE",
-      "updateConstraint": "NO_ACTION",
-      "deleteConstraint": "CASCADE"
-    },
-    "todos_todo_id_to_todo_tags_todo_id": {
-      "name": "todos_todo_id_to_todo_tags_todo_id",
-      "primaryTableName": "todos",
-      "primaryColumnName": "todo_id",
-      "foreignTableName": "todo_tags",
-      "foreignColumnName": "todo_id",
-      "cardinality": "ONE_TO_ONE",
-      "updateConstraint": "NO_ACTION",
-      "deleteConstraint": "CASCADE"
-    }
-  },
-  "tableGroups": {}
+  }
 }
\ No newline at end of file
schema.json in dist directory (As of 0.5.8)
{
  "name": "projects/test-project/instances/test-instance/databases/todo-db",
  "tables": [
    {
      "name": "comments",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "todo_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "comment_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "user_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "content",
          "type": "STRING(MAX)",
          "nullable": false
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "IDX_comments_user_id_E6D097A830600D54",
          "def": "CREATE INDEX IDX_comments_user_id_E6D097A830600D54 ON comments (user_id)",
          "table": "comments",
          "columns": [
            "user_id"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(todo_id, comment_id)",
          "table": "comments",
          "columns": [
            "todo_id",
            "comment_id"
          ]
        },
        {
          "name": "INTERLEAVE",
          "type": "INTERLEAVE",
          "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE",
          "table": "comments",
          "referenced_table": "todos",
          "columns": [
            "todo_id",
            "comment_id"
          ],
          "referenced_columns": [
            "todo_id"
          ]
        }
      ]
    },
    {
      "name": "users",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "user_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "email",
          "type": "STRING(255)",
          "nullable": false
        },
        {
          "name": "username",
          "type": "STRING(100)",
          "nullable": false
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        },
        {
          "name": "updated_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "idx_users_email",
          "def": "CREATE UNIQUE INDEX idx_users_email ON users (email)",
          "table": "users",
          "columns": [
            "email"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(user_id)",
          "table": "users",
          "columns": [
            "user_id"
          ]
        }
      ]
    },
    {
      "name": "attachments",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "todo_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "attachment_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "user_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "filename",
          "type": "STRING(255)",
          "nullable": false
        },
        {
          "name": "file_size",
          "type": "INT64",
          "nullable": false
        },
        {
          "name": "mime_type",
          "type": "STRING(100)",
          "nullable": false
        },
        {
          "name": "storage_path",
          "type": "STRING(500)",
          "nullable": false
        },
        {
          "name": "uploaded_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "IDX_attachments_user_id_4C7D73049CFAC993",
          "def": "CREATE INDEX IDX_attachments_user_id_4C7D73049CFAC993 ON attachments (user_id)",
          "table": "attachments",
          "columns": [
            "user_id"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(todo_id, attachment_id)",
          "table": "attachments",
          "columns": [
            "todo_id",
            "attachment_id"
          ]
        },
        {
          "name": "INTERLEAVE",
          "type": "INTERLEAVE",
          "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE",
          "table": "attachments",
          "referenced_table": "todos",
          "columns": [
            "todo_id",
            "attachment_id"
          ],
          "referenced_columns": [
            "todo_id"
          ]
        }
      ]
    },
    {
      "name": "categories",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "user_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "category_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "name",
          "type": "STRING(100)",
          "nullable": false
        },
        {
          "name": "color",
          "type": "STRING(7)",
          "nullable": true
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "IDX_categories_category_id_U_BC14739FA19E28EE",
          "def": "CREATE UNIQUE INDEX IDX_categories_category_id_U_BC14739FA19E28EE ON categories (category_id)",
          "table": "categories",
          "columns": [
            "category_id"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(user_id, category_id)",
          "table": "categories",
          "columns": [
            "user_id",
            "category_id"
          ]
        },
        {
          "name": "INTERLEAVE",
          "type": "INTERLEAVE",
          "def": "INTERLEAVE IN PARENT users ON DELETE CASCADE",
          "table": "categories",
          "referenced_table": "users",
          "columns": [
            "user_id",
            "category_id"
          ],
          "referenced_columns": [
            "user_id"
          ]
        }
      ]
    },
    {
      "name": "tags",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "tag_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "name",
          "type": "STRING(50)",
          "nullable": false
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "idx_tags_name",
          "def": "CREATE UNIQUE INDEX idx_tags_name ON tags (name)",
          "table": "tags",
          "columns": [
            "name"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(tag_id)",
          "table": "tags",
          "columns": [
            "tag_id"
          ]
        }
      ]
    },
    {
      "name": "todos",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "todo_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "user_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "category_id",
          "type": "STRING(36)",
          "nullable": true
        },
        {
          "name": "title",
          "type": "STRING(255)",
          "nullable": false
        },
        {
          "name": "description",
          "type": "STRING(MAX)",
          "nullable": true
        },
        {
          "name": "status",
          "type": "STRING(20)",
          "nullable": false
        },
        {
          "name": "priority",
          "type": "STRING(10)",
          "nullable": false
        },
        {
          "name": "due_date",
          "type": "DATE",
          "nullable": true
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        },
        {
          "name": "updated_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        },
        {
          "name": "completed_at",
          "type": "TIMESTAMP",
          "nullable": true
        }
      ],
      "indexes": [
        {
          "name": "idx_todos_due_date",
          "def": "CREATE INDEX idx_todos_due_date ON todos (due_date)",
          "table": "todos",
          "columns": [
            "due_date"
          ]
        },
        {
          "name": "IDX_todos_user_id_4E872528DC8BBCA7",
          "def": "CREATE INDEX IDX_todos_user_id_4E872528DC8BBCA7 ON todos (user_id)",
          "table": "todos",
          "columns": [
            "user_id"
          ]
        },
        {
          "name": "IDX_todos_category_id_N_3B7864FE3ED3FF87",
          "def": "CREATE NULL_FILTERED INDEX IDX_todos_category_id_N_3B7864FE3ED3FF87 ON todos (category_id)",
          "table": "todos",
          "columns": [
            "category_id"
          ]
        },
        {
          "name": "idx_todos_user_status",
          "def": "CREATE INDEX idx_todos_user_status ON todos (user_id, status)",
          "table": "todos",
          "columns": [
            "user_id",
            "status"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(todo_id)",
          "table": "todos",
          "columns": [
            "todo_id"
          ]
        }
      ]
    },
    {
      "name": "todo_tags",
      "type": "BASIC TABLE",
      "columns": [
        {
          "name": "todo_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "tag_id",
          "type": "STRING(36)",
          "nullable": false
        },
        {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "nullable": false
        }
      ],
      "indexes": [
        {
          "name": "IDX_todo_tags_tag_id_8034AADD17A5A521",
          "def": "CREATE INDEX IDX_todo_tags_tag_id_8034AADD17A5A521 ON todo_tags (tag_id)",
          "table": "todo_tags",
          "columns": [
            "tag_id"
          ]
        }
      ],
      "constraints": [
        {
          "name": "PRIMARY_KEY",
          "type": "PRIMARY_KEY",
          "def": "PRIMARY KEY(todo_id, tag_id)",
          "table": "todo_tags",
          "columns": [
            "todo_id",
            "tag_id"
          ]
        },
        {
          "name": "INTERLEAVE",
          "type": "INTERLEAVE",
          "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE",
          "table": "todo_tags",
          "referenced_table": "todos",
          "columns": [
            "todo_id",
            "tag_id"
          ],
          "referenced_columns": [
            "todo_id"
          ]
        }
      ]
    }
  ],
  "relations": [
    {
      "table": "comments",
      "columns": [
        "todo_id",
        "comment_id"
      ],
      "cardinality": "zero_or_one",
      "parent_table": "todos",
      "parent_columns": [
        "todo_id"
      ],
      "parent_cardinality": "exactly_one",
      "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE"
    },
    {
      "table": "attachments",
      "columns": [
        "todo_id",
        "attachment_id"
      ],
      "cardinality": "zero_or_one",
      "parent_table": "todos",
      "parent_columns": [
        "todo_id"
      ],
      "parent_cardinality": "exactly_one",
      "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE"
    },
    {
      "table": "categories",
      "columns": [
        "user_id",
        "category_id"
      ],
      "cardinality": "zero_or_one",
      "parent_table": "users",
      "parent_columns": [
        "user_id"
      ],
      "parent_cardinality": "exactly_one",
      "def": "INTERLEAVE IN PARENT users ON DELETE CASCADE"
    },
    {
      "table": "todo_tags",
      "columns": [
        "todo_id",
        "tag_id"
      ],
      "cardinality": "zero_or_one",
      "parent_table": "todos",
      "parent_columns": [
        "todo_id"
      ],
      "parent_cardinality": "exactly_one",
      "def": "INTERLEAVE IN PARENT todos ON DELETE CASCADE"
    }
  ],
  "driver": {
    "name": "spanner"
  }
}
schema.json in dist directory (>= 0.5.9)
{
  "tables": {
    "comments": {
      "name": "comments",
      "comment": null,
      "columns": {
        "todo_id": {
          "name": "todo_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "comment_id": {
          "name": "comment_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "user_id": {
          "name": "user_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "content": {
          "name": "content",
          "type": "STRING(MAX)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "IDX_comments_user_id_E6D097A830600D54": {
          "name": "IDX_comments_user_id_E6D097A830600D54",
          "unique": false,
          "columns": [
            "user_id"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "users": {
      "name": "users",
      "comment": null,
      "columns": {
        "user_id": {
          "name": "user_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "email": {
          "name": "email",
          "type": "STRING(255)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "username": {
          "name": "username",
          "type": "STRING(100)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "updated_at": {
          "name": "updated_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "idx_users_email": {
          "name": "idx_users_email",
          "unique": true,
          "columns": [
            "email"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "attachments": {
      "name": "attachments",
      "comment": null,
      "columns": {
        "todo_id": {
          "name": "todo_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "attachment_id": {
          "name": "attachment_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "user_id": {
          "name": "user_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "filename": {
          "name": "filename",
          "type": "STRING(255)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "file_size": {
          "name": "file_size",
          "type": "INT64",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "mime_type": {
          "name": "mime_type",
          "type": "STRING(100)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "storage_path": {
          "name": "storage_path",
          "type": "STRING(500)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "uploaded_at": {
          "name": "uploaded_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "IDX_attachments_user_id_4C7D73049CFAC993": {
          "name": "IDX_attachments_user_id_4C7D73049CFAC993",
          "unique": false,
          "columns": [
            "user_id"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "categories": {
      "name": "categories",
      "comment": null,
      "columns": {
        "user_id": {
          "name": "user_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "category_id": {
          "name": "category_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "name": {
          "name": "name",
          "type": "STRING(100)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "color": {
          "name": "color",
          "type": "STRING(7)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": false
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "IDX_categories_category_id_U_BC14739FA19E28EE": {
          "name": "IDX_categories_category_id_U_BC14739FA19E28EE",
          "unique": true,
          "columns": [
            "category_id"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "tags": {
      "name": "tags",
      "comment": null,
      "columns": {
        "tag_id": {
          "name": "tag_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "name": {
          "name": "name",
          "type": "STRING(50)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "idx_tags_name": {
          "name": "idx_tags_name",
          "unique": true,
          "columns": [
            "name"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "todos": {
      "name": "todos",
      "comment": null,
      "columns": {
        "todo_id": {
          "name": "todo_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "user_id": {
          "name": "user_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "category_id": {
          "name": "category_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": false
        },
        "title": {
          "name": "title",
          "type": "STRING(255)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "description": {
          "name": "description",
          "type": "STRING(MAX)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": false
        },
        "status": {
          "name": "status",
          "type": "STRING(20)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "priority": {
          "name": "priority",
          "type": "STRING(10)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "due_date": {
          "name": "due_date",
          "type": "DATE",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": false
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "updated_at": {
          "name": "updated_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "completed_at": {
          "name": "completed_at",
          "type": "TIMESTAMP",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": false
        }
      },
      "indexes": {
        "idx_todos_due_date": {
          "name": "idx_todos_due_date",
          "unique": false,
          "columns": [
            "due_date"
          ],
          "type": ""
        },
        "IDX_todos_user_id_4E872528DC8BBCA7": {
          "name": "IDX_todos_user_id_4E872528DC8BBCA7",
          "unique": false,
          "columns": [
            "user_id"
          ],
          "type": ""
        },
        "IDX_todos_category_id_N_3B7864FE3ED3FF87": {
          "name": "IDX_todos_category_id_N_3B7864FE3ED3FF87",
          "unique": false,
          "columns": [
            "category_id"
          ],
          "type": ""
        },
        "idx_todos_user_status": {
          "name": "idx_todos_user_status",
          "unique": false,
          "columns": [
            "user_id",
            "status"
          ],
          "type": ""
        }
      },
      "constraints": {}
    },
    "todo_tags": {
      "name": "todo_tags",
      "comment": null,
      "columns": {
        "todo_id": {
          "name": "todo_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "tag_id": {
          "name": "tag_id",
          "type": "STRING(36)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        },
        "created_at": {
          "name": "created_at",
          "type": "TIMESTAMP (allow_commit_timestamp=TRUE)",
          "default": null,
          "check": null,
          "comment": null,
          "notNull": true
        }
      },
      "indexes": {
        "IDX_todo_tags_tag_id_8034AADD17A5A521": {
          "name": "IDX_todo_tags_tag_id_8034AADD17A5A521",
          "unique": false,
          "columns": [
            "tag_id"
          ],
          "type": ""
        }
      },
      "constraints": {}
    }
  }
}

KaoruMuta avatar Jul 08 '25 23:07 KaoruMuta

@KaoruMuta Thanks for the detailed explanation! It was very easy to understand 😄

In detail, schema.json generated by liam under dist directory drops these information. (Difference is attached below)

  • primary, unique field in columns json block
  • relationships json block

That is correct. It may be that https://github.com/liam-hq/liam/pull/2156 is missing a parser that converts from constraints to relationships in tbls' schema.json.I'd be happy to try to fix it.Please call me if there is anything I can do to help!

MH4GF avatar Jul 09 '25 02:07 MH4GF

@MH4GF Created PR for this issue: https://github.com/liam-hq/liam/pull/2548 Please kindly review it 🙏

KaoruMuta avatar Jul 14 '25 02:07 KaoruMuta

That is correct. It may be that https://github.com/liam-hq/liam/pull/2156 is missing a parser that converts from constraints to relationships in tbls' schema.json.I'd be happy to try to fix it.Please call me if there is anything I can do to help!

After manually bisecting the issue on my end, I can confirm that it’s indeed a regression introduced between CLI 0.5.8 and 0.5.9 — specifically in https://github.com/liam-hq/liam/pull/2156 .

Thanks for working on the fix!

hoshinotsuyoshi avatar Jul 14 '25 09:07 hoshinotsuyoshi

https://github.com/liam-hq/liam/pull/2548

@KaoruMuta cc: @MH4GF At this point, I’m hesitant about having Liam CLI handle INTERLEAVE syntax directly. One reason is that the libraries we rely on for --format=postgres—such as pganalyze/pg-query-emscripten and pganalyze/libpg_query—do not support Spanner-specific syntax like INTERLEAVE. The fact that edges based on Spanner’s INTERLEAVE feature were displayed at all was thanks entirely to tbls. If we’re going to address this, I’d prefer to start by fixing the case where it breaks under --format tbls .

Between versions 0.5.8 and 0.5.9, we changed the schema of schema.json—which is part of the HTML output (under the html directory)—and removed the relationships field. At the time, we considered it redundant, since we assumed all relationships could be derived from constraints (i.e., foreign key constraints).

However, through this issue, I’ve come to realize that the JSON output generated by tbls includes relationships that can't be derived from constraints alone. In order to support edge cases like this—where INTERLEAVE implies relationships not backed by constraints—we might need to reintroduce an optional field in schema.json, and populate it with just enough additional data when using tbls.

hoshinotsuyoshi avatar Jul 14 '25 09:07 hoshinotsuyoshi

@KaoruMuta I'll be away tomorrow, and Miya-san @MH4GF will also be out for a few days. Thanks in advance for your patience if our replies are a bit delayed! 🙏☀️

hoshinotsuyoshi avatar Jul 14 '25 09:07 hoshinotsuyoshi

@KaoruMuta Thanks for putting out the PR. As Hoshino-san mentioned, there are a couple of things that bother me.

  • First, I would like to see the changes made only to the tbls parser and removed from the schemarb parser this time. The decision is that it is better not to add features that are not likely to have users yet and will not be used. Your commits can be referenced later, so we will be helpful in the future when we are needed.
  • I am wondering if we can support interleave by treating it as part of constraints. I understand that it is strictly a performance optimization feature, not data integrity.
  • Cloud Spanner is currently a work-around support, via tbls, and it is inevitable that it will be a solution that remains a compromise. Where should we place the drop-off point?
    1. provide a field for interleave, not constraints
      • pros: can be supported with the correct structure as semantics
      • discuss: interleave is a Cloud Spanner specific feature, and we have to conclude that it is an edge case, not a general feature for RDB user. How should features like “dialects” of various DB engines be represented in a unified Liam schema?
    2. support them as part of constraints and make them convertible in the tbls parser
      • pros: can use https://github.com/liam-hq/liam/pull/2548 implementation
      • cons: still have the problem that it is not strictly constraints
    3. treat it as FOREIGN KEY constraints and make it convertible in tbls parser
      • pros: can meet the goal of visualization with minimum effort, without changing schema
      • cons: not correct database presentation
    4. Other excellent options.

Based on these four, plus the possibility of a future with native support for Cloud Spanner. In my opinion, if compromises remain, I think 3. treat it as FOREIGN KEY constraints and make it convertible in tbls parser is the best place to drop. The display issue can be addressed here and may be solved by handling it as default instead of interleave. What do you think?

MH4GF avatar Jul 17 '25 06:07 MH4GF

Sorry for late, I will confirm it in a few days.

KaoruMuta avatar Jul 22 '25 00:07 KaoruMuta

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

github-actions[bot] avatar Sep 20 '25 13:09 github-actions[bot]

The same issue occurs in my PostgreSQL database. Before @liam-hq/[email protected], relations were displayed correctly, but @liam-hq/[email protected] no longer shows them.

The schema.json file generated by tbls does contain the relations field, as shown below:**

{
  ...
  "relations": [
    {
      "table": "public.XXX",
      "columns": ["YYY"],
      "cardinality": "zero_or_more",
      "parent_table": "public.ZZZ",
      "parent_columns": ["id"],
      "parent_cardinality": "exactly_one",
      "def": "FOREIGN KEY (YYY) REFERENCES ZZZZ(id) ON UPDATE CASCADE ON DELETE RESTRICT"
    }
  ]
}

Here is my attempt:

tbls doc --force

# This version correctly includes the relationships.
pnpm dlx @liam-hq/[email protected] erd build --format tbls --input ./dbdoc/schema.json 

# However, this version does NOT generate the relationships.
pnpm dlx @liam-hq/[email protected] erd build --format tbls --input ./dbdoc/schema.json 

If you need more details (schema.json sample, environment, or a minimal reproduction repository), I can provide them.

kueda9321 avatar Nov 13 '25 04:11 kueda9321