rdbr 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/CHANGELOG.md +36 -0
- data/LICENSE.txt +21 -0
- data/README.md +155 -0
- data/app/public/application.js +609 -0
- data/app/public/database-diagram.js +602 -0
- data/app/public/styles.css +2141 -0
- data/app/views/data.haml +166 -0
- data/app/views/group.haml +50 -0
- data/app/views/index.haml +108 -0
- data/app/views/layout.haml +59 -0
- data/app/views/record.haml +81 -0
- data/demo/README.md +47 -0
- data/demo/compose.yml +34 -0
- data/demo/seed.rb +270 -0
- data/demo/smoke.rb +13 -0
- data/docs/RELEASING.md +30 -0
- data/docs/VALUE_PRESENTATION.md +54 -0
- data/exe/rdbr +5 -0
- data/lib/rdbr/access/read_only.rb +61 -0
- data/lib/rdbr/access.rb +6 -0
- data/lib/rdbr/browser_launcher.rb +71 -0
- data/lib/rdbr/catalog/connection.rb +68 -0
- data/lib/rdbr/catalog/errors.rb +12 -0
- data/lib/rdbr/catalog/mysql/queries.rb +131 -0
- data/lib/rdbr/catalog/mysql.rb +146 -0
- data/lib/rdbr/catalog/postgresql/queries.rb +171 -0
- data/lib/rdbr/catalog/postgresql.rb +138 -0
- data/lib/rdbr/catalog/reader.rb +30 -0
- data/lib/rdbr/catalog/serializer.rb +24 -0
- data/lib/rdbr/catalog/sqlite.rb +182 -0
- data/lib/rdbr/catalog.rb +20 -0
- data/lib/rdbr/cli.rb +74 -0
- data/lib/rdbr/cli_options.rb +63 -0
- data/lib/rdbr/metadata/column.rb +44 -0
- data/lib/rdbr/metadata/constraints.rb +25 -0
- data/lib/rdbr/metadata/database.rb +37 -0
- data/lib/rdbr/metadata/index.rb +30 -0
- data/lib/rdbr/metadata/keys.rb +56 -0
- data/lib/rdbr/metadata/namespace.rb +26 -0
- data/lib/rdbr/metadata/relation.rb +104 -0
- data/lib/rdbr/metadata/relationship_inference.rb +75 -0
- data/lib/rdbr/metadata/relationships.rb +126 -0
- data/lib/rdbr/metadata/values.rb +73 -0
- data/lib/rdbr/metadata.rb +10 -0
- data/lib/rdbr/presentation/record_identity.rb +125 -0
- data/lib/rdbr/query/connection.rb +138 -0
- data/lib/rdbr/query/errors.rb +33 -0
- data/lib/rdbr/query/lookup.rb +40 -0
- data/lib/rdbr/query/mysql.rb +15 -0
- data/lib/rdbr/query/page.rb +66 -0
- data/lib/rdbr/query/postgresql.rb +7 -0
- data/lib/rdbr/query/postgresql_dialect.rb +11 -0
- data/lib/rdbr/query/relational.rb +239 -0
- data/lib/rdbr/query/sqlite.rb +15 -0
- data/lib/rdbr/query/type_support.rb +68 -0
- data/lib/rdbr/query/value_limiter.rb +38 -0
- data/lib/rdbr/query.rb +32 -0
- data/lib/rdbr/version.rb +3 -0
- data/lib/rdbr/web/application.rb +234 -0
- data/lib/rdbr/web/array_value_helpers.rb +92 -0
- data/lib/rdbr/web/binary_value_helpers.rb +51 -0
- data/lib/rdbr/web/column_preferences.rb +56 -0
- data/lib/rdbr/web/database_graph.rb +91 -0
- data/lib/rdbr/web/deferred_column_value_helpers.rb +20 -0
- data/lib/rdbr/web/deferred_value_helpers.rb +76 -0
- data/lib/rdbr/web/document_value_helpers.rb +68 -0
- data/lib/rdbr/web/filter_presentation_helpers.rb +44 -0
- data/lib/rdbr/web/helpers.rb +42 -0
- data/lib/rdbr/web/identifier_value_helpers.rb +20 -0
- data/lib/rdbr/web/json_page_stream.rb +30 -0
- data/lib/rdbr/web/navigation_helpers.rb +115 -0
- data/lib/rdbr/web/presentation_helpers.rb +114 -0
- data/lib/rdbr/web/range_value_helpers.rb +86 -0
- data/lib/rdbr/web/record_value_helpers.rb +90 -0
- data/lib/rdbr/web/related_value_helpers.rb +99 -0
- data/lib/rdbr/web/relationship_loader.rb +65 -0
- data/lib/rdbr/web/search_value_helpers.rb +58 -0
- data/lib/rdbr/web/spatial_value_helpers.rb +83 -0
- data/lib/rdbr/web/through_relationship_loader.rb +90 -0
- data/lib/rdbr/web/vector_value_helpers.rb +59 -0
- data/lib/rdbr.rb +25 -0
- metadata +298 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
module RDBr
|
|
2
|
+
module Catalog
|
|
3
|
+
class MysqlQueries
|
|
4
|
+
SYSTEM_SCHEMAS = %w[information_schema mysql performance_schema sys].freeze
|
|
5
|
+
DATABASE_SQL = '/* rdbr: mysql database */ SELECT DATABASE() AS name'.freeze
|
|
6
|
+
NAMESPACES_SQL = <<~SQL.freeze
|
|
7
|
+
/* rdbr: mysql namespaces */
|
|
8
|
+
SELECT SCHEMA_NAME AS name
|
|
9
|
+
FROM information_schema.SCHEMATA
|
|
10
|
+
%<system_filter>s
|
|
11
|
+
ORDER BY SCHEMA_NAME
|
|
12
|
+
SQL
|
|
13
|
+
RELATIONS_SQL = <<~SQL.freeze
|
|
14
|
+
/* rdbr: mysql relations */
|
|
15
|
+
SELECT TABLE_SCHEMA AS namespace_name, TABLE_NAME AS relation_name,
|
|
16
|
+
TABLE_TYPE AS relation_kind, TABLE_COMMENT AS comment
|
|
17
|
+
FROM information_schema.TABLES
|
|
18
|
+
WHERE TABLE_TYPE IN ('BASE TABLE', 'VIEW', 'SYSTEM VIEW')
|
|
19
|
+
%<system_filter>s
|
|
20
|
+
ORDER BY TABLE_SCHEMA, TABLE_NAME
|
|
21
|
+
SQL
|
|
22
|
+
COLUMNS_SQL = <<~SQL.freeze
|
|
23
|
+
/* rdbr: mysql columns */
|
|
24
|
+
SELECT COLUMN_NAME AS name, DATA_TYPE AS logical_type, COLUMN_TYPE AS sql_type,
|
|
25
|
+
IS_NULLABLE AS nullable, COLUMN_DEFAULT AS default_expression,
|
|
26
|
+
GENERATION_EXPRESSION AS generated_expression, EXTRA AS extra,
|
|
27
|
+
ORDINAL_POSITION AS ordinal
|
|
28
|
+
FROM information_schema.COLUMNS
|
|
29
|
+
WHERE TABLE_SCHEMA = %<namespace>s AND TABLE_NAME = %<relation>s
|
|
30
|
+
ORDER BY ORDINAL_POSITION
|
|
31
|
+
SQL
|
|
32
|
+
CONSTRAINTS_SQL = <<~SQL.freeze
|
|
33
|
+
/* rdbr: mysql constraints */
|
|
34
|
+
SELECT constraint_record.CONSTRAINT_NAME AS name,
|
|
35
|
+
constraint_record.CONSTRAINT_TYPE AS constraint_type,
|
|
36
|
+
key_column.COLUMN_NAME AS column_name,
|
|
37
|
+
key_column.ORDINAL_POSITION AS ordinal,
|
|
38
|
+
key_column.REFERENCED_TABLE_SCHEMA AS referenced_namespace,
|
|
39
|
+
key_column.REFERENCED_TABLE_NAME AS referenced_relation,
|
|
40
|
+
key_column.REFERENCED_COLUMN_NAME AS referenced_column,
|
|
41
|
+
reference.UPDATE_RULE AS on_update, reference.DELETE_RULE AS on_delete
|
|
42
|
+
FROM information_schema.TABLE_CONSTRAINTS constraint_record
|
|
43
|
+
LEFT JOIN information_schema.KEY_COLUMN_USAGE key_column
|
|
44
|
+
ON key_column.CONSTRAINT_SCHEMA = constraint_record.CONSTRAINT_SCHEMA
|
|
45
|
+
AND key_column.TABLE_NAME = constraint_record.TABLE_NAME
|
|
46
|
+
AND key_column.CONSTRAINT_NAME = constraint_record.CONSTRAINT_NAME
|
|
47
|
+
LEFT JOIN information_schema.REFERENTIAL_CONSTRAINTS reference
|
|
48
|
+
ON reference.CONSTRAINT_SCHEMA = constraint_record.CONSTRAINT_SCHEMA
|
|
49
|
+
AND reference.TABLE_NAME = constraint_record.TABLE_NAME
|
|
50
|
+
AND reference.CONSTRAINT_NAME = constraint_record.CONSTRAINT_NAME
|
|
51
|
+
WHERE constraint_record.TABLE_SCHEMA = %<namespace>s
|
|
52
|
+
AND constraint_record.TABLE_NAME = %<relation>s
|
|
53
|
+
AND constraint_record.CONSTRAINT_TYPE IN ('PRIMARY KEY', 'UNIQUE', 'FOREIGN KEY')
|
|
54
|
+
ORDER BY constraint_record.CONSTRAINT_NAME, key_column.ORDINAL_POSITION
|
|
55
|
+
SQL
|
|
56
|
+
CHECKS_SQL = <<~SQL.freeze
|
|
57
|
+
/* rdbr: mysql checks */
|
|
58
|
+
SELECT constraint_record.CONSTRAINT_NAME AS name, check_record.CHECK_CLAUSE AS expression
|
|
59
|
+
FROM information_schema.TABLE_CONSTRAINTS constraint_record
|
|
60
|
+
INNER JOIN information_schema.CHECK_CONSTRAINTS check_record
|
|
61
|
+
ON check_record.CONSTRAINT_SCHEMA = constraint_record.CONSTRAINT_SCHEMA
|
|
62
|
+
AND check_record.CONSTRAINT_NAME = constraint_record.CONSTRAINT_NAME
|
|
63
|
+
WHERE constraint_record.TABLE_SCHEMA = %<namespace>s
|
|
64
|
+
AND constraint_record.TABLE_NAME = %<relation>s
|
|
65
|
+
AND constraint_record.CONSTRAINT_TYPE = 'CHECK'
|
|
66
|
+
ORDER BY constraint_record.CONSTRAINT_NAME
|
|
67
|
+
SQL
|
|
68
|
+
INDEXES_SQL = <<~SQL.freeze
|
|
69
|
+
/* rdbr: mysql indexes */
|
|
70
|
+
SELECT INDEX_NAME AS name, NON_UNIQUE AS non_unique, INDEX_TYPE AS index_type,
|
|
71
|
+
SEQ_IN_INDEX AS ordinal, COLUMN_NAME AS column_name
|
|
72
|
+
FROM information_schema.STATISTICS
|
|
73
|
+
WHERE TABLE_SCHEMA = %<namespace>s AND TABLE_NAME = %<relation>s
|
|
74
|
+
ORDER BY INDEX_NAME, SEQ_IN_INDEX
|
|
75
|
+
SQL
|
|
76
|
+
|
|
77
|
+
def initialize(connection, include_system: false)
|
|
78
|
+
@connection = connection
|
|
79
|
+
@include_system = include_system
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def database = connection.select_all(DATABASE_SQL)
|
|
83
|
+
|
|
84
|
+
def namespaces
|
|
85
|
+
connection.select_all(format(NAMESPACES_SQL, system_filter: namespace_filter))
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def relations
|
|
89
|
+
connection.select_all(format(RELATIONS_SQL, system_filter: relation_filter))
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def columns(namespace, relation) = select_for(COLUMNS_SQL, namespace, relation)
|
|
93
|
+
|
|
94
|
+
def constraints(namespace, relation) = select_for(CONSTRAINTS_SQL, namespace, relation)
|
|
95
|
+
|
|
96
|
+
def checks(namespace, relation)
|
|
97
|
+
connection.select_all_optional(format_for(CHECKS_SQL, namespace, relation))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def indexes(namespace, relation) = select_for(INDEXES_SQL, namespace, relation)
|
|
101
|
+
|
|
102
|
+
private
|
|
103
|
+
|
|
104
|
+
attr_reader :connection
|
|
105
|
+
|
|
106
|
+
def select_for(sql, namespace, relation)
|
|
107
|
+
connection.select_all(format_for(sql, namespace, relation))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def format_for(sql, namespace, relation)
|
|
111
|
+
format(sql, namespace: connection.quote(namespace), relation: connection.quote(relation))
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def namespace_filter
|
|
115
|
+
return '' if @include_system
|
|
116
|
+
|
|
117
|
+
"WHERE SCHEMA_NAME NOT IN (#{quoted_system_schemas})"
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def relation_filter
|
|
121
|
+
return '' if @include_system
|
|
122
|
+
|
|
123
|
+
"AND TABLE_SCHEMA NOT IN (#{quoted_system_schemas})"
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def quoted_system_schemas
|
|
127
|
+
SYSTEM_SCHEMAS.map{|name| connection.quote(name) }.join(', ')
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
require_relative 'mysql/queries'
|
|
2
|
+
|
|
3
|
+
module RDBr
|
|
4
|
+
module Catalog
|
|
5
|
+
class Mysql < Reader
|
|
6
|
+
RELATION_KINDS = { 'BASE TABLE' => :table, 'VIEW' => :view, 'SYSTEM VIEW' => :view }.freeze
|
|
7
|
+
FOREIGN_KEY_ACTIONS = {
|
|
8
|
+
'NO ACTION' => :no_action, 'RESTRICT' => :restrict, 'CASCADE' => :cascade,
|
|
9
|
+
'SET NULL' => :set_null, 'SET DEFAULT' => :set_default
|
|
10
|
+
}.freeze
|
|
11
|
+
TYPE_MAPPINGS = {
|
|
12
|
+
/bigint/ => :int8, /tinyint|smallint|mediumint/ => :int2, /int|integer/ => :int4,
|
|
13
|
+
/decimal|numeric/ => :numeric, /float/ => :float4, /double|real/ => :float8,
|
|
14
|
+
/varchar|enum|set/ => :varchar, /char/ => :char, /text/ => :text,
|
|
15
|
+
/datetime|timestamp/ => :timestamp, /date/ => :date, /time/ => :time, /year/ => :int2,
|
|
16
|
+
/bool|boolean/ => :boolean, /json/ => :json, /binary|blob/ => :blob, /geometry/ => :geometry,
|
|
17
|
+
/bit/ => :varbit
|
|
18
|
+
}.freeze
|
|
19
|
+
|
|
20
|
+
def initialize(connection, include_system: false)
|
|
21
|
+
super
|
|
22
|
+
@queries = MysqlQueries.new(connection, include_system: include_system)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def read
|
|
26
|
+
relations_by_namespace = queries.relations.group_by{|row| row.fetch('namespace_name') }
|
|
27
|
+
namespaces = queries.namespaces.map do |namespace_row|
|
|
28
|
+
name = namespace_row.fetch('name')
|
|
29
|
+
relations = relations_by_namespace.fetch(name, []).map{|row| build_relation(name, row) }
|
|
30
|
+
Metadata::Namespace.new(name: name, relations: relations)
|
|
31
|
+
end
|
|
32
|
+
Metadata::Database.new(name: database_name, adapter: database_adapter, namespaces: namespaces)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
attr_reader :queries
|
|
38
|
+
|
|
39
|
+
def database_name
|
|
40
|
+
queries.database.first&.fetch('name', nil) || 'mysql'
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def database_adapter
|
|
44
|
+
connection.database_product.to_s.include?('MariaDB') ? :mariadb : :mysql
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def build_relation(namespace, row)
|
|
48
|
+
relation = row.fetch('relation_name')
|
|
49
|
+
constraints = queries.constraints(namespace, relation)
|
|
50
|
+
Metadata::Relation.new(
|
|
51
|
+
name: relation,
|
|
52
|
+
kind: RELATION_KINDS.fetch(row.fetch('relation_kind')),
|
|
53
|
+
comment: present_comment(row['comment']),
|
|
54
|
+
columns: build_columns(queries.columns(namespace, relation)),
|
|
55
|
+
primary_key: build_primary_key(constraints),
|
|
56
|
+
foreign_keys: build_foreign_keys(constraints),
|
|
57
|
+
unique_constraints: build_unique_constraints(constraints),
|
|
58
|
+
check_constraints: build_checks(queries.checks(namespace, relation)),
|
|
59
|
+
indexes: build_indexes(queries.indexes(namespace, relation))
|
|
60
|
+
)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def build_columns(rows)
|
|
64
|
+
rows.map do |row|
|
|
65
|
+
generated = !row.fetch('generated_expression').to_s.empty?
|
|
66
|
+
Metadata::Column.new(
|
|
67
|
+
name: row.fetch('name'), logical_type: logical_type(row.fetch('logical_type'), row.fetch('sql_type')),
|
|
68
|
+
sql_type: row.fetch('sql_type'), nullable: row.fetch('nullable') == 'YES',
|
|
69
|
+
default: generated ? nil : row['default_expression'],
|
|
70
|
+
generated_expression: generated ? row.fetch('generated_expression') : nil,
|
|
71
|
+
identity: row.fetch('extra').to_s.include?('auto_increment') ? :by_default : nil,
|
|
72
|
+
ordinal: Integer(row.fetch('ordinal'))
|
|
73
|
+
)
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def build_primary_key(rows)
|
|
78
|
+
group = constraint_groups(rows).find{|name, _items| name == 'PRIMARY' }
|
|
79
|
+
Metadata::PrimaryKey.new(name: group.first, columns: columns(group.last)) if group
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def build_foreign_keys(rows)
|
|
83
|
+
constraint_groups(rows).filter_map do |name, items|
|
|
84
|
+
next unless items.first.fetch('constraint_type') == 'FOREIGN KEY'
|
|
85
|
+
|
|
86
|
+
Metadata::ForeignKey.new(
|
|
87
|
+
name: name, columns: columns(items), referenced_namespace: items.first.fetch('referenced_namespace'),
|
|
88
|
+
referenced_relation: items.first.fetch('referenced_relation'),
|
|
89
|
+
referenced_columns: items.map{|item| item.fetch('referenced_column') },
|
|
90
|
+
on_update: foreign_key_action(items.first['on_update']),
|
|
91
|
+
on_delete: foreign_key_action(items.first['on_delete'])
|
|
92
|
+
)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def build_unique_constraints(rows)
|
|
97
|
+
constraint_groups(rows).filter_map do |name, items|
|
|
98
|
+
next unless items.first.fetch('constraint_type') == 'UNIQUE'
|
|
99
|
+
|
|
100
|
+
Metadata::UniqueConstraint.new(name: name, columns: columns(items))
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def build_checks(rows)
|
|
105
|
+
rows.map do |row|
|
|
106
|
+
Metadata::CheckConstraint.new(name: row.fetch('name'), expression: row.fetch('expression'))
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def build_indexes(rows)
|
|
111
|
+
rows.group_by{|row| row.fetch('name') }.filter_map do |name, items|
|
|
112
|
+
index_columns = items.sort_by{|item| Integer(item.fetch('ordinal')) }.filter_map{|item| item['column_name'] }
|
|
113
|
+
next if index_columns.empty?
|
|
114
|
+
|
|
115
|
+
Metadata::Index.new(
|
|
116
|
+
name: name, columns: index_columns, unique: items.first.fetch('non_unique').to_i.zero?,
|
|
117
|
+
using: items.first.fetch('index_type').to_s.downcase
|
|
118
|
+
)
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def constraint_groups(rows)
|
|
123
|
+
rows.group_by{|row| row.fetch('name') }
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def columns(rows)
|
|
127
|
+
rows.sort_by{|row| Integer(row.fetch('ordinal')) }.map{|row| row.fetch('column_name') }
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def logical_type(data_type, sql_type)
|
|
131
|
+
return :boolean if data_type == 'tinyint' && sql_type.match?(/tinyint\(1\)/i)
|
|
132
|
+
|
|
133
|
+
normalized = data_type.downcase
|
|
134
|
+
TYPE_MAPPINGS.find{|pattern, _type| normalized.match?(pattern) }&.last || normalized.to_sym
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def foreign_key_action(value)
|
|
138
|
+
FOREIGN_KEY_ACTIONS.fetch(value.to_s.upcase, :no_action)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def present_comment(value)
|
|
142
|
+
value unless value.to_s.empty?
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
module RDBr
|
|
2
|
+
module Catalog
|
|
3
|
+
class PostgresqlQueries
|
|
4
|
+
DATABASE_SQL = '/* rdbr: database */ SELECT current_database() AS name'.freeze
|
|
5
|
+
NAMESPACES_SQL = <<~SQL.freeze
|
|
6
|
+
/* rdbr: namespaces */
|
|
7
|
+
SELECT namespace.nspname AS name
|
|
8
|
+
FROM pg_namespace namespace
|
|
9
|
+
WHERE true
|
|
10
|
+
%<system_filter>s
|
|
11
|
+
ORDER BY namespace.nspname
|
|
12
|
+
SQL
|
|
13
|
+
RELATIONS_SQL = <<~SQL.freeze
|
|
14
|
+
/* rdbr: relations */
|
|
15
|
+
SELECT
|
|
16
|
+
relation.oid::bigint AS oid,
|
|
17
|
+
namespace.nspname AS namespace_name,
|
|
18
|
+
relation.relname AS relation_name,
|
|
19
|
+
relation.relkind AS relation_kind,
|
|
20
|
+
obj_description(relation.oid, 'pg_class') AS comment
|
|
21
|
+
FROM pg_class relation
|
|
22
|
+
INNER JOIN pg_namespace namespace ON namespace.oid = relation.relnamespace
|
|
23
|
+
WHERE relation.relkind IN ('r', 'p', 'v', 'm', 'f')
|
|
24
|
+
%<system_filter>s
|
|
25
|
+
ORDER BY namespace.nspname, relation.relname
|
|
26
|
+
SQL
|
|
27
|
+
COLUMNS_SQL = <<~SQL.freeze
|
|
28
|
+
/* rdbr: columns */
|
|
29
|
+
SELECT
|
|
30
|
+
attribute.attname AS name,
|
|
31
|
+
type.typname AS logical_type,
|
|
32
|
+
format_type(attribute.atttypid, attribute.atttypmod) AS sql_type,
|
|
33
|
+
NOT attribute.attnotnull AS nullable,
|
|
34
|
+
pg_get_expr(default_value.adbin, default_value.adrelid, true) AS default_expression,
|
|
35
|
+
%<generated_kind>s AS generated_kind,
|
|
36
|
+
attribute.attidentity AS identity_kind,
|
|
37
|
+
attribute.attnum AS ordinal
|
|
38
|
+
FROM pg_attribute attribute
|
|
39
|
+
INNER JOIN pg_type type ON type.oid = attribute.atttypid
|
|
40
|
+
LEFT JOIN pg_attrdef default_value
|
|
41
|
+
ON default_value.adrelid = attribute.attrelid
|
|
42
|
+
AND default_value.adnum = attribute.attnum
|
|
43
|
+
WHERE attribute.attrelid = %<relation_oid>d
|
|
44
|
+
AND attribute.attnum > 0
|
|
45
|
+
AND NOT attribute.attisdropped
|
|
46
|
+
ORDER BY attribute.attnum
|
|
47
|
+
SQL
|
|
48
|
+
CONSTRAINTS_SQL = <<~SQL.freeze
|
|
49
|
+
/* rdbr: constraints */
|
|
50
|
+
SELECT
|
|
51
|
+
constraint_record.conname AS name,
|
|
52
|
+
constraint_record.contype AS constraint_type,
|
|
53
|
+
to_json(ARRAY(
|
|
54
|
+
SELECT attribute.attname
|
|
55
|
+
FROM unnest(constraint_record.conkey) WITH ORDINALITY key(attribute_number, position)
|
|
56
|
+
INNER JOIN pg_attribute attribute
|
|
57
|
+
ON attribute.attrelid = constraint_record.conrelid
|
|
58
|
+
AND attribute.attnum = key.attribute_number
|
|
59
|
+
ORDER BY key.position
|
|
60
|
+
))::text AS columns,
|
|
61
|
+
referenced_namespace.nspname AS referenced_namespace,
|
|
62
|
+
referenced_relation.relname AS referenced_relation,
|
|
63
|
+
to_json(ARRAY(
|
|
64
|
+
SELECT attribute.attname
|
|
65
|
+
FROM unnest(constraint_record.confkey) WITH ORDINALITY key(attribute_number, position)
|
|
66
|
+
INNER JOIN pg_attribute attribute
|
|
67
|
+
ON attribute.attrelid = constraint_record.confrelid
|
|
68
|
+
AND attribute.attnum = key.attribute_number
|
|
69
|
+
ORDER BY key.position
|
|
70
|
+
))::text AS referenced_columns,
|
|
71
|
+
constraint_record.confupdtype AS on_update,
|
|
72
|
+
constraint_record.confdeltype AS on_delete,
|
|
73
|
+
pg_get_expr(constraint_record.conbin, constraint_record.conrelid, true) AS check_expression
|
|
74
|
+
FROM pg_constraint constraint_record
|
|
75
|
+
LEFT JOIN pg_class referenced_relation ON referenced_relation.oid = constraint_record.confrelid
|
|
76
|
+
LEFT JOIN pg_namespace referenced_namespace
|
|
77
|
+
ON referenced_namespace.oid = referenced_relation.relnamespace
|
|
78
|
+
WHERE constraint_record.conrelid = %<relation_oid>d
|
|
79
|
+
AND constraint_record.contype IN ('p', 'u', 'f', 'c')
|
|
80
|
+
ORDER BY constraint_record.conname
|
|
81
|
+
SQL
|
|
82
|
+
INDEXES_SQL = <<~SQL.freeze
|
|
83
|
+
/* rdbr: indexes */
|
|
84
|
+
SELECT
|
|
85
|
+
index_relation.relname AS name,
|
|
86
|
+
index.indisunique AS unique,
|
|
87
|
+
access_method.amname AS using,
|
|
88
|
+
pg_get_expr(index.indpred, index.indrelid, true) AS predicate,
|
|
89
|
+
json_agg(
|
|
90
|
+
json_build_object(
|
|
91
|
+
'column', attribute.attname,
|
|
92
|
+
'expression', CASE
|
|
93
|
+
WHEN key.attribute_number = 0
|
|
94
|
+
THEN pg_get_indexdef(index.indexrelid, key.position::integer, true)
|
|
95
|
+
END
|
|
96
|
+
) ORDER BY key.position
|
|
97
|
+
)::text AS parts
|
|
98
|
+
FROM pg_index index
|
|
99
|
+
INNER JOIN pg_class index_relation ON index_relation.oid = index.indexrelid
|
|
100
|
+
INNER JOIN pg_am access_method ON access_method.oid = index_relation.relam
|
|
101
|
+
CROSS JOIN LATERAL unnest(index.indkey)
|
|
102
|
+
WITH ORDINALITY key(attribute_number, position)
|
|
103
|
+
LEFT JOIN pg_attribute attribute
|
|
104
|
+
ON attribute.attrelid = index.indrelid
|
|
105
|
+
AND attribute.attnum = key.attribute_number
|
|
106
|
+
WHERE index.indrelid = %<relation_oid>d
|
|
107
|
+
AND key.position <= index.indnkeyatts
|
|
108
|
+
GROUP BY
|
|
109
|
+
index.indexrelid,
|
|
110
|
+
index_relation.relname,
|
|
111
|
+
index.indisunique,
|
|
112
|
+
access_method.amname,
|
|
113
|
+
index.indpred,
|
|
114
|
+
index.indrelid
|
|
115
|
+
ORDER BY index_relation.relname
|
|
116
|
+
SQL
|
|
117
|
+
|
|
118
|
+
def initialize(connection, include_system: false, generated_columns: true)
|
|
119
|
+
@connection = connection
|
|
120
|
+
@include_system = include_system
|
|
121
|
+
@generated_columns = generated_columns
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def database
|
|
125
|
+
connection.select_all(DATABASE_SQL)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def namespaces
|
|
129
|
+
connection.select_all(format(NAMESPACES_SQL, system_filter: system_namespace_filter))
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def relations
|
|
133
|
+
connection.select_all(format(RELATIONS_SQL, system_filter: system_namespace_filter))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def columns(relation_oid)
|
|
137
|
+
connection.select_all(
|
|
138
|
+
format(
|
|
139
|
+
COLUMNS_SQL,
|
|
140
|
+
relation_oid: Integer(relation_oid),
|
|
141
|
+
generated_kind: generated_kind_expression
|
|
142
|
+
)
|
|
143
|
+
)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def constraints(relation_oid)
|
|
147
|
+
connection.select_all(format(CONSTRAINTS_SQL, relation_oid: Integer(relation_oid)))
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def indexes(relation_oid)
|
|
151
|
+
connection.select_all(format(INDEXES_SQL, relation_oid: Integer(relation_oid)))
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private
|
|
155
|
+
|
|
156
|
+
attr_reader :connection
|
|
157
|
+
|
|
158
|
+
def generated_kind_expression
|
|
159
|
+
return 'attribute.attgenerated' if @generated_columns
|
|
160
|
+
|
|
161
|
+
"''::text"
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def system_namespace_filter
|
|
165
|
+
return '' if @include_system
|
|
166
|
+
|
|
167
|
+
"AND namespace.nspname !~ '^pg_' AND namespace.nspname <> 'information_schema'"
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
require_relative 'postgresql/queries'
|
|
2
|
+
|
|
3
|
+
module RDBr
|
|
4
|
+
module Catalog
|
|
5
|
+
class Postgresql < Reader
|
|
6
|
+
RELATION_KINDS = {
|
|
7
|
+
'r' => :table,
|
|
8
|
+
'p' => :partitioned_table,
|
|
9
|
+
'v' => :view,
|
|
10
|
+
'm' => :materialized_view,
|
|
11
|
+
'f' => :foreign_table
|
|
12
|
+
}.freeze
|
|
13
|
+
FOREIGN_KEY_ACTIONS = {
|
|
14
|
+
'a' => :no_action,
|
|
15
|
+
'r' => :restrict,
|
|
16
|
+
'c' => :cascade,
|
|
17
|
+
'n' => :set_null,
|
|
18
|
+
'd' => :set_default
|
|
19
|
+
}.freeze
|
|
20
|
+
IDENTITY_KINDS = { 'a' => :always, 'd' => :by_default }.freeze
|
|
21
|
+
|
|
22
|
+
def initialize(connection, include_system: false)
|
|
23
|
+
super
|
|
24
|
+
@queries = PostgresqlQueries.new(
|
|
25
|
+
connection,
|
|
26
|
+
include_system: include_system,
|
|
27
|
+
generated_columns: generated_columns_supported?(connection.database_version)
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def read
|
|
32
|
+
relations_by_namespace = queries.relations.group_by{|row| row.fetch('namespace_name') }
|
|
33
|
+
namespaces = queries.namespaces.map do |namespace|
|
|
34
|
+
rows = relations_by_namespace.fetch(namespace.fetch('name'), [])
|
|
35
|
+
Metadata::Namespace.new(name: namespace.fetch('name'), relations: rows.map{|row| build_relation(row) })
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Metadata::Database.new(name: database_name, adapter: :postgresql, namespaces: namespaces)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def generated_columns_supported?(version)
|
|
44
|
+
return version >= 120_000 if version.is_a?(Integer)
|
|
45
|
+
|
|
46
|
+
version >= Gem::Version.new('12')
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
attr_reader :queries
|
|
50
|
+
|
|
51
|
+
def database_name
|
|
52
|
+
queries.database.first.fetch('name')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def build_relation(row)
|
|
56
|
+
relation_oid = Integer(row.fetch('oid'))
|
|
57
|
+
constraints = queries.constraints(relation_oid)
|
|
58
|
+
Metadata::Relation.new(
|
|
59
|
+
name: row.fetch('relation_name'),
|
|
60
|
+
kind: RELATION_KINDS.fetch(row.fetch('relation_kind')),
|
|
61
|
+
comment: row['comment'],
|
|
62
|
+
columns: build_columns(relation_oid),
|
|
63
|
+
primary_key: build_primary_key(constraints),
|
|
64
|
+
foreign_keys: build_foreign_keys(constraints),
|
|
65
|
+
unique_constraints: build_unique_constraints(constraints),
|
|
66
|
+
check_constraints: build_check_constraints(constraints),
|
|
67
|
+
indexes: build_indexes(relation_oid)
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def build_columns(relation_oid)
|
|
72
|
+
queries.columns(relation_oid).map do |row|
|
|
73
|
+
generated = row.fetch('generated_kind') != ''
|
|
74
|
+
expression = row['default_expression']
|
|
75
|
+
Metadata::Column.new(
|
|
76
|
+
name: row.fetch('name'),
|
|
77
|
+
logical_type: row.fetch('logical_type').to_sym,
|
|
78
|
+
sql_type: row.fetch('sql_type'),
|
|
79
|
+
nullable: row.fetch('nullable'),
|
|
80
|
+
default: generated ? nil : expression,
|
|
81
|
+
generated_expression: generated ? expression : nil,
|
|
82
|
+
identity: IDENTITY_KINDS[row.fetch('identity_kind')],
|
|
83
|
+
ordinal: Integer(row.fetch('ordinal'))
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def build_primary_key(rows)
|
|
89
|
+
row = rows.find{|item| item.fetch('constraint_type') == 'p' }
|
|
90
|
+
Metadata::PrimaryKey.new(name: row.fetch('name'), columns: json_array(row.fetch('columns'))) if row
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def build_foreign_keys(rows)
|
|
94
|
+
rows.select{|row| row.fetch('constraint_type') == 'f' }.map do |row|
|
|
95
|
+
Metadata::ForeignKey.new(
|
|
96
|
+
name: row.fetch('name'),
|
|
97
|
+
columns: json_array(row.fetch('columns')),
|
|
98
|
+
referenced_namespace: row.fetch('referenced_namespace'),
|
|
99
|
+
referenced_relation: row.fetch('referenced_relation'),
|
|
100
|
+
referenced_columns: json_array(row.fetch('referenced_columns')),
|
|
101
|
+
on_update: FOREIGN_KEY_ACTIONS.fetch(row.fetch('on_update')),
|
|
102
|
+
on_delete: FOREIGN_KEY_ACTIONS.fetch(row.fetch('on_delete'))
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def build_unique_constraints(rows)
|
|
108
|
+
rows.select{|row| row.fetch('constraint_type') == 'u' }.map do |row|
|
|
109
|
+
Metadata::UniqueConstraint.new(name: row.fetch('name'), columns: json_array(row.fetch('columns')))
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def build_check_constraints(rows)
|
|
114
|
+
rows.select{|row| row.fetch('constraint_type') == 'c' }.map do |row|
|
|
115
|
+
Metadata::CheckConstraint.new(name: row.fetch('name'), expression: row.fetch('check_expression'))
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def build_indexes(relation_oid)
|
|
120
|
+
queries.indexes(relation_oid).map do |row|
|
|
121
|
+
parts = JSON.parse(row.fetch('parts'))
|
|
122
|
+
Metadata::Index.new(
|
|
123
|
+
name: row.fetch('name'),
|
|
124
|
+
columns: parts.filter_map{|part| part['column'] },
|
|
125
|
+
expressions: parts.filter_map{|part| part['expression'] },
|
|
126
|
+
unique: row.fetch('unique'),
|
|
127
|
+
where: row['predicate'],
|
|
128
|
+
using: row.fetch('using')
|
|
129
|
+
)
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def json_array(value)
|
|
134
|
+
JSON.parse(value || '[]')
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
module RDBr
|
|
2
|
+
module Catalog
|
|
3
|
+
class Reader
|
|
4
|
+
def self.for(adapter_name)
|
|
5
|
+
return Postgresql if [ 'postgresql', 'postgis' ].include?(adapter_name)
|
|
6
|
+
return Sqlite if [ 'sqlite', 'sqlite3' ].include?(adapter_name)
|
|
7
|
+
return Mysql if [ 'mysql', 'mysql2', 'trilogy' ].include?(adapter_name)
|
|
8
|
+
|
|
9
|
+
raise UnsupportedAdapterError, "Unsupported database adapter: #{adapter_name}"
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(connection, include_system: false)
|
|
13
|
+
@connection = connection
|
|
14
|
+
@include_system = include_system
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def read
|
|
18
|
+
raise NotImplementedError
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
attr_reader :connection
|
|
24
|
+
|
|
25
|
+
def include_system?
|
|
26
|
+
@include_system
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
module RDBr
|
|
2
|
+
module Catalog
|
|
3
|
+
module Serializer
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def call(value)
|
|
7
|
+
case value
|
|
8
|
+
when Data
|
|
9
|
+
value.to_h.transform_values{|item| call(item) }
|
|
10
|
+
when Array
|
|
11
|
+
value.map{|item| call(item) }
|
|
12
|
+
when Hash
|
|
13
|
+
value.transform_values{|item| call(item) }
|
|
14
|
+
else
|
|
15
|
+
value
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pretty_json(value)
|
|
20
|
+
JSON.pretty_generate(call(value))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|