From 1bed38df0575381c9e573e8aeb42354a9f82729f Mon Sep 17 00:00:00 2001 From: k1LoW Date: Thu, 15 Jun 2023 16:57:09 +0900 Subject: [PATCH] Add markdown and ER diagram image generation for viewpoints --- cmd/doc.go | 64 +------- config/config.go | 119 +++++--------- config/templates.go | 5 +- config/viewpoints.go | 7 - output/gviz/gviz.go | 100 +++++++++++- output/md/md.go | 145 +++++++++++++++--- output/md/md_test.go | 5 +- output/md/templates/viewpoint.md.tmpl | 20 +++ schema/filter.go | 137 +++++++++++++++++ schema/schema.go | 18 +++ testdata/md_test_README.md.golden | 4 + testdata/md_test_viewpoint-1.md.golden | 15 ++ .../md_test_viewpoint-1.md.mermaid.golden | 23 +++ testutil/schema.go | 3 + 14 files changed, 485 insertions(+), 180 deletions(-) create mode 100644 output/md/templates/viewpoint.md.tmpl create mode 100644 schema/filter.go create mode 100644 testdata/md_test_viewpoint-1.md.golden create mode 100644 testdata/md_test_viewpoint-1.md.mermaid.golden diff --git a/cmd/doc.go b/cmd/doc.go index ed8434f3c..7e0c4c48e 100644 --- a/cmd/doc.go +++ b/cmd/doc.go @@ -93,7 +93,7 @@ var docCmd = &cobra.Command{ } if c.NeedToGenerateERImages() { - if err := withDot(s, c, force); err != nil { + if err := gviz.Output(s, c, force); err != nil { return err } } @@ -113,52 +113,6 @@ var docCmd = &cobra.Command{ }, } -func withDot(s *schema.Schema, c *config.Config, force bool) error { - erFormat := c.ER.Format - outputPath := c.DocPath - fullPath, err := filepath.Abs(outputPath) - if err != nil { - return errors.WithStack(err) - } - - if !force && outputErExists(s, fullPath) { - return errors.New("output ER diagram files already exists") - } - - err = os.MkdirAll(fullPath, 0755) // #nosec - if err != nil { - return errors.WithStack(err) - } - - erFileName := fmt.Sprintf("schema.%s", erFormat) - fmt.Printf("%s\n", filepath.Join(outputPath, erFileName)) - - file, err := os.OpenFile(filepath.Join(fullPath, erFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) // #nosec - if err != nil { - return errors.WithStack(err) - } - g := gviz.New(c) - if err := g.OutputSchema(file, s); err != nil { - return errors.WithStack(err) - } - - // tables - for _, t := range s.Tables { - erFileName := fmt.Sprintf("%s.%s", t.Name, erFormat) - fmt.Printf("%s\n", filepath.Join(outputPath, erFileName)) - - file, err := os.OpenFile(filepath.Join(fullPath, erFileName), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) // #nosec - if err != nil { - return errors.WithStack(err) - } - if err := g.OutputTable(file, t); err != nil { - return errors.WithStack(err) - } - } - - return nil -} - func withSchemaFile(s *schema.Schema, c *config.Config) (e error) { sf, err := os.Create(c.SchemaFilePath()) if err != nil { @@ -207,22 +161,6 @@ func loadDocArgs(args []string) ([]config.Option, error) { return options, nil } -func outputErExists(s *schema.Schema, path string) bool { - // schema.png - erFileName := fmt.Sprintf("schema.%s", erFormat) - if _, err := os.Lstat(filepath.Join(path, erFileName)); err == nil { - return true - } - // tables - for _, t := range s.Tables { - erFileName := fmt.Sprintf("%s.%s", t.Name, erFormat) - if _, err := os.Lstat(filepath.Join(path, erFileName)); err == nil { - return true - } - } - return false -} - func init() { rootCmd.AddCommand(docCmd) docCmd.Flags().StringVarP(&dsn, "dsn", "", "", "data source name") diff --git a/config/config.go b/config/config.go index dae8687ae..69b9504d2 100644 --- a/config/config.go +++ b/config/config.go @@ -389,27 +389,6 @@ func (c *Config) ModifySchema(s *schema.Schema) error { for _, l := range c.Labels { s.Labels = s.Labels.Merge(l) } - // set Viewpoints - for _, v := range c.Viewpoints { - s.Viewpoints = s.Viewpoints.Merge(&schema.Viewpoint{ - Name: v.Name, - Desc: v.Desc, - Labels: v.Labels, - Tables: v.Tables, - }) - } - for _, v := range s.Viewpoints { - L: - for _, l := range v.Labels { - for _, t := range s.Tables { - if t.Labels.Contains(l) { - continue L - } - } - return fmt.Errorf("viewpoint '%s' has unknown label '%s'", v.Name, l) - } - } - if err := detectCardinality(s); err != nil { return err } @@ -441,6 +420,41 @@ func (c *Config) ModifySchema(s *schema.Schema) error { if err := c.detectShowColumnsForER(s); err != nil { return err } + + // set Viewpoints + // viewpoints should be created using as complete a schema as possible + for _, v := range c.Viewpoints { + cs, err := s.Clone() + if err != nil { + return err + } + if err := cs.Filter(&schema.FilterOption{ + Include: v.Tables, + IncludeLabels: v.Labels, + Distance: 0, + }); err != nil { + return err + } + s.Viewpoints = s.Viewpoints.Merge(&schema.Viewpoint{ + Name: v.Name, + Desc: v.Desc, + Labels: v.Labels, + Tables: v.Tables, + Schema: cs, + }) + } + for _, v := range s.Viewpoints { + L: + for _, l := range v.Labels { + for _, t := range s.Tables { + if t.Labels.Contains(l) { + continue L + } + } + return fmt.Errorf("viewpoint '%s' has unknown label '%s'", v.Name, l) + } + } + return nil } @@ -459,63 +473,12 @@ func (c *Config) MergeAdditionalData(s *schema.Schema) error { // FilterTables filter tables from schema.Schema using include: and exclude: and includeLabels func (c *Config) FilterTables(s *schema.Schema) error { - i := append(c.Include, s.NormalizeTableNames(c.Include)...) - e := append(c.Exclude, s.NormalizeTableNames(c.Exclude)...) - - includes := []*schema.Table{} - excludes := []*schema.Table{} - for _, t := range s.Tables { - li, mi := matchLength(i, t.Name) - le, me := matchLength(e, t.Name) - ml := matchLabels(c.includeLabels, t.Labels) - switch { - case mi: - if me && li < le { - excludes = append(excludes, t) - continue - } - includes = append(includes, t) - case ml: - if me { - excludes = append(excludes, t) - continue - } - includes = append(includes, t) - case len(c.Include) == 0 && len(c.includeLabels) == 0: - if me { - excludes = append(excludes, t) - continue - } - includes = append(includes, t) - default: - excludes = append(excludes, t) - } - } - - collects := []*schema.Table{} - for _, t := range includes { - ts, _, err := t.CollectTablesAndRelations(c.Distance, true) - if err != nil { - return err - } - for _, tt := range ts { - if !tt.Contains(includes) { - collects = append(collects, tt) - } - } - } - - for _, t := range excludes { - if t.Contains(collects) { - continue - } - err := excludeTableFromSchema(t.Name, s) - if err != nil { - return errors.Wrap(errors.WithStack(err), fmt.Sprintf("failed to filter table '%s'", t.Name)) - } - } - - return nil + return s.Filter(&schema.FilterOption{ + Include: c.Include, + Exclude: c.Exclude, + IncludeLabels: c.includeLabels, + Distance: c.Distance, + }) } func (c *Config) mergeDictFromSchema(s *schema.Schema) { diff --git a/config/templates.go b/config/templates.go index 854895acf..0e38ef48b 100644 --- a/config/templates.go +++ b/config/templates.go @@ -12,8 +12,9 @@ type Templates struct { // MD holds the paths to the markdown template files. // If populated the files are used to override the default ones. type MD struct { - Index string `yaml:"index,omitempty"` - Table string `yaml:"table,omitempty"` + Index string `yaml:"index,omitempty"` + Table string `yaml:"table,omitempty"` + Viewpoint string `yaml:"viewpoint,omitempty"` } // Dot holds the paths to the dot template files. diff --git a/config/viewpoints.go b/config/viewpoints.go index c0134ce26..bce238a12 100644 --- a/config/viewpoints.go +++ b/config/viewpoints.go @@ -1,15 +1,8 @@ package config -import "github.com/k1LoW/tbls/schema" - type Viewpoint struct { Name string `yaml:"name,omitempty"` Desc string `yaml:"desc,omitempty"` Labels []string `yaml:"labels,omitempty"` Tables []string `yaml:"tables,omitempty"` } - -func (v *Viewpoint) FilterTables(s *schema.Schema) error { - c := &Config{Include: v.Tables, includeLabels: v.Labels} - return c.FilterTables(s) -} diff --git a/output/gviz/gviz.go b/output/gviz/gviz.go index 7a29d982f..f66a75db9 100644 --- a/output/gviz/gviz.go +++ b/output/gviz/gviz.go @@ -2,6 +2,7 @@ package gviz import ( "bytes" + "fmt" "io" "os" "path/filepath" @@ -33,7 +34,7 @@ func New(c *config.Config) *Gviz { } } -// OutputSchema output dot format for full relation. +// OutputSchema generage image for full relation. func (g *Gviz) OutputSchema(wr io.Writer, s *schema.Schema) error { buf := &bytes.Buffer{} if err := g.dot.OutputSchema(buf, s); err != nil { @@ -42,11 +43,19 @@ func (g *Gviz) OutputSchema(wr io.Writer, s *schema.Schema) error { return g.render(wr, buf.Bytes()) } -// OutputTable output dot format for table. +// OutputTable generage image for table. func (g *Gviz) OutputTable(wr io.Writer, t *schema.Table) error { buf := &bytes.Buffer{} - err := g.dot.OutputTable(buf, t) - if err != nil { + if err := g.dot.OutputTable(buf, t); err != nil { + return errors.WithStack(err) + } + return g.render(wr, buf.Bytes()) +} + +// OutputViewpoint generage image for viewpoint. +func (g *Gviz) OutputViewpoint(wr io.Writer, v *schema.Viewpoint) error { + buf := &bytes.Buffer{} + if err := g.dot.OutputSchema(buf, v.Schema); err != nil { return errors.WithStack(err) } return g.render(wr, buf.Bytes()) @@ -79,6 +88,66 @@ func (g *Gviz) render(wr io.Writer, b []byte) (e error) { return nil } +// Output generate images. +func Output(s *schema.Schema, c *config.Config, force bool) (e error) { + erFormat := c.ER.Format + outputPath := c.DocPath + fullPath, err := filepath.Abs(outputPath) + if err != nil { + return errors.WithStack(err) + } + + if !force && outputErExists(s, c.ER.Format, fullPath) { + return errors.New("output ER diagram files already exists") + } + + err = os.MkdirAll(fullPath, 0755) // #nosec + if err != nil { + return errors.WithStack(err) + } + + fn := fmt.Sprintf("schema.%s", erFormat) + fmt.Printf("%s\n", filepath.Join(outputPath, fn)) + + f, err := os.OpenFile(filepath.Join(fullPath, fn), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) // #nosec + if err != nil { + return errors.WithStack(err) + } + g := New(c) + if err := g.OutputSchema(f, s); err != nil { + return errors.WithStack(err) + } + + // tables + for _, t := range s.Tables { + fn := fmt.Sprintf("%s.%s", t.Name, erFormat) + fmt.Printf("%s\n", filepath.Join(outputPath, fn)) + + f, err := os.OpenFile(filepath.Join(fullPath, fn), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) // #nosec + if err != nil { + return errors.WithStack(err) + } + if err := g.OutputTable(f, t); err != nil { + return errors.WithStack(err) + } + } + + // viewpoints + for i, v := range s.Viewpoints { + fn := fmt.Sprintf("viewpoint-%d.%s", i, erFormat) + fmt.Printf("%s\n", filepath.Join(outputPath, fn)) + f, err := os.OpenFile(filepath.Join(fullPath, fn), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) // #nosec + if err != nil { + return errors.WithStack(err) + } + if err := g.OutputViewpoint(f, v); err != nil { + return errors.WithStack(err) + } + } + + return nil +} + // getFaceFunc func getFaceFunc(keyword string) (func(size float64) (font.Face, error), error) { var ( @@ -135,3 +204,26 @@ func getFaceFunc(keyword string) (func(size float64) (font.Face, error), error) } return faceFunc, nil } + +func outputErExists(s *schema.Schema, erFormat, path string) bool { + // schema.png + fn := fmt.Sprintf("schema.%s", erFormat) + if _, err := os.Lstat(filepath.Join(path, fn)); err == nil { + return true + } + // tables + for _, t := range s.Tables { + fn := fmt.Sprintf("%s.%s", t.Name, erFormat) + if _, err := os.Lstat(filepath.Join(path, fn)); err == nil { + return true + } + } + // viewpoints + for i := range s.Viewpoints { + fn := fmt.Sprintf("viewpoint-%d.%s", i, erFormat) + if _, err := os.Lstat(filepath.Join(path, fn)); err == nil { + return true + } + } + return false +} diff --git a/output/md/md.go b/output/md/md.go index 9d787d312..d6b12398e 100644 --- a/output/md/md.go +++ b/output/md/md.go @@ -97,6 +97,34 @@ func (m *Md) OutputTable(wr io.Writer, t *schema.Table) error { return nil } +// OutputViewpoint output md format for viewpoint. +func (m *Md) OutputViewpoint(wr io.Writer, i int, v *schema.Viewpoint) error { + ts, err := m.viewpointTemplate() + if err != nil { + return errors.WithStack(err) + } + tmpl := template.Must(template.New("viewpoint").Funcs(output.Funcs(&m.config.MergedDict)).Parse(ts)) + templateData := m.makeSchemaTemplateData(v.Schema) + templateData["er"] = !m.config.ER.Skip + templateData["Name"] = v.Name + templateData["Desc"] = v.Desc + switch m.config.ER.Format { + case "mermaid": + buf := new(bytes.Buffer) + mmd := mermaid.New(m.config) + if err := mmd.OutputSchema(buf, v.Schema); err != nil { + return err + } + templateData["erDiagram"] = fmt.Sprintf("```mermaid\n%s```", buf.String()) + default: + templateData["erDiagram"] = fmt.Sprintf("![er](%sviewpoint-%d.%s)", m.config.BaseUrl, i, m.config.ER.Format) + } + if err := tmpl.Execute(wr, templateData); err != nil { + return errors.WithStack(err) + } + return nil +} + // Output generate markdown files. func Output(s *schema.Schema, c *config.Config, force bool) (e error) { docPath := c.DocPath @@ -116,9 +144,9 @@ func Output(s *schema.Schema, c *config.Config, force bool) (e error) { } // README.md - file, err := os.Create(filepath.Clean(filepath.Join(fullPath, "README.md"))) + f, err := os.Create(filepath.Clean(filepath.Join(fullPath, "README.md"))) defer func() { - err := file.Close() + err := f.Close() if err != nil { e = err } @@ -127,28 +155,46 @@ func Output(s *schema.Schema, c *config.Config, force bool) (e error) { return errors.WithStack(err) } md := New(c) - if err := md.OutputSchema(file, s); err != nil { + if err := md.OutputSchema(f, s); err != nil { return errors.WithStack(err) } fmt.Printf("%s\n", filepath.Join(docPath, "README.md")) // tables for _, t := range s.Tables { - file, err := os.Create(filepath.Clean(filepath.Join(fullPath, fmt.Sprintf("%s.md", t.Name)))) + f, err := os.Create(filepath.Clean(filepath.Join(fullPath, fmt.Sprintf("%s.md", t.Name)))) if err != nil { - _ = file.Close() + _ = f.Close() return errors.WithStack(err) } - md := New(c) - if err := md.OutputTable(file, t); err != nil { - _ = file.Close() + if err := md.OutputTable(f, t); err != nil { + _ = f.Close() return errors.WithStack(err) } fmt.Printf("%s\n", filepath.Join(docPath, fmt.Sprintf("%s.md", t.Name))) - if err := file.Close(); err != nil { + if err := f.Close(); err != nil { return errors.WithStack(err) } } + + // viewpoints + for i, v := range s.Viewpoints { + fn := fmt.Sprintf("viewpoint-%d.md", i) + f, err := os.Create(filepath.Clean(filepath.Join(fullPath, fn))) + if err != nil { + _ = f.Close() + return errors.WithStack(err) + } + if err := md.OutputViewpoint(f, i, v); err != nil { + _ = f.Close() + return errors.WithStack(err) + } + fmt.Printf("%s\n", filepath.Join(docPath, fn)) + if err := f.Close(); err != nil { + return errors.WithStack(err) + } + } + return nil } @@ -269,11 +315,12 @@ func DiffSchemaAndDocs(docPath string, s *schema.Schema, c *config.Config) (stri if err != nil { return "", errors.WithStack(err) } + diffed := map[string]struct{}{} // README.md md := New(c) - b := new(bytes.Buffer) - if err := md.OutputSchema(b, s); err != nil { + buf := new(bytes.Buffer) + if err := md.OutputSchema(buf, s); err != nil { return "", errors.WithStack(err) } @@ -293,7 +340,7 @@ func DiffSchemaAndDocs(docPath string, s *schema.Schema, c *config.Config) (stri d := difflib.UnifiedDiff{ A: difflib.SplitLines(string(a)), - B: difflib.SplitLines(b.String()), + B: difflib.SplitLines(buf.String()), FromFile: from, ToFile: to, Context: 3, @@ -304,31 +351,58 @@ func DiffSchemaAndDocs(docPath string, s *schema.Schema, c *config.Config) (stri diff += fmt.Sprintf("diff '%s' '%s'\n", from, to) diff += text } + diffed["README.md"] = struct{}{} // tables - diffed := map[string]struct{}{ - "README.md": struct{}{}, - } for _, t := range s.Tables { - b := new(bytes.Buffer) + buf := new(bytes.Buffer) to := fmt.Sprintf("%s %s", mdsn, t.Name) + if err := md.OutputTable(buf, t); err != nil { + return "", errors.WithStack(err) + } + fn := fmt.Sprintf("%s.md", t.Name) + targetPath := filepath.Join(fullPath, fn) + a, err := os.ReadFile(filepath.Clean(targetPath)) + if err != nil { + a = []byte{} + } + from := filepath.Join(docPath, fn) - md := New(c) + d := difflib.UnifiedDiff{ + A: difflib.SplitLines(string(a)), + B: difflib.SplitLines(buf.String()), + FromFile: from, + ToFile: to, + Context: 3, + } - if err := md.OutputTable(b, t); err != nil { + text, _ := difflib.GetUnifiedDiffString(d) + if text != "" { + diff += fmt.Sprintf("diff '%s' '%s'\n", from, to) + diff += text + } + diffed[fn] = struct{}{} + } + + // viewpoints + for i, v := range s.Viewpoints { + buf := new(bytes.Buffer) + n := fmt.Sprintf("viewpoint-%d", i) + fn := fmt.Sprintf("viewpoint-%d.md", i) + to := fmt.Sprintf("%s %s", mdsn, n) + if err := md.OutputViewpoint(buf, i, v); err != nil { return "", errors.WithStack(err) } - targetPath := filepath.Join(fullPath, fmt.Sprintf("%s.md", t.Name)) - diffed[fmt.Sprintf("%s.md", t.Name)] = struct{}{} + targetPath := filepath.Join(fullPath, fn) a, err := os.ReadFile(filepath.Clean(targetPath)) if err != nil { a = []byte{} } - from := filepath.Join(docPath, fmt.Sprintf("%s.md", t.Name)) + from := filepath.Join(docPath, fn) d := difflib.UnifiedDiff{ A: difflib.SplitLines(string(a)), - B: difflib.SplitLines(b.String()), + B: difflib.SplitLines(buf.String()), FromFile: from, ToFile: to, Context: 3, @@ -339,8 +413,13 @@ func DiffSchemaAndDocs(docPath string, s *schema.Schema, c *config.Config) (stri diff += fmt.Sprintf("diff '%s' '%s'\n", from, to) diff += text } + diffed[fn] = struct{}{} + } + + files, err := os.ReadDir(fullPath) + if err != nil { + return "", errors.WithStack(err) } - files, _ := os.ReadDir(fullPath) for _, f := range files { if _, ok := diffed[f.Name()]; ok { continue @@ -378,7 +457,7 @@ func DiffSchemaAndDocs(docPath string, s *schema.Schema, c *config.Config) (stri } func (m *Md) indexTemplate() (string, error) { - if len(m.config.Templates.MD.Index) > 0 { + if m.config.Templates.MD.Index != "" { tb, err := os.ReadFile(m.config.Templates.MD.Index) if err != nil { return "", errors.WithStack(err) @@ -394,7 +473,7 @@ func (m *Md) indexTemplate() (string, error) { } func (m *Md) tableTemplate() (string, error) { - if len(m.config.Templates.MD.Table) > 0 { + if m.config.Templates.MD.Table != "" { tb, err := os.ReadFile(m.config.Templates.MD.Table) if err != nil { return "", errors.WithStack(err) @@ -409,6 +488,22 @@ func (m *Md) tableTemplate() (string, error) { } } +func (m *Md) viewpointTemplate() (string, error) { + if m.config.Templates.MD.Viewpoint != "" { + tb, err := os.ReadFile(m.config.Templates.MD.Viewpoint) + if err != nil { + return "", errors.WithStack(err) + } + return string(tb), nil + } else { + tb, err := m.tmpl.ReadFile("templates/viewpoint.md.tmpl") + if err != nil { + return "", errors.WithStack(err) + } + return string(tb), nil + } +} + func (m *Md) makeSchemaTemplateData(s *schema.Schema) map[string]interface{} { number := m.config.Format.Number adjust := m.config.Format.Adjust diff --git a/output/md/md_test.go b/output/md/md_test.go index 21b2f05a3..897cd85a6 100644 --- a/output/md/md_test.go +++ b/output/md/md_test.go @@ -22,7 +22,7 @@ var tests = []struct { gotFile string wantFile string }{ - {"README.md", "png", false, false, true, false, "b", "README.md", "md_test_README.md"}, + {"README.md", "png", false, false, false, false, "b", "README.md", "md_test_README.md"}, {"a.md", "png", false, false, true, false, "b", "a.md", "md_test_a.md"}, {"--adjust option", "png", true, false, true, false, "b", "README.md", "md_test_README.md.adjust"}, {"number", "png", false, true, true, false, "b", "README.md", "md_test_README.md.number"}, @@ -31,6 +31,9 @@ var tests = []struct { {"mermaid a.md", "mermaid", false, false, false, false, "b", "a.md", "md_test_a.md.mermaid"}, {"showOnlyFirstParagraph README.md", "png", false, false, false, true, "b", "README.md", "md_test_README.md.first_para"}, {"showOnlyFirstParagraph a.md", "png", false, false, false, true, "b", "a.md", "md_test_a.md.first_para"}, + + {"viewpoint-1.md", "png", false, false, false, false, "b", "viewpoint-1.md", "md_test_viewpoint-1.md"}, + {"viewpoint-1.md", "mermaid", false, false, false, false, "b", "viewpoint-1.md", "md_test_viewpoint-1.md.mermaid"}, } var testsTemplate = []struct { diff --git a/output/md/templates/viewpoint.md.tmpl b/output/md/templates/viewpoint.md.tmpl new file mode 100644 index 000000000..9eb715799 --- /dev/null +++ b/output/md/templates/viewpoint.md.tmpl @@ -0,0 +1,20 @@ +# {{ .Name }} +{{- if ne .Desc "" }} + +## {{ "Description" | lookup }} + +{{ .Desc | nl2mdnl }} +{{- end }} +{{- if .er }} + +{{ .erDiagram }} +{{- end }} + +## {{ "Tables" | lookup }} +{{ range $t := .Tables }} +|{{ range $d := $t }} {{ $d | nl2br }} |{{ end }} +{{- end -}} + +--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/schema/filter.go b/schema/filter.go new file mode 100644 index 000000000..6d6518f71 --- /dev/null +++ b/schema/filter.go @@ -0,0 +1,137 @@ +package schema + +import ( + "fmt" + "strings" + + "github.com/minio/pkg/wildcard" + "github.com/pkg/errors" +) + +type FilterOption struct { + Include []string + Exclude []string + IncludeLabels []string + Distance int +} + +func (s *Schema) Filter(opt *FilterOption) error { + i := append(opt.Include, s.NormalizeTableNames(opt.Include)...) + e := append(opt.Exclude, s.NormalizeTableNames(opt.Exclude)...) + + includes := []*Table{} + excludes := []*Table{} + for _, t := range s.Tables { + li, mi := matchLength(i, t.Name) + le, me := matchLength(e, t.Name) + ml := matchLabels(opt.IncludeLabels, t.Labels) + switch { + case mi: + if me && li < le { + excludes = append(excludes, t) + continue + } + includes = append(includes, t) + case ml: + if me { + excludes = append(excludes, t) + continue + } + includes = append(includes, t) + case len(opt.Include) == 0 && len(opt.IncludeLabels) == 0: + if me { + excludes = append(excludes, t) + continue + } + includes = append(includes, t) + default: + excludes = append(excludes, t) + } + } + + collects := []*Table{} + for _, t := range includes { + ts, _, err := t.CollectTablesAndRelations(opt.Distance, true) + if err != nil { + return err + } + for _, tt := range ts { + if !tt.Contains(includes) { + collects = append(collects, tt) + } + } + } + + for _, t := range excludes { + if t.Contains(collects) { + continue + } + err := excludeTableFromSchema(t.Name, s) + if err != nil { + return errors.Wrap(errors.WithStack(err), fmt.Sprintf("failed to filter table '%s'", t.Name)) + } + } + + return nil +} + +func excludeTableFromSchema(name string, s *Schema) error { + // Tables + tables := []*Table{} + for _, t := range s.Tables { + if t.Name != name { + tables = append(tables, t) + } + for _, c := range t.Columns { + // ChildRelations + childRelations := []*Relation{} + for _, r := range c.ChildRelations { + if r.Table.Name != name && r.ParentTable.Name != name { + childRelations = append(childRelations, r) + } + } + c.ChildRelations = childRelations + + // ParentRelations + parentRelations := []*Relation{} + for _, r := range c.ParentRelations { + if r.Table.Name != name && r.ParentTable.Name != name { + parentRelations = append(parentRelations, r) + } + } + c.ParentRelations = parentRelations + } + } + s.Tables = tables + + // Relations + relations := []*Relation{} + for _, r := range s.Relations { + if r.Table.Name != name && r.ParentTable.Name != name { + relations = append(relations, r) + } + } + s.Relations = relations + + return nil +} + +func matchLabels(il []string, l Labels) bool { + for _, ll := range l { + for _, ill := range il { + if wildcard.MatchSimple(ill, ll.Name) { + return true + } + } + } + return false +} + +func matchLength(s []string, e string) (int, bool) { + for _, v := range s { + if wildcard.MatchSimple(v, e) { + return len(strings.ReplaceAll(v, "*", "")), true + } + } + return 0, false +} diff --git a/schema/schema.go b/schema/schema.go index 420160b3a..3cc684f37 100644 --- a/schema/schema.go +++ b/schema/schema.go @@ -57,6 +57,8 @@ type Viewpoint struct { Desc string `yaml:"desc,omitempty"` Labels []string `yaml:"labels,omitempty"` Tables []string `yaml:"tables,omitempty"` + + Schema *Schema `yaml:"-"` } type Viewpoints []*Viewpoint @@ -353,6 +355,22 @@ func (s *Schema) Repair() error { s.Functions = nil } + // viewpoints should be created using as complete a schema as possible + for _, v := range s.Viewpoints { + cs, err := s.Clone() + if err != nil { + return errors.Wrap(err, "failed to repair viewpoint") + } + if err := cs.Filter(&FilterOption{ + Include: v.Tables, + IncludeLabels: v.Labels, + Distance: 0, + }); err != nil { + return errors.Wrap(err, "failed to repair viewpoint") + } + v.Schema = cs + } + return nil } diff --git a/testdata/md_test_README.md.golden b/testdata/md_test_README.md.golden index c5a392266..27448fe28 100644 --- a/testdata/md_test_README.md.golden +++ b/testdata/md_test_README.md.golden @@ -16,6 +16,10 @@ | [a](a.md) | 2 | TABLE A | | `blue` `green` | | [b](b.md) | 2 | table b | | `red` `green` | +## Relations + +![er](schema.png) + --- > Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/testdata/md_test_viewpoint-1.md.golden b/testdata/md_test_viewpoint-1.md.golden new file mode 100644 index 000000000..789137a7c --- /dev/null +++ b/testdata/md_test_viewpoint-1.md.golden @@ -0,0 +1,15 @@ +# label blue + +## Description + +select label blue + +![er](viewpoint-1.png) + +## Tables + +| Name | Columns | Comment | Type | Labels | +| ---- | ------- | ------- | ---- | ------ | +| [a](a.md) | 2 | table a | | `blue` `green` |--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/testdata/md_test_viewpoint-1.md.mermaid.golden b/testdata/md_test_viewpoint-1.md.mermaid.golden new file mode 100644 index 000000000..6882f56db --- /dev/null +++ b/testdata/md_test_viewpoint-1.md.mermaid.golden @@ -0,0 +1,23 @@ +# label blue + +## Description + +select label blue + +```mermaid +erDiagram + + +"a" { + INTEGER a + TEXT a2 +} +``` + +## Tables + +| Name | Columns | Comment | Type | Labels | +| ---- | ------- | ------- | ---- | ------ | +| [a](a.md) | 2 | table a | | `blue` `green` |--- + +> Generated by [tbls](https://github.com/k1LoW/tbls) diff --git a/testutil/schema.go b/testutil/schema.go index 51ead3936..06525df56 100644 --- a/testutil/schema.go +++ b/testutil/schema.go @@ -148,5 +148,8 @@ func NewSchema(t *testing.T) *schema.Schema { Meta: &schema.DriverMeta{}, }, } + if err := s.Repair(); err != nil { + t.Fatal(err) + } return s }