better_structure_sql 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 +41 -0
- data/LICENSE +21 -0
- data/README.md +557 -0
- data/app/controllers/better_structure_sql/application_controller.rb +61 -0
- data/app/controllers/better_structure_sql/schema_versions_controller.rb +243 -0
- data/app/helpers/better_structure_sql/schema_versions_helper.rb +46 -0
- data/app/views/better_structure_sql/schema_versions/index.html.erb +110 -0
- data/app/views/better_structure_sql/schema_versions/show.html.erb +186 -0
- data/app/views/layouts/better_structure_sql/application.html.erb +105 -0
- data/config/database.yml +3 -0
- data/config/routes.rb +12 -0
- data/lib/better_structure_sql/adapters/base_adapter.rb +234 -0
- data/lib/better_structure_sql/adapters/mysql_adapter.rb +476 -0
- data/lib/better_structure_sql/adapters/mysql_config.rb +32 -0
- data/lib/better_structure_sql/adapters/postgresql_adapter.rb +646 -0
- data/lib/better_structure_sql/adapters/postgresql_config.rb +25 -0
- data/lib/better_structure_sql/adapters/registry.rb +115 -0
- data/lib/better_structure_sql/adapters/sqlite_adapter.rb +644 -0
- data/lib/better_structure_sql/adapters/sqlite_config.rb +26 -0
- data/lib/better_structure_sql/configuration.rb +129 -0
- data/lib/better_structure_sql/database_version.rb +46 -0
- data/lib/better_structure_sql/dependency_resolver.rb +63 -0
- data/lib/better_structure_sql/dumper.rb +544 -0
- data/lib/better_structure_sql/engine.rb +28 -0
- data/lib/better_structure_sql/file_writer.rb +180 -0
- data/lib/better_structure_sql/formatter.rb +70 -0
- data/lib/better_structure_sql/generators/base.rb +33 -0
- data/lib/better_structure_sql/generators/domain_generator.rb +22 -0
- data/lib/better_structure_sql/generators/extension_generator.rb +23 -0
- data/lib/better_structure_sql/generators/foreign_key_generator.rb +43 -0
- data/lib/better_structure_sql/generators/function_generator.rb +33 -0
- data/lib/better_structure_sql/generators/index_generator.rb +50 -0
- data/lib/better_structure_sql/generators/materialized_view_generator.rb +31 -0
- data/lib/better_structure_sql/generators/pragma_generator.rb +23 -0
- data/lib/better_structure_sql/generators/sequence_generator.rb +27 -0
- data/lib/better_structure_sql/generators/table_generator.rb +126 -0
- data/lib/better_structure_sql/generators/trigger_generator.rb +54 -0
- data/lib/better_structure_sql/generators/type_generator.rb +47 -0
- data/lib/better_structure_sql/generators/view_generator.rb +27 -0
- data/lib/better_structure_sql/introspection/extensions.rb +29 -0
- data/lib/better_structure_sql/introspection/foreign_keys.rb +29 -0
- data/lib/better_structure_sql/introspection/functions.rb +29 -0
- data/lib/better_structure_sql/introspection/indexes.rb +29 -0
- data/lib/better_structure_sql/introspection/sequences.rb +29 -0
- data/lib/better_structure_sql/introspection/tables.rb +29 -0
- data/lib/better_structure_sql/introspection/triggers.rb +29 -0
- data/lib/better_structure_sql/introspection/types.rb +37 -0
- data/lib/better_structure_sql/introspection/views.rb +41 -0
- data/lib/better_structure_sql/introspection.rb +31 -0
- data/lib/better_structure_sql/manifest_generator.rb +65 -0
- data/lib/better_structure_sql/migration_patch.rb +196 -0
- data/lib/better_structure_sql/pg_version.rb +44 -0
- data/lib/better_structure_sql/railtie.rb +124 -0
- data/lib/better_structure_sql/schema_loader.rb +168 -0
- data/lib/better_structure_sql/schema_version.rb +86 -0
- data/lib/better_structure_sql/schema_versions.rb +213 -0
- data/lib/better_structure_sql/version.rb +5 -0
- data/lib/better_structure_sql/zip_generator.rb +81 -0
- data/lib/better_structure_sql.rb +81 -0
- data/lib/generators/better_structure_sql/install_generator.rb +44 -0
- data/lib/generators/better_structure_sql/migration_generator.rb +34 -0
- data/lib/generators/better_structure_sql/templates/README +49 -0
- data/lib/generators/better_structure_sql/templates/add_metadata_migration.rb.erb +25 -0
- data/lib/generators/better_structure_sql/templates/better_structure_sql.rb +46 -0
- data/lib/generators/better_structure_sql/templates/migration.rb.erb +26 -0
- data/lib/tasks/better_structure_sql.rake +190 -0
- metadata +299 -0
|
@@ -0,0 +1,646 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
module Adapters
|
|
5
|
+
# PostgreSQL adapter implementing all introspection and generation methods
|
|
6
|
+
#
|
|
7
|
+
# Provides full PostgreSQL support including extensions, custom types, materialized views,
|
|
8
|
+
# functions, triggers, sequences, and all standard database objects.
|
|
9
|
+
# Preserves existing query logic for backward compatibility.
|
|
10
|
+
class PostgresqlAdapter < BaseAdapter
|
|
11
|
+
# Introspection methods - migrated from Introspection modules
|
|
12
|
+
|
|
13
|
+
# Fetch all extensions from the database
|
|
14
|
+
#
|
|
15
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
16
|
+
# @return [Array<Hash>] Array of extension hashes with :name, :version, :schema
|
|
17
|
+
def fetch_extensions(connection)
|
|
18
|
+
query = <<~SQL.squish
|
|
19
|
+
SELECT extname, extversion, nspname as schema_name
|
|
20
|
+
FROM pg_extension
|
|
21
|
+
JOIN pg_namespace ON pg_namespace.oid = pg_extension.extnamespace
|
|
22
|
+
WHERE nspname NOT IN ('pg_catalog', 'information_schema')
|
|
23
|
+
ORDER BY extname
|
|
24
|
+
SQL
|
|
25
|
+
|
|
26
|
+
connection.execute(query).map do |row|
|
|
27
|
+
{
|
|
28
|
+
name: row['extname'],
|
|
29
|
+
version: row['extversion'],
|
|
30
|
+
schema: row['schema_name']
|
|
31
|
+
}
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Fetch all custom types (enums, composite types, domains) from the database
|
|
36
|
+
#
|
|
37
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
38
|
+
# @return [Array<Hash>] Array of type hashes with :name, :schema, :type, and type-specific attributes
|
|
39
|
+
def fetch_custom_types(connection)
|
|
40
|
+
query = <<~SQL.squish
|
|
41
|
+
SELECT
|
|
42
|
+
t.typname as name,
|
|
43
|
+
t.typtype as type,
|
|
44
|
+
n.nspname as schema
|
|
45
|
+
FROM pg_type t
|
|
46
|
+
JOIN pg_namespace n ON n.oid = t.typnamespace
|
|
47
|
+
LEFT JOIN pg_class c ON c.reltype = t.oid AND c.relkind IN ('r', 'v', 'm')
|
|
48
|
+
WHERE t.typtype IN ('e', 'c', 'd')
|
|
49
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
50
|
+
AND c.oid IS NULL
|
|
51
|
+
ORDER BY t.typname
|
|
52
|
+
SQL
|
|
53
|
+
|
|
54
|
+
connection.execute(query).map do |row|
|
|
55
|
+
type_data = {
|
|
56
|
+
name: row['schema'] == 'public' ? row['name'] : "#{row['schema']}.#{row['name']}",
|
|
57
|
+
schema: row['schema'],
|
|
58
|
+
type: type_category(row['type'])
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
case row['type']
|
|
62
|
+
when 'e'
|
|
63
|
+
type_data[:values] = fetch_enum_values(connection, row['name'])
|
|
64
|
+
when 'c'
|
|
65
|
+
type_data[:attributes] = fetch_composite_attributes(connection, row['name'])
|
|
66
|
+
when 'd'
|
|
67
|
+
type_data.merge!(fetch_domain_details(connection, row['name']))
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
type_data
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# Fetch all tables from the database
|
|
75
|
+
#
|
|
76
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
77
|
+
# @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
|
|
78
|
+
def fetch_tables(connection)
|
|
79
|
+
query = <<~SQL.squish
|
|
80
|
+
SELECT table_name, table_schema
|
|
81
|
+
FROM information_schema.tables
|
|
82
|
+
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
|
|
83
|
+
AND table_type = 'BASE TABLE'
|
|
84
|
+
ORDER BY table_name
|
|
85
|
+
SQL
|
|
86
|
+
|
|
87
|
+
connection.execute(query).map do |row|
|
|
88
|
+
table_name = row['table_name']
|
|
89
|
+
{
|
|
90
|
+
name: table_name,
|
|
91
|
+
schema: row['table_schema'],
|
|
92
|
+
columns: fetch_columns(connection, table_name),
|
|
93
|
+
primary_key: fetch_primary_key(connection, table_name),
|
|
94
|
+
constraints: fetch_constraints(connection, table_name)
|
|
95
|
+
}
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Fetch all indexes from the database
|
|
100
|
+
#
|
|
101
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
102
|
+
# @return [Array<Hash>] Array of index hashes with :schema, :table, :name, :definition
|
|
103
|
+
def fetch_indexes(connection)
|
|
104
|
+
query = <<~SQL.squish
|
|
105
|
+
SELECT
|
|
106
|
+
pi.schemaname,
|
|
107
|
+
pi.tablename,
|
|
108
|
+
pi.indexname,
|
|
109
|
+
pi.indexdef
|
|
110
|
+
FROM pg_indexes pi
|
|
111
|
+
LEFT JOIN pg_matviews mv ON mv.matviewname = pi.tablename AND mv.schemaname = pi.schemaname
|
|
112
|
+
WHERE pi.schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
113
|
+
AND mv.matviewname IS NULL
|
|
114
|
+
ORDER BY pi.tablename, pi.indexname
|
|
115
|
+
SQL
|
|
116
|
+
|
|
117
|
+
connection.execute(query).map do |row|
|
|
118
|
+
{
|
|
119
|
+
schema: row['schemaname'],
|
|
120
|
+
table: row['tablename'],
|
|
121
|
+
name: row['indexname'],
|
|
122
|
+
definition: row['indexdef']
|
|
123
|
+
}
|
|
124
|
+
end.reject { |idx| idx[:name].end_with?('_pkey') }
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# Fetch all foreign keys from the database
|
|
128
|
+
#
|
|
129
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
130
|
+
# @return [Array<Hash>] Array of foreign key hashes with :table, :name, :column, :foreign_table, :foreign_column, :on_update, :on_delete
|
|
131
|
+
def fetch_foreign_keys(connection)
|
|
132
|
+
query = <<~SQL.squish
|
|
133
|
+
SELECT
|
|
134
|
+
tc.table_name,
|
|
135
|
+
tc.constraint_name,
|
|
136
|
+
kcu.column_name,
|
|
137
|
+
ccu.table_name AS foreign_table_name,
|
|
138
|
+
ccu.column_name AS foreign_column_name,
|
|
139
|
+
rc.update_rule,
|
|
140
|
+
rc.delete_rule
|
|
141
|
+
FROM information_schema.table_constraints AS tc
|
|
142
|
+
JOIN information_schema.key_column_usage AS kcu
|
|
143
|
+
ON tc.constraint_name = kcu.constraint_name
|
|
144
|
+
AND tc.table_schema = kcu.table_schema
|
|
145
|
+
JOIN information_schema.constraint_column_usage AS ccu
|
|
146
|
+
ON ccu.constraint_name = tc.constraint_name
|
|
147
|
+
AND ccu.table_schema = tc.table_schema
|
|
148
|
+
JOIN information_schema.referential_constraints AS rc
|
|
149
|
+
ON rc.constraint_name = tc.constraint_name
|
|
150
|
+
AND rc.constraint_schema = tc.table_schema
|
|
151
|
+
WHERE tc.constraint_type = 'FOREIGN KEY'
|
|
152
|
+
AND tc.table_schema = 'public'
|
|
153
|
+
ORDER BY tc.table_name, tc.constraint_name
|
|
154
|
+
SQL
|
|
155
|
+
|
|
156
|
+
connection.execute(query).map do |row|
|
|
157
|
+
{
|
|
158
|
+
table: row['table_name'],
|
|
159
|
+
name: row['constraint_name'],
|
|
160
|
+
column: row['column_name'],
|
|
161
|
+
foreign_table: row['foreign_table_name'],
|
|
162
|
+
foreign_column: row['foreign_column_name'],
|
|
163
|
+
on_update: row['update_rule'],
|
|
164
|
+
on_delete: row['delete_rule']
|
|
165
|
+
}
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# Fetch all views from the database
|
|
170
|
+
#
|
|
171
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
172
|
+
# @return [Array<Hash>] Array of view hashes with :schema, :name, :definition
|
|
173
|
+
def fetch_views(connection)
|
|
174
|
+
query = <<~SQL.squish
|
|
175
|
+
SELECT
|
|
176
|
+
schemaname,
|
|
177
|
+
viewname,
|
|
178
|
+
definition
|
|
179
|
+
FROM pg_views
|
|
180
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
181
|
+
ORDER BY viewname
|
|
182
|
+
SQL
|
|
183
|
+
|
|
184
|
+
connection.execute(query).map do |row|
|
|
185
|
+
{
|
|
186
|
+
schema: row['schemaname'],
|
|
187
|
+
name: row['viewname'],
|
|
188
|
+
definition: row['definition']
|
|
189
|
+
}
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
# Fetch all materialized views from the database
|
|
194
|
+
#
|
|
195
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
196
|
+
# @return [Array<Hash>] Array of materialized view hashes with :schema, :name, :definition, :indexes
|
|
197
|
+
def fetch_materialized_views(connection)
|
|
198
|
+
query = <<~SQL.squish
|
|
199
|
+
SELECT
|
|
200
|
+
schemaname,
|
|
201
|
+
matviewname,
|
|
202
|
+
definition
|
|
203
|
+
FROM pg_matviews
|
|
204
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
205
|
+
ORDER BY matviewname
|
|
206
|
+
SQL
|
|
207
|
+
|
|
208
|
+
connection.execute(query).map do |row|
|
|
209
|
+
{
|
|
210
|
+
schema: row['schemaname'],
|
|
211
|
+
name: row['matviewname'],
|
|
212
|
+
definition: row['definition'],
|
|
213
|
+
indexes: fetch_materialized_view_indexes(connection, row['matviewname'])
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Fetch all functions from the database
|
|
219
|
+
#
|
|
220
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
221
|
+
# @return [Array<Hash>] Array of function hashes with :schema, :name, :definition, :arguments, :return_type, :language, :volatility, :strict, :security_definer
|
|
222
|
+
def fetch_functions(connection)
|
|
223
|
+
query = <<~SQL.squish
|
|
224
|
+
SELECT
|
|
225
|
+
n.nspname as schema,
|
|
226
|
+
p.proname as name,
|
|
227
|
+
pg_get_functiondef(p.oid) as definition,
|
|
228
|
+
pg_get_function_identity_arguments(p.oid) as arguments,
|
|
229
|
+
pg_get_function_result(p.oid) as return_type,
|
|
230
|
+
l.lanname as language,
|
|
231
|
+
p.provolatile as volatility,
|
|
232
|
+
p.proisstrict as strict,
|
|
233
|
+
p.prosecdef as security_definer
|
|
234
|
+
FROM pg_proc p
|
|
235
|
+
JOIN pg_namespace n ON n.oid = p.pronamespace
|
|
236
|
+
JOIN pg_language l ON l.oid = p.prolang
|
|
237
|
+
LEFT JOIN pg_depend d ON d.objid = p.oid AND d.deptype = 'e'
|
|
238
|
+
WHERE n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
239
|
+
AND p.prokind = 'f'
|
|
240
|
+
AND d.objid IS NULL
|
|
241
|
+
ORDER BY n.nspname, p.proname
|
|
242
|
+
SQL
|
|
243
|
+
|
|
244
|
+
connection.execute(query).map do |row|
|
|
245
|
+
{
|
|
246
|
+
schema: row['schema'],
|
|
247
|
+
name: row['name'],
|
|
248
|
+
definition: row['definition'],
|
|
249
|
+
arguments: row['arguments'],
|
|
250
|
+
return_type: row['return_type'],
|
|
251
|
+
language: row['language'],
|
|
252
|
+
volatility: volatility_code(row['volatility']),
|
|
253
|
+
strict: row['strict'],
|
|
254
|
+
security_definer: row['security_definer']
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
# Fetch all sequences from the database
|
|
260
|
+
#
|
|
261
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
262
|
+
# @return [Array<Hash>] Array of sequence hashes with :name, :schema, :start_value, :increment, :min_value, :max_value, :cache_size, :cycle
|
|
263
|
+
def fetch_sequences(connection)
|
|
264
|
+
query = <<~SQL.squish
|
|
265
|
+
SELECT
|
|
266
|
+
sequencename,
|
|
267
|
+
schemaname,
|
|
268
|
+
start_value,
|
|
269
|
+
increment_by,
|
|
270
|
+
min_value,
|
|
271
|
+
max_value,
|
|
272
|
+
cache_size,
|
|
273
|
+
cycle
|
|
274
|
+
FROM pg_sequences
|
|
275
|
+
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
|
|
276
|
+
ORDER BY sequencename
|
|
277
|
+
SQL
|
|
278
|
+
|
|
279
|
+
connection.execute(query).map do |row|
|
|
280
|
+
{
|
|
281
|
+
name: row['sequencename'],
|
|
282
|
+
schema: row['schemaname'],
|
|
283
|
+
start_value: row['start_value'],
|
|
284
|
+
increment: row['increment_by'],
|
|
285
|
+
min_value: row['min_value'],
|
|
286
|
+
max_value: row['max_value'],
|
|
287
|
+
cache_size: row['cache_size'],
|
|
288
|
+
cycle: row['cycle']
|
|
289
|
+
}
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# Fetch all triggers from the database
|
|
294
|
+
#
|
|
295
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
296
|
+
# @return [Array<Hash>] Array of trigger hashes with :schema, :name, :table_name, :definition
|
|
297
|
+
def fetch_triggers(connection)
|
|
298
|
+
query = <<~SQL.squish
|
|
299
|
+
SELECT
|
|
300
|
+
n.nspname as schema,
|
|
301
|
+
t.tgname as name,
|
|
302
|
+
c.relname as table_name,
|
|
303
|
+
pg_get_triggerdef(t.oid) as definition
|
|
304
|
+
FROM pg_trigger t
|
|
305
|
+
JOIN pg_class c ON c.oid = t.tgrelid
|
|
306
|
+
JOIN pg_namespace n ON n.oid = c.relnamespace
|
|
307
|
+
WHERE NOT t.tgisinternal
|
|
308
|
+
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
|
|
309
|
+
ORDER BY c.relname, t.tgname
|
|
310
|
+
SQL
|
|
311
|
+
|
|
312
|
+
connection.execute(query).map do |row|
|
|
313
|
+
{
|
|
314
|
+
schema: row['schema'],
|
|
315
|
+
name: row['name'],
|
|
316
|
+
table_name: row['table_name'],
|
|
317
|
+
definition: row['definition']
|
|
318
|
+
}
|
|
319
|
+
end
|
|
320
|
+
end
|
|
321
|
+
|
|
322
|
+
# Capability methods - PostgreSQL supports all features
|
|
323
|
+
|
|
324
|
+
# Indicates whether PostgreSQL supports extensions
|
|
325
|
+
#
|
|
326
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
327
|
+
def supports_extensions?
|
|
328
|
+
true
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Indicates whether PostgreSQL supports materialized views
|
|
332
|
+
#
|
|
333
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
334
|
+
def supports_materialized_views?
|
|
335
|
+
true
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
# Indicates whether PostgreSQL supports custom types
|
|
339
|
+
#
|
|
340
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
341
|
+
def supports_custom_types?
|
|
342
|
+
true
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
# Indicates whether PostgreSQL supports domains
|
|
346
|
+
#
|
|
347
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
348
|
+
def supports_domains?
|
|
349
|
+
true
|
|
350
|
+
end
|
|
351
|
+
|
|
352
|
+
# Indicates whether PostgreSQL supports functions
|
|
353
|
+
#
|
|
354
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
355
|
+
def supports_functions?
|
|
356
|
+
true
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# Indicates whether PostgreSQL supports triggers
|
|
360
|
+
#
|
|
361
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
362
|
+
def supports_triggers?
|
|
363
|
+
true
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Indicates whether PostgreSQL supports sequences
|
|
367
|
+
#
|
|
368
|
+
# @return [Boolean] Always true for PostgreSQL
|
|
369
|
+
def supports_sequences?
|
|
370
|
+
true
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
# Version detection
|
|
374
|
+
|
|
375
|
+
# Get the current PostgreSQL database version
|
|
376
|
+
#
|
|
377
|
+
# @return [String] Normalized version string (e.g., "14.5")
|
|
378
|
+
def database_version
|
|
379
|
+
@database_version ||= begin
|
|
380
|
+
version_string = connection.select_value('SELECT version()')
|
|
381
|
+
parse_version(version_string)
|
|
382
|
+
end
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
# Parse PostgreSQL version string into normalized format
|
|
386
|
+
#
|
|
387
|
+
# @param version_string [String] Raw version string from PostgreSQL (e.g., "PostgreSQL 14.5...")
|
|
388
|
+
# @return [String] Normalized version (e.g., "14.5") or "unknown" if parsing fails
|
|
389
|
+
def parse_version(version_string)
|
|
390
|
+
# Example: "PostgreSQL 14.5 (Ubuntu 14.5-1.pgdg20.04+1) on x86_64-pc-linux-gnu..."
|
|
391
|
+
# Extract major.minor version
|
|
392
|
+
match = version_string.match(/PostgreSQL (\d+\.\d+)/)
|
|
393
|
+
return 'unknown' unless match
|
|
394
|
+
|
|
395
|
+
match[1]
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
private
|
|
399
|
+
|
|
400
|
+
# Helper methods for introspection
|
|
401
|
+
|
|
402
|
+
# Fetch columns for a specific table
|
|
403
|
+
#
|
|
404
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
405
|
+
# @param table_name [String] Name of the table
|
|
406
|
+
# @return [Array<Hash>] Array of column hashes with :name, :type, :default, :nullable, etc.
|
|
407
|
+
def fetch_columns(connection, table_name)
|
|
408
|
+
query = <<~SQL.squish
|
|
409
|
+
SELECT
|
|
410
|
+
column_name,
|
|
411
|
+
data_type,
|
|
412
|
+
column_default,
|
|
413
|
+
is_nullable,
|
|
414
|
+
character_maximum_length,
|
|
415
|
+
numeric_precision,
|
|
416
|
+
numeric_scale,
|
|
417
|
+
udt_name
|
|
418
|
+
FROM information_schema.columns
|
|
419
|
+
WHERE table_name = $1
|
|
420
|
+
AND table_schema = 'public'
|
|
421
|
+
ORDER BY ordinal_position
|
|
422
|
+
SQL
|
|
423
|
+
|
|
424
|
+
connection.select_all(
|
|
425
|
+
query.gsub('$1', connection.quote(table_name))
|
|
426
|
+
).map do |row|
|
|
427
|
+
{
|
|
428
|
+
name: row['column_name'],
|
|
429
|
+
type: resolve_column_type(row),
|
|
430
|
+
default: row['column_default'],
|
|
431
|
+
nullable: row['is_nullable'] == 'YES',
|
|
432
|
+
length: row['character_maximum_length'],
|
|
433
|
+
precision: row['numeric_precision'],
|
|
434
|
+
scale: row['numeric_scale']
|
|
435
|
+
}
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
# Fetch primary key columns for a specific table
|
|
440
|
+
#
|
|
441
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
442
|
+
# @param table_name [String] Name of the table
|
|
443
|
+
# @return [Array<String>] Array of primary key column names
|
|
444
|
+
def fetch_primary_key(connection, table_name)
|
|
445
|
+
query = <<~SQL.squish
|
|
446
|
+
SELECT a.attname as column_name
|
|
447
|
+
FROM pg_index i
|
|
448
|
+
JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = ANY(i.indkey)
|
|
449
|
+
WHERE i.indrelid = $1::regclass
|
|
450
|
+
AND i.indisprimary
|
|
451
|
+
ORDER BY a.attnum
|
|
452
|
+
SQL
|
|
453
|
+
|
|
454
|
+
result = connection.select_all(
|
|
455
|
+
query.gsub('$1', connection.quote(table_name))
|
|
456
|
+
)
|
|
457
|
+
result.pluck('column_name')
|
|
458
|
+
end
|
|
459
|
+
|
|
460
|
+
# Fetch constraints (CHECK, UNIQUE) for a specific table
|
|
461
|
+
#
|
|
462
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
463
|
+
# @param table_name [String] Name of the table
|
|
464
|
+
# @return [Array<Hash>] Array of constraint hashes with :name, :definition, :type
|
|
465
|
+
def fetch_constraints(connection, table_name)
|
|
466
|
+
query = <<~SQL.squish
|
|
467
|
+
SELECT
|
|
468
|
+
conname as name,
|
|
469
|
+
pg_get_constraintdef(oid) as definition,
|
|
470
|
+
contype as type
|
|
471
|
+
FROM pg_constraint
|
|
472
|
+
WHERE conrelid = $1::regclass
|
|
473
|
+
AND contype IN ('c', 'u')
|
|
474
|
+
ORDER BY conname
|
|
475
|
+
SQL
|
|
476
|
+
|
|
477
|
+
connection.select_all(
|
|
478
|
+
query.gsub('$1', connection.quote(table_name))
|
|
479
|
+
).map do |row|
|
|
480
|
+
{
|
|
481
|
+
name: row['name'],
|
|
482
|
+
definition: row['definition'],
|
|
483
|
+
type: constraint_type(row['type'])
|
|
484
|
+
}
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# Fetch enum values for a specific enum type
|
|
489
|
+
#
|
|
490
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
491
|
+
# @param type_name [String] Name of the enum type
|
|
492
|
+
# @return [Array<String>] Array of enum values in sort order
|
|
493
|
+
def fetch_enum_values(connection, type_name)
|
|
494
|
+
query = <<~SQL.squish
|
|
495
|
+
SELECT e.enumlabel
|
|
496
|
+
FROM pg_enum e
|
|
497
|
+
JOIN pg_type t ON t.oid = e.enumtypid
|
|
498
|
+
WHERE t.typname = $1
|
|
499
|
+
ORDER BY e.enumsortorder
|
|
500
|
+
SQL
|
|
501
|
+
|
|
502
|
+
connection.select_all(
|
|
503
|
+
query.gsub('$1', connection.quote(type_name))
|
|
504
|
+
).pluck('enumlabel')
|
|
505
|
+
end
|
|
506
|
+
|
|
507
|
+
# Fetch attributes for a composite type
|
|
508
|
+
#
|
|
509
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
510
|
+
# @param type_name [String] Name of the composite type
|
|
511
|
+
# @return [Array<Hash>] Array of attribute hashes with :name, :type
|
|
512
|
+
def fetch_composite_attributes(connection, type_name)
|
|
513
|
+
query = <<~SQL.squish
|
|
514
|
+
SELECT
|
|
515
|
+
a.attname as name,
|
|
516
|
+
format_type(a.atttypid, a.atttypmod) as type
|
|
517
|
+
FROM pg_attribute a
|
|
518
|
+
JOIN pg_type t ON t.typrelid = a.attrelid
|
|
519
|
+
WHERE t.typname = $1
|
|
520
|
+
AND a.attnum > 0
|
|
521
|
+
AND NOT a.attisdropped
|
|
522
|
+
ORDER BY a.attnum
|
|
523
|
+
SQL
|
|
524
|
+
|
|
525
|
+
connection.select_all(
|
|
526
|
+
query.gsub('$1', connection.quote(type_name))
|
|
527
|
+
).map do |row|
|
|
528
|
+
{ name: row['name'], type: row['type'] }
|
|
529
|
+
end
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
# Fetch details for a domain type
|
|
533
|
+
#
|
|
534
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
535
|
+
# @param type_name [String] Name of the domain type
|
|
536
|
+
# @return [Hash] Domain details with :base_type, :constraint
|
|
537
|
+
def fetch_domain_details(connection, type_name)
|
|
538
|
+
query = <<~SQL.squish
|
|
539
|
+
SELECT
|
|
540
|
+
format_type(t.typbasetype, t.typtypmod) as base_type,
|
|
541
|
+
pg_get_constraintdef(c.oid) as constraint
|
|
542
|
+
FROM pg_type t
|
|
543
|
+
LEFT JOIN pg_constraint c ON c.contypid = t.oid
|
|
544
|
+
WHERE t.typname = $1
|
|
545
|
+
SQL
|
|
546
|
+
|
|
547
|
+
result = connection.select_all(
|
|
548
|
+
query.gsub('$1', connection.quote(type_name))
|
|
549
|
+
).first
|
|
550
|
+
{
|
|
551
|
+
base_type: result['base_type'],
|
|
552
|
+
constraint: result['constraint']
|
|
553
|
+
}
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Fetch indexes for a materialized view
|
|
557
|
+
#
|
|
558
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
559
|
+
# @param matview_name [String] Name of the materialized view
|
|
560
|
+
# @return [Array<String>] Array of index definition SQL strings
|
|
561
|
+
def fetch_materialized_view_indexes(connection, matview_name)
|
|
562
|
+
query = <<~SQL.squish
|
|
563
|
+
SELECT indexdef
|
|
564
|
+
FROM pg_indexes
|
|
565
|
+
WHERE tablename = $1
|
|
566
|
+
ORDER BY indexname
|
|
567
|
+
SQL
|
|
568
|
+
|
|
569
|
+
connection.select_all(
|
|
570
|
+
query.gsub('$1', connection.quote(matview_name))
|
|
571
|
+
).pluck('indexdef')
|
|
572
|
+
end
|
|
573
|
+
|
|
574
|
+
# Resolve PostgreSQL column type into normalized format
|
|
575
|
+
#
|
|
576
|
+
# @param row [Hash] Column information row from information_schema.columns
|
|
577
|
+
# @return [String] Normalized column type with length/precision if applicable
|
|
578
|
+
def resolve_column_type(row)
|
|
579
|
+
case row['data_type']
|
|
580
|
+
when 'ARRAY'
|
|
581
|
+
# For arrays, udt_name contains the base type with leading underscore (e.g., _varchar)
|
|
582
|
+
base_type = row['udt_name'].sub(/^_/, '')
|
|
583
|
+
"#{base_type}[]"
|
|
584
|
+
when 'character varying'
|
|
585
|
+
row['character_maximum_length'] ? "varchar(#{row['character_maximum_length']})" : 'varchar'
|
|
586
|
+
when 'character'
|
|
587
|
+
row['character_maximum_length'] ? "char(#{row['character_maximum_length']})" : 'char'
|
|
588
|
+
when 'numeric'
|
|
589
|
+
if row['numeric_precision'] && row['numeric_scale']
|
|
590
|
+
"numeric(#{row['numeric_precision']},#{row['numeric_scale']})"
|
|
591
|
+
else
|
|
592
|
+
'numeric'
|
|
593
|
+
end
|
|
594
|
+
when 'timestamp without time zone'
|
|
595
|
+
'timestamp'
|
|
596
|
+
when 'timestamp with time zone'
|
|
597
|
+
'timestamptz'
|
|
598
|
+
when 'time without time zone'
|
|
599
|
+
'time'
|
|
600
|
+
when 'USER-DEFINED'
|
|
601
|
+
row['udt_name']
|
|
602
|
+
else
|
|
603
|
+
row['data_type']
|
|
604
|
+
end
|
|
605
|
+
end
|
|
606
|
+
|
|
607
|
+
# Convert PostgreSQL constraint type code to symbol
|
|
608
|
+
#
|
|
609
|
+
# @param type_code [String] PostgreSQL constraint type code ('c' = check, 'u' = unique)
|
|
610
|
+
# @return [Symbol] Constraint type (:check, :unique, :unknown)
|
|
611
|
+
def constraint_type(type_code)
|
|
612
|
+
case type_code
|
|
613
|
+
when 'c' then :check
|
|
614
|
+
when 'u' then :unique
|
|
615
|
+
else :unknown
|
|
616
|
+
end
|
|
617
|
+
end
|
|
618
|
+
|
|
619
|
+
# Convert PostgreSQL type code to category string
|
|
620
|
+
#
|
|
621
|
+
# @param type_code [String] PostgreSQL type code ('e' = enum, 'c' = composite, 'd' = domain)
|
|
622
|
+
# @return [String] Type category ('enum', 'composite', 'domain', 'unknown')
|
|
623
|
+
def type_category(type_code)
|
|
624
|
+
case type_code
|
|
625
|
+
when 'e' then 'enum'
|
|
626
|
+
when 'c' then 'composite'
|
|
627
|
+
when 'd' then 'domain'
|
|
628
|
+
else 'unknown'
|
|
629
|
+
end
|
|
630
|
+
end
|
|
631
|
+
|
|
632
|
+
# Convert PostgreSQL volatility code to string
|
|
633
|
+
#
|
|
634
|
+
# @param code [String] PostgreSQL volatility code ('i' = immutable, 's' = stable, 'v' = volatile)
|
|
635
|
+
# @return [String] Volatility string ('IMMUTABLE', 'STABLE', 'VOLATILE')
|
|
636
|
+
def volatility_code(code)
|
|
637
|
+
case code
|
|
638
|
+
when 'i' then 'IMMUTABLE'
|
|
639
|
+
when 's' then 'STABLE'
|
|
640
|
+
when 'v' then 'VOLATILE'
|
|
641
|
+
else 'VOLATILE'
|
|
642
|
+
end
|
|
643
|
+
end
|
|
644
|
+
end
|
|
645
|
+
end
|
|
646
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
module Adapters
|
|
5
|
+
# PostgreSQL-specific configuration settings
|
|
6
|
+
#
|
|
7
|
+
# Provides configuration options specific to PostgreSQL database adapter.
|
|
8
|
+
# Currently uses the main Configuration class feature toggles.
|
|
9
|
+
# Future PostgreSQL-specific options may include pg_dump_compatibility_mode,
|
|
10
|
+
# use_pg_catalog_vs_information_schema, and minimum_version_check.
|
|
11
|
+
class PostgresqlConfig
|
|
12
|
+
# PostgreSQL-specific feature toggles can be added here
|
|
13
|
+
# For now, we're using the main Configuration class feature toggles
|
|
14
|
+
# In the future, we might add PostgreSQL-specific options like:
|
|
15
|
+
# - pg_dump_compatibility_mode
|
|
16
|
+
# - use_pg_catalog_vs_information_schema
|
|
17
|
+
# - minimum_version_check
|
|
18
|
+
|
|
19
|
+
# Initialize PostgreSQL configuration with default values
|
|
20
|
+
def initialize
|
|
21
|
+
# Future PostgreSQL-specific settings will be initialized here
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|