Skip to content

Commit 08f3c07

Browse files
committed
sqldoc: init
0 parents  commit 08f3c07

File tree

11 files changed

+609
-0
lines changed

11 files changed

+609
-0
lines changed

.envrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
PATH_add bin

.gitignore

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Output documentation
2+
schema.md
3+
schema/*
4+
5+
# Development binaries
6+
bin/*
7+
!bin/.keep
8+
9+
# Binaries for programs and plugins
10+
*.exe
11+
*.exe~
12+
*.dll
13+
*.so
14+
*.dylib
15+
16+
# Test binary, built with `go test -c`
17+
*.test
18+
19+
# Output of the go coverage tool, specifically when used with LiteIDE
20+
*.out
21+
22+
# Go workspace file
23+
go.work

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# SQLDoc
2+
3+
SQLDoc is a markdown documentation for SQL tables. Inspired by Rails ActiveRecord `schema.rb` and [drwl/annotaterb](https://github.com/drwl/annotaterb).
4+
5+
## Why?
6+
7+
Projects often manages their schema roll out through migrations (e.g. [golang-migrate/migrate](https://github.com/golang-migrate/migrate)). As a project matures, it's quite common to encounter several `ALTER TABLE` commands, which makes it difficult to have a near-instant idea of what the schema looks like.
8+
9+
In Rails, `schema.rb` provides that insight, while extensions like [drwl/annotaterb](https://github.com/drwl/annotaterb) goes further to co-locate the schema documentation with the model definitions.
10+
11+
SQLDoc makes it easy to look at Markdown documentation for SQL tables, which I prefer over running `psql -c "\d+ table_name"`.

bin/.keep

Whitespace-only changes.

config/config.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package config
2+
3+
import (
4+
_ "embed"
5+
"errors"
6+
"os"
7+
8+
"gopkg.in/yaml.v3"
9+
)
10+
11+
//go:embed example.yaml
12+
var example []byte
13+
14+
var ErrDatabaseURLRequired = errors.New("no database URL found, specify in configuration or set DATABASE_URL environment variable")
15+
16+
type Config struct {
17+
Database Database `json:"database"`
18+
Documentation Documentation `json:"documentation"`
19+
}
20+
21+
type Database struct {
22+
URL string `json:"url"`
23+
Schemas []string `json:"schemas"`
24+
ExcludeTables []string `json:"exclude_tables" yaml:"exclude_tables"`
25+
}
26+
27+
type Documentation struct {
28+
Strategy string `json:"strategy"`
29+
Directory string `json:"directory"`
30+
Filename string `json:"filename"`
31+
Stdout bool `json:"stdout"`
32+
}
33+
34+
func Default() *Config {
35+
return &Config{
36+
Database: Database{
37+
URL: os.Getenv("DATABASE_URL"),
38+
Schemas: []string{"public"},
39+
ExcludeTables: []string{},
40+
},
41+
Documentation: Documentation{
42+
Strategy: "unified",
43+
Directory: ".",
44+
Filename: "schema.md",
45+
Stdout: true,
46+
},
47+
}
48+
}
49+
50+
func Parse(data []byte) (*Config, error) {
51+
config := Default()
52+
if err := yaml.Unmarshal(data, config); err != nil {
53+
return nil, err
54+
}
55+
return config, nil
56+
}
57+
58+
func ParseFile(path string) (*Config, error) {
59+
data, err := os.ReadFile(path)
60+
if err != nil {
61+
return nil, err
62+
}
63+
return Parse(data)
64+
}
65+
66+
func Load(path string) (*Config, error) {
67+
var conf *Config
68+
if path == "" {
69+
conf = Default()
70+
} else {
71+
var err error
72+
conf, err = ParseFile(path)
73+
if err != nil {
74+
return nil, err
75+
}
76+
}
77+
if dburl := os.Getenv("DATABASE_URL"); dburl != "" {
78+
conf.Database.URL = dburl
79+
}
80+
if conf.Database.URL == "" {
81+
return nil, ErrDatabaseURLRequired
82+
}
83+
return conf, nil
84+
}
85+
86+
func WriteExample(path string) error {
87+
return os.WriteFile(path, example, 0644)
88+
}

config/example.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
# Example configuration file for sqldoc.
2+
# When the value is optional, the defaults shown below are used.
3+
4+
database:
5+
# Required: fallback to $DATABASE_URL value.
6+
url: "postgres://user:password@localhost:5432/database_name"
7+
# Optional: Schemas to be included in the documentation.
8+
schemas: ["public"]
9+
# Optional: Tables to be excluded from the documentation.
10+
exclude_tables: [""]
11+
12+
documentation:
13+
# Documentation strategies:
14+
# - unified: All tables are documented in a single file.
15+
# - per_table: Each table is documented in a separate file.
16+
strategy: unified
17+
# Optional: Output directory for the documentation.
18+
directory: "."
19+
# Optional: Output filename for the documentation. Only used in "unified" strategy.
20+
filename: "schema.md"
21+
# Write the output to STDOUT as well.
22+
stdout: true

db/db.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
)
8+
9+
type Database interface {
10+
Close() error
11+
12+
ListTables(ctx context.Context, schema string) ([]string, error)
13+
ListColumns(ctx context.Context, schema, table string) ([]TableColumn, error)
14+
}
15+
16+
type TableColumn struct {
17+
Name string
18+
Type string
19+
Nullable bool
20+
Default string
21+
}
22+
23+
func New(url string) (Database, error) {
24+
protocol := strings.Split(url, "://")[0]
25+
switch protocol {
26+
case "postgres", "postgresql", "pgx":
27+
return NewPostgres(url)
28+
default:
29+
return nil, fmt.Errorf("unsupported protocol: %s", protocol)
30+
}
31+
}

db/postgres.go

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
package db
2+
3+
import (
4+
"context"
5+
"database/sql"
6+
"strings"
7+
8+
_ "github.com/jackc/pgx/v5/stdlib"
9+
)
10+
11+
type Postgres struct {
12+
db *sql.DB
13+
}
14+
15+
func NewPostgres(dsn string) (*Postgres, error) {
16+
db, err := sql.Open("pgx", dsn)
17+
if err != nil {
18+
return nil, err
19+
}
20+
return &Postgres{db}, nil
21+
}
22+
23+
func (p *Postgres) Close() error {
24+
if p.db == nil {
25+
return nil
26+
}
27+
if err := p.db.Close(); err != nil {
28+
return err
29+
}
30+
p.db = nil
31+
return nil
32+
}
33+
34+
const pgListTables = `SELECT DISTINCT table_name FROM information_schema.tables WHERE table_schema = $1 ORDER BY table_name ASC`
35+
36+
func (p *Postgres) ListTables(ctx context.Context, schema string) ([]string, error) {
37+
rows, err := p.db.QueryContext(ctx, pgListTables, schema)
38+
if err != nil {
39+
return nil, err
40+
}
41+
defer rows.Close()
42+
var tables []string
43+
for rows.Next() {
44+
var table string
45+
if err := rows.Scan(&table); err != nil {
46+
return nil, err
47+
}
48+
tables = append(tables, table)
49+
}
50+
if err := rows.Close(); err != nil {
51+
return nil, err
52+
}
53+
if err := rows.Err(); err != nil {
54+
return nil, err
55+
}
56+
return tables, nil
57+
}
58+
59+
const pgListColumns = `SELECT column_name, data_type, is_nullable, column_default FROM information_schema.columns WHERE table_schema = $1 AND table_name = $2`
60+
61+
func (p *Postgres) ListColumns(ctx context.Context, schema, table string) ([]TableColumn, error) {
62+
rows, err := p.db.QueryContext(ctx, pgListColumns, schema, table)
63+
if err != nil {
64+
return nil, err
65+
}
66+
defer rows.Close()
67+
var columns []TableColumn
68+
for rows.Next() {
69+
var c TableColumn
70+
var nullable string
71+
var defaultVal sql.NullString
72+
if err := rows.Scan(&c.Name, &c.Type, &nullable, &defaultVal); err != nil {
73+
return nil, err
74+
}
75+
c.Nullable = nullable == "YES"
76+
if defaultVal.Valid {
77+
c.Default = strings.TrimSpace(defaultVal.String)
78+
}
79+
columns = append(columns, c)
80+
}
81+
if err := rows.Close(); err != nil {
82+
return nil, err
83+
}
84+
if err := rows.Err(); err != nil {
85+
return nil, err
86+
}
87+
return columns, nil
88+
}

go.mod

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
module go.husin.dev/sqldoc
2+
3+
go 1.22.2
4+
5+
require (
6+
github.com/charmbracelet/glamour v0.7.0
7+
github.com/jackc/pgx/v5 v5.5.5
8+
github.com/nao1215/markdown v0.0.8
9+
gopkg.in/yaml.v3 v3.0.1
10+
)
11+
12+
require (
13+
github.com/alecthomas/chroma/v2 v2.8.0 // indirect
14+
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
15+
github.com/aymerick/douceur v0.2.0 // indirect
16+
github.com/dlclark/regexp2 v1.4.0 // indirect
17+
github.com/gorilla/css v1.0.0 // indirect
18+
github.com/jackc/pgpassfile v1.0.0 // indirect
19+
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
20+
github.com/jackc/puddle/v2 v2.2.1 // indirect
21+
github.com/karrick/godirwalk v1.17.0 // indirect
22+
github.com/kr/text v0.2.0 // indirect
23+
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
24+
github.com/mattn/go-isatty v0.0.18 // indirect
25+
github.com/mattn/go-runewidth v0.0.14 // indirect
26+
github.com/microcosm-cc/bluemonday v1.0.25 // indirect
27+
github.com/muesli/reflow v0.3.0 // indirect
28+
github.com/muesli/termenv v0.15.2 // indirect
29+
github.com/olekukonko/tablewriter v0.0.5 // indirect
30+
github.com/rivo/uniseg v0.2.0 // indirect
31+
github.com/rogpeppe/go-internal v1.12.0 // indirect
32+
github.com/yuin/goldmark v1.5.4 // indirect
33+
github.com/yuin/goldmark-emoji v1.0.2 // indirect
34+
golang.org/x/crypto v0.17.0 // indirect
35+
golang.org/x/net v0.17.0 // indirect
36+
golang.org/x/sync v0.1.0 // indirect
37+
golang.org/x/sys v0.15.0 // indirect
38+
golang.org/x/text v0.14.0 // indirect
39+
)

0 commit comments

Comments
 (0)