8000 add key representation and mermaid ER diagrams support by jcarnu · Pull Request #1 · wilsonehusin/sqldoc · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

add key representation and mermaid ER diagrams support #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type Documentation struct {
Directory string `json:"directory"`
Filename string `json:"filename"`
Stdout bool `json:"stdout"`
Mermaid bool `json:"mermaid"`
}

func Default() *Config {
Expand Down
2 changes: 2 additions & 0 deletions config/example.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -20,3 +20,5 @@ documentation:
filename: "schema.md"
# Write the output to STDOUT as well.
stdout: true
# Write mermaid output
mermaid: true
30 changes: 30 additions & 0 deletions db/db.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,43 @@ type Database interface {

ListTables(ctx context.Context, schema string) ([]string, error)
ListColumns(ctx context.Context, schema, table string) ([]TableColumn, error)
ListConstraints(ctx context.Context, schema, table string) ([]TableConstraint, error)
}

type TableColumn struct {
Name string
Type string
Nullable bool
Default string
PK bool
FK string
FK_ref string
UK bool
Comment on lines +22 to +25
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went back and forth on this one in my head, and I think it might be better for us to keep these in separate struct as TableIndex? That should allow us to be expansive too with informing the index kind (e.g. btree).

I find that making the view as close as possible to \d output would be best as it doesn't burden users with re-familiarizing on output type. What do you think?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry not to have replied sooner. I have little time to contribute for this project (and little free time in general).

You're right, I would normally have created two issues to discuss and 2 PR for this contrib, I just wanted to give some views what could be done to my mind and prototype it. I didn't give more "context" to my contrib.

The story:

My main goal was to prototype constraints on tables such as primary key and foreign keys. Then I just remembered of what schemaspy did : a dot generated ERD. Then I thought about contextualized diagrams with mermaid. And at that point, as I already prototyped it, I decided to submit my work as DRAFT, not to be integrated into main code but to give some ideas (you also had them, so that's fine).

Maybe textual information is fine just like PostgreSQL psql \d but having (markdown) tables with PK and FK seems more visual to me.

Refering tables+column could be displayed in psql way.

Moreover, my main goal would have been to show constraints on columns : references, not nul, checks and so on.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Depending on my free time, I could review this work to be more consistent. But if you want to go faster, feel free to get inspiration, discard my PR or reuse parts (and I'm totally fine with it). If you prefer let me continue this work, you have to know about my little time availability and it may require me weeks to finish (sorry about that).

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to apologize — I'm glad that someone else resonated with the usefulness with this project! I'm probably going to spend some time to make some progress on the project in a few weeks time, but I will surely let you know if I decide to tackle these features specifically.

Thank you!

}

/*
tc.constraint_schema
, tc.constraint_name
, tc.constraint_type
, kcu1.constraint_schema
, kcu1.constraint_name
, kcu1.table_name from_table
, kcu1.column_name from_column
, kcu1.ordinal_position from_position
, kcu2.constraint_schema
, kcu2.constraint_name
, kcu2.table_name to_table
, kcu2.column_name to_column
, kcu2.ordinal_position to_position
*/
type TableConstraint struct {
Schema string
Name string
Type string
TableColumn string // referring column
TargetSchema string // referred schema
TargetTable string // referred table
TargetColumn string // referred column
}

func New(url string) (Database, error) {
Expand Down
101 changes: 100 additions & 1 deletion db/postgres.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,35 @@ func (p *Postgres) ListTables(ctx context.Context, schema string) ([]string, err
return tables, nil
}

const pgListColumns = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2`
const pgListColumns = `SELECT
c.column_name
, c.data_type
, c.is_nullable
, c.column_default

FROM information_schema.columns c
WHERE c.table_schema = $1 AND c.table_name = $2
ORDER BY ordinal_position`

/*
const pgListColumns = `
SELECT
c.column_name
, c.data_type
, c.is_nullable
, c.column_default
, tc.constraint_type,
, *
FROM information_schema.columns c
LEFT OUTER JOIN information_schema.key_column_usage k
ON c.column_name = k.column_name
AND c.table_name = k.table_name
LEFT OUTER JOIN information_schema.table_constraints tc
ON tc.constraint_name = k.constraint_name
WHERE c.table_name = $1
AND c.table_schema = $2
`
*/
func (p *Postgres) ListColumns(ctx context.Context, schema, table string) ([]TableColumn, error) {
rows, err := p.db.QueryContext(ctx, pgListColumns, schema, table)
if err != nil {
Expand Down Expand Up @@ -86,3 +113,75 @@ func (p *Postgres) ListColumns(ctx context.Context, schema, table string) ([]Tab
}
return columns, nil
}

const pgListConstraints = `
SELECT
tc.constraint_schema
, tc.constraint_name
, tc.constraint_type
-- , kcu1.constraint_schema
-- , kcu1.constraint_name
-- , kcu1.table_name from_table
, kcu1.column_name from_column
-- , kcu1.ordinal_position from_position
-- , kcu2.constraint_schema
-- , kcu2.constraint_name
, kcu2.table_schema to_schema
, kcu2.table_name to_table
, kcu2.column_name to_column
-- , kcu2.ordinal_position to_position
FROM information_schema.table_constraints tc
LEFT OUTER JOIN information_schema.referential_constraints rc
USING(constraint_schema, constraint_name)
JOIN information_schema.key_column_usage as kcu1
ON kcu1.constraint_catalog = tc.constraint_catalog
AND kcu1.constraint_schema = tc.constraint_schema
AND kcu1.constraint_name = tc.constraint_name
LEFT OUTER JOIN information_schema.key_column_usage as kcu2
ON kcu2.constraint_catalog = rc.unique_constraint_catalog
AND kcu2.constraint_schema = rc.unique_constraint_schema
AND kcu2.constraint_name = rc.unique_constraint_name
AND kcu2.ordinal_position = kcu1.ordinal_position
WHERE tc.table_name = $2
AND tc.table_schema = $1
`

func (p *Postgres) ListConstraints(ctx context.Context, schema, table string) ([]TableConstraint, error) {
rows, err := p.db.QueryContext(ctx, pgListConstraints, schema, table)
if err != nil {
return nil, err
}
defer rows.Close()
var constraints []TableConstraint
for rows.Next() {
var c TableConstraint
var tblColumn sql.NullString
var tgtSchema sql.NullString
var tgtTable sql.NullString
var tgtColumn sql.NullString
if err := rows.Scan(&c.Schema, &c.Name, &c.Type, &tblColumn, &tgtSchema, &tgtTable, &tgtColumn); err != nil {
return nil, err
}
if tblColumn.Valid {
c.TableColumn = tblColumn.String
}
if tgtSchema.Valid {
c.TargetSchema = tgtSchema.String
}
if tgtTable.Valid {
c.TargetTable = tgtTable.String
}
if tgtColumn.Valid {
c.TargetColumn = tgtColumn.String
}

constraints = append(constraints, c)
}
if err := rows.Close(); err != nil {
return nil, err
}
if err := rows.Err(); err != nil {
return nil, err
}
return constraints, nil
}
121 changes: 115 additions & 6 deletions md/md.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"os"
"path/filepath"
"slices"
"strings"

"github.com/charmbracelet/glamour"
"github.com/nao1215/markdown"
Expand Down Expand Up @@ -100,30 +101,138 @@ func (m *MD) renderTable(ctx context.Context, w io.Writer, schema, tableName str
canvas.H1(markdown.Code(tableName))
canvas.PlainText("")
mdTableRows := [][]string{}
constraints, err := m.db.ListConstraints(ctx, schema, tableName)

if err != nil {
return fmt.Errorf("fetching constraints for '%s.%s': %w", schema, tableName, err)
}

columns, err := m.db.ListColumns(ctx, schema, tableName)
if err != nil {
return fmt.Errorf("fetching columns for '%s.%s': %w", schema, tableName, err)
}
for _, column := range columns {
for idx := range columns {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

was it necessary to use idx instead of getting the column struct directly?

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is that I change slice values later in constraints checks. Using column is using a copy of the slice item. Maybe I'm wrong as I'm less acquainted with Golang than with other languages. It looked like it did not work without slice indirection.

nullable := ""
if !column.Nullable {
if !columns[idx].Nullable {
nullable = "NOT NULL"
}
d := column.Default
d := columns[idx].Default
if d != "" {
d = markdown.Code(d)
}
c := []string{}
t := ""
for _, constraint := range constraints {
if constraint.TableColumn == columns[idx].Name {
if constraint.Type == "PRIMARY KEY" {
c = append(c, "PK")
columns[idx].PK = true
}
if constraint.Type == "UNIQUE" {
c = append(c, "UK")
columns[idx].UK = true
}
if constraint.Type == "FOREIGN KEY" {
c = append(c, "FK")
t = fmt.Sprintf("%s.%s", constraint.TargetSchema, constraint.TargetTable)
columns[idx].FK = t
t = fmt.Sprintf("%s.%s", t, constraint.TargetColumn)
columns[idx].FK_ref = constraint.TargetColumn

}
}

}
mdTableRows = append(mdTableRows, []string{
markdown.Code(column.Name),
column.Type,
markdown.Code(columns[idx].Name),
columns[idx].Type,
nullable,
d,
strings.Join(c[:], ", "),
t,
})
}
canvas.Table(markdown.TableSet{
Header: []string{"Name", "Type", "Nullable", "Default"},
Header: []string{"Name", "Type", "Nullable", "Default", "Key", "Target"},
Rows: mdTableRows,
})

if m.conf.Documentation.Mermaid {
mermaid_dep := ""

mermaid := "```mermaid\nerDiagram\n"
mermaid = fmt.Sprintf("%s\"%s.%s\"{\n", mermaid, schema, tableName)
for _, column := range columns {
// nullable := ""
// if !column.Nullable {
// nullable = "NN"
// }
k := []string{}
if column.PK {
k = append(k, "PK")
}
if column.FK != "" {
k = append(k, "FK")
mermaid_dep = fmt.Sprintf("%s\n\"%s.%s\" o|--o| \"%s\":%s",
mermaid_dep,
schema, tableName,
column.FK,
column.FK_ref)
}

if column.UK {
k = append(k, "UK")
}

mermaid = fmt.Sprintf("%s%s %s %s\n", mermaid,
strings.Replace(column.Type, " ", "_", -1),
column.Name, strings.Join(k[:], ","))
}

mermaid = fmt.Sprintf("%s}\n%s\n```", mermaid, mermaid_dep)

canvas.PlainText(mermaid)
}

if len(constraints) > 0 {
canvas.PlainText("")
canvas.H2("Constraints")
canvas.PlainText("")
mdConstraintsRows := [][]string{}
for _, constraint := range constraints {

col := ""
if constraint.TableColumn != "" {
col = markdown.Code(constraint.TableColumn)
}
tgtsch := ""
if constraint.TargetSchema != "" {
tgtsch = markdown.Code(constraint.TargetSchema)
}
tgttbl := ""
if constraint.TargetTable != "" {
tgttbl = markdown.Code(constraint.TargetTable)
}
tgtcol := ""
if constraint.TargetColumn != "" {
tgtcol = markdown.Code(constraint.TargetColumn)
}
mdConstraintsRows = append(mdConstraintsRows, []string{
markdown.Code(constraint.Schema),
markdown.Code(constraint.Name),
markdown.Code(constraint.Type),
col,
tgtsch,
tgttbl,
tgtcol,
})
}
canvas.Table(markdown.TableSet{
Header: []string{"Schema", "Name", "Type", "Referring Column", "Target Schema", "Target Table", "Target Column"},
Rows: mdConstraintsRows,
})
}

if m.conf.Documentation.Stdout {
out, err := glamour.Render(canvas.String(), "dark")
if err != nil {
Expand Down
0