pg-erd 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop +1 -0
- data/.rubocop.yml +35 -0
- data/Dockerfile +12 -0
- data/Gemfile +3 -0
- data/Gemfile.lock +25 -0
- data/LICENSE +674 -0
- data/README.md +66 -0
- data/bin/pg-erd +63 -0
- data/bin/pg-erd-everything +66 -0
- data/bin/pg-erd-png +4 -0
- data/bin/pg-erd-svg +3 -0
- data/bin/pg-inspect +20 -0
- data/bin/pg-list-databases +7 -0
- data/lib/dot/id.rb +24 -0
- data/lib/dot/node.rb +4 -0
- data/lib/pgerd/column.rb +48 -0
- data/lib/pgerd/database.rb +75 -0
- data/lib/pgerd/erbal_t.rb +8 -0
- data/lib/pgerd/erd/table_node.rb +46 -0
- data/lib/pgerd/erd/view_node.rb +46 -0
- data/lib/pgerd/erd.rb +136 -0
- data/lib/pgerd/foreign_key.rb +29 -0
- data/lib/pgerd/table.rb +39 -0
- data/lib/pgerd/version.rb +3 -0
- data/lib/pgerd/view.rb +39 -0
- data/lib/pgerd.rb +2 -0
- metadata +121 -0
data/README.md
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Postgres ERD
|
|
2
|
+
|
|
3
|
+
## Generate Entity-Relationship Diagrams for Postgresql Databases
|
|
4
|
+
|
|
5
|
+
Postgres ERD is a program that allows you to easily generate a diagram based on a postgresql database schema. The diagram gives an overview of how your tables are related. Having a diagram to describes your tables is perfect for documentation.
|
|
6
|
+
|
|
7
|
+
Postgres ERD is heavily inspired by [Rails ERD](https://voormedia.github.io/rails-erd/) but it serves some different use cases. First and foremost it does not rely on Rails (like Rails ERD). Thus you can use it for any database schema regardless of your project's language.
|
|
8
|
+
|
|
9
|
+
## An Example
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Requirements
|
|
13
|
+
|
|
14
|
+
- Ruby 2.4+
|
|
15
|
+
- postgresql
|
|
16
|
+
|
|
17
|
+
# Getting started
|
|
18
|
+
|
|
19
|
+
[It's part of a two-step program](https://www.youtube.com/watch?v=_c1NJQ0UP_Q):
|
|
20
|
+
|
|
21
|
+
Install the gem
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
gem install pg-erd
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Use the thing:
|
|
28
|
+
|
|
29
|
+
```sh
|
|
30
|
+
pg-erd {{the-name-of-your-database}}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
This will output a diagram of your database in [dot format](http://www.graphviz.org/). You can save it
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
pg-erd {{the-name-of-your-database}} > my-awesome-database.dot
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
... or run it though graphviz to create a png file:
|
|
40
|
+
|
|
41
|
+
```sh
|
|
42
|
+
pg-erd {{the-name-of-your-database}} | dot -Tpng > erd.png
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
... which you may also do like this:
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
pg-erd --title "Secret Project X" --format png {{the-name-of-your-database}} > erd.png
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
There are many more formats and a few more useful options. You can find them all with `--help`
|
|
52
|
+
|
|
53
|
+
# Free extras
|
|
54
|
+
|
|
55
|
+
## pg-erd-everything
|
|
56
|
+
|
|
57
|
+
This command will create an ERD for every database on your system. It will write many image files, one for each database, to the current directory.
|
|
58
|
+
|
|
59
|
+
## pg-list-databases
|
|
60
|
+
|
|
61
|
+
Like `ls` but it lists the names of your postgres databases one by one.
|
|
62
|
+
|
|
63
|
+
## pg-inspect
|
|
64
|
+
|
|
65
|
+
This command outputs a summary of your database structure to the console. It shows the same information as an ERD but then in plain text.
|
|
66
|
+
|
data/bin/pg-erd
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'optparse'
|
|
3
|
+
require_relative '../lib/pgerd'
|
|
4
|
+
|
|
5
|
+
def parse_arguments!(args = ARGV)
|
|
6
|
+
options = {
|
|
7
|
+
format: 'dot',
|
|
8
|
+
size: nil,
|
|
9
|
+
show_id: true,
|
|
10
|
+
show_timestamps: true,
|
|
11
|
+
verbose: false
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
opt_parser = OptionParser.new do |opts|
|
|
15
|
+
opts.banner = "Usage: #{$0} [options] DATABASE"
|
|
16
|
+
opts.separator ''
|
|
17
|
+
|
|
18
|
+
opts.on("-fFORMAT", "--format FORMAT", GraphViz::Constants::FORMATS.join(', ') + " (default=dot)") do |format|
|
|
19
|
+
options[:format] = format.downcase
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on("--[no-]id", 'Show (or hide) the id column (default=show)') do |show_id|
|
|
23
|
+
options[:show_id] = show_id
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
opts.on("--[no-]timestamps", 'Show (or hide) the timestamp columns (default=show)') do |show_timestamps|
|
|
27
|
+
options[:show_timestamps] = show_timestamps
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on("-tTITLE", "--title TITLE", "override the title") do |title|
|
|
31
|
+
options[:title] = title
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("--a4", 'try to fit the erd on an A4') do |size|
|
|
35
|
+
options[:size] = 'A4'
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
opts.on("-v", "--[no-]verbose", "Verbose output") do |v|
|
|
39
|
+
options[:verbose] = v
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
|
43
|
+
puts opts
|
|
44
|
+
exit
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
opt_parser.parse!(args)
|
|
48
|
+
options
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
options = parse_arguments!
|
|
52
|
+
|
|
53
|
+
STDERR.puts options if options[:verbose]
|
|
54
|
+
|
|
55
|
+
require 'pg'
|
|
56
|
+
CONNECTION = PG.connect(dbname: ARGV.last)
|
|
57
|
+
|
|
58
|
+
include Pgerd
|
|
59
|
+
|
|
60
|
+
database = Database.new(ARGV.first)
|
|
61
|
+
erd = Erd.new(database, options)
|
|
62
|
+
|
|
63
|
+
puts erd.diagram.output(options[:format] => String)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'pg'
|
|
4
|
+
require 'optparse'
|
|
5
|
+
require_relative '../lib/pgerd'
|
|
6
|
+
include Pgerd
|
|
7
|
+
|
|
8
|
+
def parse_arguments!(args = ARGV)
|
|
9
|
+
options = {
|
|
10
|
+
format: 'svg',
|
|
11
|
+
show_id: true,
|
|
12
|
+
show_timestamps: true,
|
|
13
|
+
verbose: false
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
opt_parser = OptionParser.new do |opts|
|
|
17
|
+
opts.banner = "Usage: #{$0} [options] DATABASE"
|
|
18
|
+
opts.separator ''
|
|
19
|
+
|
|
20
|
+
# opts.on("-fFORMAT", "--format FORMAT", GraphViz::Constants::FORMATS.join(', ') + " (default=dot)") do |format|
|
|
21
|
+
# options[:format] = format.downcase
|
|
22
|
+
# end
|
|
23
|
+
|
|
24
|
+
opts.on("--[no-]id", 'Show (or hide) the id column (default=show)') do |show_id|
|
|
25
|
+
options[:show_id] = show_id
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
opts.on("--[no-]timestamps", 'Show (or hide) the timestamp columns (default=show)') do |show_timestamps|
|
|
29
|
+
options[:show_timestamps] = show_timestamps
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
opts.on("-tTITLE", "--title TITLE", "override the title") do |title|
|
|
33
|
+
options[:title] = title
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
opts.on("-v", "--[no-]verbose", "Verbose output") do |v|
|
|
37
|
+
options[:verbose] = v
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
|
41
|
+
puts opts
|
|
42
|
+
exit
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
opt_parser.parse!(args)
|
|
46
|
+
options
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
options = parse_arguments!
|
|
50
|
+
|
|
51
|
+
STDERR.puts options if options[:verbose]
|
|
52
|
+
|
|
53
|
+
def each_database
|
|
54
|
+
PG.connect.query("SELECT datname FROM pg_database WHERE datistemplate = false;").each do |result|
|
|
55
|
+
yield result['datname']
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
each_database do |name|
|
|
60
|
+
puts name
|
|
61
|
+
database = Database.new(name)
|
|
62
|
+
erd = Erd.new(database, options)
|
|
63
|
+
File.open("#{name}.#{options[:format]}", 'w') do |file|
|
|
64
|
+
file.puts erd.diagram.output(options[:format] => String)
|
|
65
|
+
end
|
|
66
|
+
end
|
data/bin/pg-erd-png
ADDED
data/bin/pg-erd-svg
ADDED
data/bin/pg-inspect
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
require 'pg'
|
|
3
|
+
CONNECTION = PG.connect(dbname: ARGV.first)
|
|
4
|
+
|
|
5
|
+
require_relative '../lib/pgerd'
|
|
6
|
+
include Pgerd
|
|
7
|
+
|
|
8
|
+
database = Database.new(ARGV.first)
|
|
9
|
+
|
|
10
|
+
database.tables.each do |table|
|
|
11
|
+
puts "#{table.name}"
|
|
12
|
+
puts "-" * 20
|
|
13
|
+
puts table.columns.map { |column| "#{column.name} - #{column.data_type}" }
|
|
14
|
+
puts
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
if database.foreign_keys.any?
|
|
18
|
+
puts '# foreign keys'
|
|
19
|
+
database.foreign_keys.each { |x| puts x }
|
|
20
|
+
end
|
data/lib/dot/id.rb
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module Dot
|
|
2
|
+
# An ID is one of the following:
|
|
3
|
+
|
|
4
|
+
# Any string of alphabetic ([a-zA-Z\200-\377]) characters, underscores ('_') or digits ([0-9]), not beginning with a digit;
|
|
5
|
+
# a numeral [-]?(.[0-9]+ | [0-9]+(.[0-9]*)? );
|
|
6
|
+
# any double-quoted string ("...") possibly containing escaped quotes ('");
|
|
7
|
+
# an HTML string (<...>).
|
|
8
|
+
class Id
|
|
9
|
+
def initialize(string)
|
|
10
|
+
@string = string.to_s
|
|
11
|
+
raise ArgumentError if @string.empty?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def escaped
|
|
15
|
+
@string.gsub!("\"", "\\\"")
|
|
16
|
+
@string.gsub!("\'", "\\\'")
|
|
17
|
+
@string
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_dot
|
|
21
|
+
"\"#{escaped}\""
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
data/lib/dot/node.rb
ADDED
data/lib/pgerd/column.rb
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
module Pgerd
|
|
2
|
+
class Column
|
|
3
|
+
include Comparable
|
|
4
|
+
attr_accessor :name, :data_type
|
|
5
|
+
ID = %w(id)
|
|
6
|
+
GROUPS = %i(id other timestamps)
|
|
7
|
+
|
|
8
|
+
def initialize(name, data_type)
|
|
9
|
+
@name = name
|
|
10
|
+
@data_type = data_type
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def <=>(other)
|
|
14
|
+
to_a_for_sorting <=> other.to_a_for_sorting
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def group
|
|
18
|
+
if ID.include? @name
|
|
19
|
+
:id
|
|
20
|
+
elsif @data_type.include?("timestamp")
|
|
21
|
+
:timestamps
|
|
22
|
+
else
|
|
23
|
+
:other
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def group_for_sorting
|
|
28
|
+
GROUPS.index(group)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def abbreviated_data_type
|
|
32
|
+
# The SQL standard requires that writing just timestamp be equivalent to timestamp without time zone,
|
|
33
|
+
# and PostgreSQL honors that behavior.
|
|
34
|
+
@data_type
|
|
35
|
+
.gsub('timestamp without time zone', 'timestamp')
|
|
36
|
+
.gsub(/\bwithout\b/, 'w/o')
|
|
37
|
+
.gsub(/\bwith\b/, 'w/')
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def to_s
|
|
41
|
+
"#{name} [label = <>]"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def to_a_for_sorting
|
|
45
|
+
[group_for_sorting, name, data_type]
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
require_relative 'foreign_key'
|
|
2
|
+
require_relative 'table'
|
|
3
|
+
require_relative 'view'
|
|
4
|
+
|
|
5
|
+
module Pgerd
|
|
6
|
+
class Database
|
|
7
|
+
attr_reader :name
|
|
8
|
+
TABLES_TO_IGNORE = %w(ar_internal_metadata schema_migrations)
|
|
9
|
+
|
|
10
|
+
def initialize(name)
|
|
11
|
+
@name = name
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all_table_names
|
|
15
|
+
connection
|
|
16
|
+
.query("SELECT tablename FROM pg_catalog.pg_tables")
|
|
17
|
+
.map { |table| table['tablename'] }
|
|
18
|
+
.reject { |name| name.start_with?('pg_') || name.start_with?('sql_')}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def all_view_names
|
|
22
|
+
connection
|
|
23
|
+
.query("SELECT viewname FROM pg_catalog.pg_views WHERE schemaname NOT IN ('pg_catalog', 'information_schema')")
|
|
24
|
+
.map { |view| view['viewname'] }
|
|
25
|
+
.reject { |name| name.start_with?('pg_') || name.start_with?('sql_')}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def foreign_keys
|
|
29
|
+
@foreign_keys ||= raw_foreign_keys.map { |data| ForeignKey.new(data) }
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def raw_foreign_keys
|
|
33
|
+
connection
|
|
34
|
+
.query <<-END_SQL
|
|
35
|
+
select c.constraint_name
|
|
36
|
+
, x.table_schema as from_schema
|
|
37
|
+
, x.table_name as from_table
|
|
38
|
+
, x.column_name as from_column
|
|
39
|
+
, y.table_schema as to_schema
|
|
40
|
+
, y.table_name as to_table
|
|
41
|
+
, y.column_name as to_column
|
|
42
|
+
from information_schema.referential_constraints c
|
|
43
|
+
join information_schema.key_column_usage x
|
|
44
|
+
on x.constraint_name = c.constraint_name
|
|
45
|
+
join information_schema.key_column_usage y
|
|
46
|
+
on y.ordinal_position = x.position_in_unique_constraint
|
|
47
|
+
and y.constraint_name = c.unique_constraint_name
|
|
48
|
+
order by c.constraint_name, x.ordinal_position
|
|
49
|
+
END_SQL
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def table_names
|
|
53
|
+
all_table_names - TABLES_TO_IGNORE
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def tables
|
|
57
|
+
@tables ||=
|
|
58
|
+
table_names
|
|
59
|
+
.map { |name| Table.new(self, name) }
|
|
60
|
+
.sort_by { |table| table.foreign_keys.size }
|
|
61
|
+
.reverse
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def views
|
|
65
|
+
@views ||=
|
|
66
|
+
all_view_names
|
|
67
|
+
.map { |name| View.new(self, name) }
|
|
68
|
+
.reverse
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def connection
|
|
72
|
+
@connection ||= PG.connect(dbname: name)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative '../erbal_t'
|
|
2
|
+
|
|
3
|
+
module Pgerd
|
|
4
|
+
class Erd
|
|
5
|
+
class TableNode
|
|
6
|
+
TABLE_TEMPLATE = <<-END_HTML
|
|
7
|
+
<table border="1" align="center" cellborder="0" cellpadding="2" cellspacing="2">
|
|
8
|
+
<tr><td bgcolor="#f8f8f8" align="center" cellpadding="8"><font point-size="11"><%= name %></font></td></tr>
|
|
9
|
+
<% grouped_columns.each_with_index do |group, index| %>
|
|
10
|
+
<% if index > 0 %>
|
|
11
|
+
<tr><td height='3'></td></tr>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% group.each do |column| %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td align="left" port="<%= column.name %>"><%= column.name %> <font color="grey60"><%= column.abbreviated_data_type %></font></td>
|
|
16
|
+
</tr>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</table>
|
|
20
|
+
END_HTML
|
|
21
|
+
|
|
22
|
+
attr_reader :table, :groups_to_hide
|
|
23
|
+
|
|
24
|
+
def initialize(table, options = {})
|
|
25
|
+
@table = table
|
|
26
|
+
|
|
27
|
+
@groups_to_hide = []
|
|
28
|
+
@groups_to_hide << :id unless options.fetch(:show_id, true)
|
|
29
|
+
@groups_to_hide << :timestamps unless options.fetch(:show_timestamps, true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.render(table, options = {})
|
|
33
|
+
new(table, options).to_html
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_html
|
|
37
|
+
grouped_columns = @table.columns
|
|
38
|
+
.reject { |column| @groups_to_hide.include? column.group }
|
|
39
|
+
.group_by(&:group)
|
|
40
|
+
.values
|
|
41
|
+
|
|
42
|
+
ErbalT.new(name: @table.name, grouped_columns: grouped_columns).render(TABLE_TEMPLATE).strip
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
require_relative '../erbal_t'
|
|
2
|
+
|
|
3
|
+
module Pgerd
|
|
4
|
+
class Erd
|
|
5
|
+
class ViewNode
|
|
6
|
+
VIEW_TEMPLATE = <<-END_HTML
|
|
7
|
+
<table border="1" align="center" cellborder="0" cellpadding="2" cellspacing="2">
|
|
8
|
+
<tr><td bgcolor="#fafaf8" align="center" cellpadding="8"><font point-size="11"><%= name %> *view*</font></td></tr>
|
|
9
|
+
<% grouped_columns.each_with_index do |group, index| %>
|
|
10
|
+
<% if index > 0 %>
|
|
11
|
+
<tr><td height='3'></td></tr>
|
|
12
|
+
<% end %>
|
|
13
|
+
<% group.each do |column| %>
|
|
14
|
+
<tr>
|
|
15
|
+
<td align="left" port="<%= column.name %>"><%= column.name %> <font color="grey60"><%= column.abbreviated_data_type %></font></td>
|
|
16
|
+
</tr>
|
|
17
|
+
<% end %>
|
|
18
|
+
<% end %>
|
|
19
|
+
</table>
|
|
20
|
+
END_HTML
|
|
21
|
+
|
|
22
|
+
attr_reader :view, :groups_to_hide
|
|
23
|
+
|
|
24
|
+
def initialize(view, options = {})
|
|
25
|
+
@view = view
|
|
26
|
+
|
|
27
|
+
@groups_to_hide = []
|
|
28
|
+
@groups_to_hide << :id unless options.fetch(:show_id, true)
|
|
29
|
+
@groups_to_hide << :timestamps unless options.fetch(:show_timestamps, true)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def self.render(view, options = {})
|
|
33
|
+
new(view, options).to_html
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def to_html
|
|
37
|
+
grouped_columns = @view.columns
|
|
38
|
+
.reject { |column| @groups_to_hide.include? column.group }
|
|
39
|
+
.group_by(&:group)
|
|
40
|
+
.values
|
|
41
|
+
|
|
42
|
+
ErbalT.new(name: @view.name, grouped_columns: grouped_columns).render(VIEW_TEMPLATE).strip
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
data/lib/pgerd/erd.rb
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
require 'ruby-graphviz'
|
|
2
|
+
require 'ruby-progressbar'
|
|
3
|
+
require_relative 'erd/table_node'
|
|
4
|
+
require_relative 'erd/view_node'
|
|
5
|
+
|
|
6
|
+
module Pgerd
|
|
7
|
+
class Erd
|
|
8
|
+
attr_reader :diagram
|
|
9
|
+
|
|
10
|
+
def initialize(database, options = {})
|
|
11
|
+
@database = database
|
|
12
|
+
@title = options[:title] || @database.name
|
|
13
|
+
@size = options[:size].to_s.downcase
|
|
14
|
+
@options = options
|
|
15
|
+
|
|
16
|
+
create_a_digraph
|
|
17
|
+
draw_the_tables
|
|
18
|
+
draw_the_views
|
|
19
|
+
draw_the_foreign_keys
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def to_s
|
|
23
|
+
@diagram.output(dot: String)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def a4?
|
|
27
|
+
@size == 'a4'
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def create_a_digraph
|
|
33
|
+
@diagram = GraphViz.new(@title,
|
|
34
|
+
bgcolor: '#f4f8fa',
|
|
35
|
+
center: 1,
|
|
36
|
+
concentrate: true,
|
|
37
|
+
fontcolor: '#000000d0',
|
|
38
|
+
fontname: 'Helvetica',
|
|
39
|
+
fontsize: 13,
|
|
40
|
+
label: @title + "\n\n",
|
|
41
|
+
labelloc: 't',
|
|
42
|
+
# margin: 0,
|
|
43
|
+
nodesep: 0.2,
|
|
44
|
+
outputorder: 'nodesfirst',
|
|
45
|
+
overlap: false,
|
|
46
|
+
pad: '0.4,0.4',
|
|
47
|
+
rankdir: 'LR',
|
|
48
|
+
ranksep: '0.5',
|
|
49
|
+
splines: true,
|
|
50
|
+
type: :digraph)
|
|
51
|
+
|
|
52
|
+
if a4?
|
|
53
|
+
diagram[:size] = '8.3,11.7!'
|
|
54
|
+
diagram[:ratio] = 'compress'
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
@diagram.node[:color] = '#000000d0'
|
|
58
|
+
@diagram.node[:fillcolor] = '#ffffff'
|
|
59
|
+
@diagram.node[:fontname] = 'Helvetica'
|
|
60
|
+
@diagram.node[:fontsize] = 10
|
|
61
|
+
# @diagram.node[:margin] = "0.07,0.05"
|
|
62
|
+
@diagram.node[:margin] = "0"
|
|
63
|
+
@diagram.node[:penwidth] = "1.0"
|
|
64
|
+
@diagram.node[:shape] = 'box'
|
|
65
|
+
@diagram.node[:style] = 'filled'
|
|
66
|
+
|
|
67
|
+
@diagram.edge[:arrowsize] = "0.9"
|
|
68
|
+
@diagram.edge[:arrowtail] = "none"
|
|
69
|
+
@diagram.edge[:color] = '#000000d0'
|
|
70
|
+
@diagram.edge[:dir] = "both"
|
|
71
|
+
@diagram.edge[:fontname] = "Helvetica"
|
|
72
|
+
@diagram.edge[:fontsize] = "7"
|
|
73
|
+
@diagram.edge[:labelangle] = "32"
|
|
74
|
+
@diagram.edge[:labeldistance] = "1.8"
|
|
75
|
+
@diagram.edge[:penwidth] = "1.0"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def draw_the_tables
|
|
79
|
+
progress = ProgressBar.create(
|
|
80
|
+
title: 'Tables',
|
|
81
|
+
total: @database.tables.count,
|
|
82
|
+
format: '%t: |%B| %%%p %E',
|
|
83
|
+
output: $stderr)
|
|
84
|
+
@database.tables.each do |table|
|
|
85
|
+
draw_table(table)
|
|
86
|
+
progress.increment
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def draw_table(table)
|
|
91
|
+
html = Erd::TableNode.render(table, @options)
|
|
92
|
+
@diagram.add_node(table.name, label: "<#{html}>")
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def draw_the_views
|
|
96
|
+
progress = ProgressBar.create(
|
|
97
|
+
title: 'Views',
|
|
98
|
+
total: @database.tables.count,
|
|
99
|
+
format: '%t: |%B| %%%p %E',
|
|
100
|
+
output: $stderr)
|
|
101
|
+
@database.views.each do |view|
|
|
102
|
+
draw_view(view)
|
|
103
|
+
progress.increment
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def draw_view(view)
|
|
108
|
+
html = Erd::ViewNode.render(view, @options)
|
|
109
|
+
@diagram.add_node(view.name, label: "<#{html}>")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw_the_foreign_keys
|
|
113
|
+
progress = ProgressBar.create(
|
|
114
|
+
title: 'Foreign Keys',
|
|
115
|
+
total: @database.foreign_keys.count,
|
|
116
|
+
format: '%t: |%B| %%%p %E',
|
|
117
|
+
output: $stderr)
|
|
118
|
+
@database.foreign_keys.each do |fk|
|
|
119
|
+
draw_foreign_key(fk)
|
|
120
|
+
progress.increment
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def draw_foreign_key(fk)
|
|
125
|
+
from = address(fk.from_table, fk.from_column)
|
|
126
|
+
to = address(fk.to_table, fk.to_column)
|
|
127
|
+
|
|
128
|
+
@diagram.add_edge(from, to, weight: 2)
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def address(table, column)
|
|
132
|
+
return table if column == 'id'
|
|
133
|
+
{ table => column }
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Pgerd
|
|
2
|
+
class ForeignKey
|
|
3
|
+
include Comparable
|
|
4
|
+
attr_reader :from_table, :from_column, :to_table, :to_column
|
|
5
|
+
|
|
6
|
+
def initialize(data)
|
|
7
|
+
@from_table = data['from_table']
|
|
8
|
+
@from_column = data['from_column']
|
|
9
|
+
@to_table = data['to_table']
|
|
10
|
+
@to_column = data['to_column']
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_s
|
|
14
|
+
"#{@from_table}.#{@from_column} --> #{@to_table}.#{@to_column}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def <=>(other)
|
|
18
|
+
to_a_for_sorting <=> other.to_a_for_sorting
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_a_for_sorting
|
|
22
|
+
[@from_table, @from_column, @to_table, @to_column]
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def for_table?(table)
|
|
26
|
+
@from_table == table || @to_table == table
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|