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.
Files changed (83) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +36 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +155 -0
  5. data/app/public/application.js +609 -0
  6. data/app/public/database-diagram.js +602 -0
  7. data/app/public/styles.css +2141 -0
  8. data/app/views/data.haml +166 -0
  9. data/app/views/group.haml +50 -0
  10. data/app/views/index.haml +108 -0
  11. data/app/views/layout.haml +59 -0
  12. data/app/views/record.haml +81 -0
  13. data/demo/README.md +47 -0
  14. data/demo/compose.yml +34 -0
  15. data/demo/seed.rb +270 -0
  16. data/demo/smoke.rb +13 -0
  17. data/docs/RELEASING.md +30 -0
  18. data/docs/VALUE_PRESENTATION.md +54 -0
  19. data/exe/rdbr +5 -0
  20. data/lib/rdbr/access/read_only.rb +61 -0
  21. data/lib/rdbr/access.rb +6 -0
  22. data/lib/rdbr/browser_launcher.rb +71 -0
  23. data/lib/rdbr/catalog/connection.rb +68 -0
  24. data/lib/rdbr/catalog/errors.rb +12 -0
  25. data/lib/rdbr/catalog/mysql/queries.rb +131 -0
  26. data/lib/rdbr/catalog/mysql.rb +146 -0
  27. data/lib/rdbr/catalog/postgresql/queries.rb +171 -0
  28. data/lib/rdbr/catalog/postgresql.rb +138 -0
  29. data/lib/rdbr/catalog/reader.rb +30 -0
  30. data/lib/rdbr/catalog/serializer.rb +24 -0
  31. data/lib/rdbr/catalog/sqlite.rb +182 -0
  32. data/lib/rdbr/catalog.rb +20 -0
  33. data/lib/rdbr/cli.rb +74 -0
  34. data/lib/rdbr/cli_options.rb +63 -0
  35. data/lib/rdbr/metadata/column.rb +44 -0
  36. data/lib/rdbr/metadata/constraints.rb +25 -0
  37. data/lib/rdbr/metadata/database.rb +37 -0
  38. data/lib/rdbr/metadata/index.rb +30 -0
  39. data/lib/rdbr/metadata/keys.rb +56 -0
  40. data/lib/rdbr/metadata/namespace.rb +26 -0
  41. data/lib/rdbr/metadata/relation.rb +104 -0
  42. data/lib/rdbr/metadata/relationship_inference.rb +75 -0
  43. data/lib/rdbr/metadata/relationships.rb +126 -0
  44. data/lib/rdbr/metadata/values.rb +73 -0
  45. data/lib/rdbr/metadata.rb +10 -0
  46. data/lib/rdbr/presentation/record_identity.rb +125 -0
  47. data/lib/rdbr/query/connection.rb +138 -0
  48. data/lib/rdbr/query/errors.rb +33 -0
  49. data/lib/rdbr/query/lookup.rb +40 -0
  50. data/lib/rdbr/query/mysql.rb +15 -0
  51. data/lib/rdbr/query/page.rb +66 -0
  52. data/lib/rdbr/query/postgresql.rb +7 -0
  53. data/lib/rdbr/query/postgresql_dialect.rb +11 -0
  54. data/lib/rdbr/query/relational.rb +239 -0
  55. data/lib/rdbr/query/sqlite.rb +15 -0
  56. data/lib/rdbr/query/type_support.rb +68 -0
  57. data/lib/rdbr/query/value_limiter.rb +38 -0
  58. data/lib/rdbr/query.rb +32 -0
  59. data/lib/rdbr/version.rb +3 -0
  60. data/lib/rdbr/web/application.rb +234 -0
  61. data/lib/rdbr/web/array_value_helpers.rb +92 -0
  62. data/lib/rdbr/web/binary_value_helpers.rb +51 -0
  63. data/lib/rdbr/web/column_preferences.rb +56 -0
  64. data/lib/rdbr/web/database_graph.rb +91 -0
  65. data/lib/rdbr/web/deferred_column_value_helpers.rb +20 -0
  66. data/lib/rdbr/web/deferred_value_helpers.rb +76 -0
  67. data/lib/rdbr/web/document_value_helpers.rb +68 -0
  68. data/lib/rdbr/web/filter_presentation_helpers.rb +44 -0
  69. data/lib/rdbr/web/helpers.rb +42 -0
  70. data/lib/rdbr/web/identifier_value_helpers.rb +20 -0
  71. data/lib/rdbr/web/json_page_stream.rb +30 -0
  72. data/lib/rdbr/web/navigation_helpers.rb +115 -0
  73. data/lib/rdbr/web/presentation_helpers.rb +114 -0
  74. data/lib/rdbr/web/range_value_helpers.rb +86 -0
  75. data/lib/rdbr/web/record_value_helpers.rb +90 -0
  76. data/lib/rdbr/web/related_value_helpers.rb +99 -0
  77. data/lib/rdbr/web/relationship_loader.rb +65 -0
  78. data/lib/rdbr/web/search_value_helpers.rb +58 -0
  79. data/lib/rdbr/web/spatial_value_helpers.rb +83 -0
  80. data/lib/rdbr/web/through_relationship_loader.rb +90 -0
  81. data/lib/rdbr/web/vector_value_helpers.rb +59 -0
  82. data/lib/rdbr.rb +25 -0
  83. 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