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,476 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
module Adapters
|
|
5
|
+
# MySQL adapter implementing introspection via information_schema and MySQL system tables.
|
|
6
|
+
#
|
|
7
|
+
# Provides MySQL-specific SQL generation with proper dialect support.
|
|
8
|
+
# This adapter handles MySQL's unique features including stored procedures, triggers,
|
|
9
|
+
# ENUM/SET types, and AUTO_INCREMENT sequences.
|
|
10
|
+
class MysqlAdapter < BaseAdapter
|
|
11
|
+
# Introspection methods using information_schema
|
|
12
|
+
|
|
13
|
+
# Fetch database extensions (not supported in MySQL)
|
|
14
|
+
#
|
|
15
|
+
# @param _connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection (unused)
|
|
16
|
+
# @return [Array] Empty array as MySQL doesn't support extensions like PostgreSQL
|
|
17
|
+
def fetch_extensions(_connection)
|
|
18
|
+
# MySQL doesn't support extensions like PostgreSQL
|
|
19
|
+
[]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Fetch custom types (not supported in MySQL)
|
|
23
|
+
#
|
|
24
|
+
# @param _connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection (unused)
|
|
25
|
+
# @return [Array] Empty array as MySQL doesn't support standalone custom types
|
|
26
|
+
# @note MySQL has ENUM and SET types, but they are defined inline with columns, not as custom types
|
|
27
|
+
def fetch_custom_types(_connection)
|
|
28
|
+
# MySQL has limited support for custom types (ENUM and SET are inline)
|
|
29
|
+
# Return empty array since ENUMs/SETs are defined per-column
|
|
30
|
+
[]
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Fetch all tables from the current database
|
|
34
|
+
#
|
|
35
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
36
|
+
# @return [Array<Hash>] Array of table hashes with :name, :schema, :columns, :primary_key, :constraints
|
|
37
|
+
def fetch_tables(connection)
|
|
38
|
+
query = <<~SQL.squish
|
|
39
|
+
SELECT
|
|
40
|
+
TABLE_NAME,
|
|
41
|
+
TABLE_SCHEMA
|
|
42
|
+
FROM information_schema.TABLES
|
|
43
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
44
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
|
45
|
+
ORDER BY TABLE_NAME
|
|
46
|
+
SQL
|
|
47
|
+
|
|
48
|
+
connection.execute(query).map do |row|
|
|
49
|
+
table_name = row[0] # MySQL returns arrays not hashes by default
|
|
50
|
+
{
|
|
51
|
+
name: table_name,
|
|
52
|
+
schema: row[1],
|
|
53
|
+
columns: fetch_columns(connection, table_name),
|
|
54
|
+
primary_key: fetch_primary_key(connection, table_name),
|
|
55
|
+
constraints: fetch_constraints(connection, table_name)
|
|
56
|
+
}
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Fetch all indexes from the current database
|
|
61
|
+
#
|
|
62
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
63
|
+
# @return [Array<Hash>] Array of index hashes with :table, :name, :columns, :unique, :type
|
|
64
|
+
def fetch_indexes(connection)
|
|
65
|
+
query = <<~SQL.squish
|
|
66
|
+
SELECT
|
|
67
|
+
TABLE_NAME,
|
|
68
|
+
INDEX_NAME,
|
|
69
|
+
COLUMN_NAME,
|
|
70
|
+
SEQ_IN_INDEX,
|
|
71
|
+
NON_UNIQUE,
|
|
72
|
+
INDEX_TYPE
|
|
73
|
+
FROM information_schema.STATISTICS
|
|
74
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
75
|
+
AND INDEX_NAME != 'PRIMARY'
|
|
76
|
+
ORDER BY TABLE_NAME, INDEX_NAME, SEQ_IN_INDEX
|
|
77
|
+
SQL
|
|
78
|
+
|
|
79
|
+
# Group by table and index name to build multi-column indexes
|
|
80
|
+
indexes_by_key = {}
|
|
81
|
+
|
|
82
|
+
connection.execute(query).each do |row|
|
|
83
|
+
table_name = row[0]
|
|
84
|
+
index_name = row[1]
|
|
85
|
+
column_name = row[2]
|
|
86
|
+
# seq_in_index = row[3] # Used for ordering, handled by ORDER BY in query
|
|
87
|
+
non_unique = row[4]
|
|
88
|
+
index_type = row[5]
|
|
89
|
+
|
|
90
|
+
key = "#{table_name}.#{index_name}"
|
|
91
|
+
indexes_by_key[key] ||= {
|
|
92
|
+
table: table_name,
|
|
93
|
+
name: index_name,
|
|
94
|
+
columns: [],
|
|
95
|
+
unique: non_unique.to_i.zero?,
|
|
96
|
+
type: index_type
|
|
97
|
+
}
|
|
98
|
+
indexes_by_key[key][:columns] << column_name
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
indexes_by_key.values
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Fetch all foreign keys from the current database
|
|
105
|
+
#
|
|
106
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
107
|
+
# @return [Array<Hash>] Array of foreign key hashes with :table, :name, :column, :foreign_table, :foreign_column, :on_update, :on_delete
|
|
108
|
+
def fetch_foreign_keys(connection)
|
|
109
|
+
query = <<~SQL.squish
|
|
110
|
+
SELECT
|
|
111
|
+
kcu.TABLE_NAME,
|
|
112
|
+
kcu.CONSTRAINT_NAME,
|
|
113
|
+
kcu.COLUMN_NAME,
|
|
114
|
+
kcu.REFERENCED_TABLE_NAME,
|
|
115
|
+
kcu.REFERENCED_COLUMN_NAME,
|
|
116
|
+
rc.UPDATE_RULE,
|
|
117
|
+
rc.DELETE_RULE
|
|
118
|
+
FROM information_schema.KEY_COLUMN_USAGE kcu
|
|
119
|
+
JOIN information_schema.REFERENTIAL_CONSTRAINTS rc
|
|
120
|
+
ON kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
|
121
|
+
AND kcu.TABLE_SCHEMA = rc.CONSTRAINT_SCHEMA
|
|
122
|
+
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
|
123
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
|
124
|
+
ORDER BY kcu.TABLE_NAME, kcu.CONSTRAINT_NAME
|
|
125
|
+
SQL
|
|
126
|
+
|
|
127
|
+
connection.execute(query).map do |row|
|
|
128
|
+
{
|
|
129
|
+
table: row[0],
|
|
130
|
+
name: row[1],
|
|
131
|
+
column: row[2],
|
|
132
|
+
foreign_table: row[3],
|
|
133
|
+
foreign_column: row[4],
|
|
134
|
+
on_update: row[5],
|
|
135
|
+
on_delete: row[6]
|
|
136
|
+
}
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Fetch all views from the current database
|
|
141
|
+
#
|
|
142
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
143
|
+
# @return [Array<Hash>] Array of view hashes with :schema, :name, :definition, :check_option, :updatable
|
|
144
|
+
def fetch_views(connection)
|
|
145
|
+
query = <<~SQL.squish
|
|
146
|
+
SELECT
|
|
147
|
+
TABLE_NAME,
|
|
148
|
+
VIEW_DEFINITION,
|
|
149
|
+
CHECK_OPTION,
|
|
150
|
+
IS_UPDATABLE
|
|
151
|
+
FROM information_schema.VIEWS
|
|
152
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
|
153
|
+
ORDER BY TABLE_NAME
|
|
154
|
+
SQL
|
|
155
|
+
|
|
156
|
+
connection.execute(query).map do |row|
|
|
157
|
+
{
|
|
158
|
+
schema: 'public', # MySQL doesn't use schemas like PostgreSQL
|
|
159
|
+
name: row[0],
|
|
160
|
+
definition: row[1],
|
|
161
|
+
check_option: row[2],
|
|
162
|
+
updatable: row[3] == 'YES'
|
|
163
|
+
}
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Fetch materialized views (not supported in MySQL)
|
|
168
|
+
#
|
|
169
|
+
# @param _connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection (unused)
|
|
170
|
+
# @return [Array] Empty array as MySQL doesn't support materialized views
|
|
171
|
+
def fetch_materialized_views(_connection)
|
|
172
|
+
# MySQL doesn't support materialized views
|
|
173
|
+
[]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Fetch all stored procedures and functions from the current database
|
|
177
|
+
#
|
|
178
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
179
|
+
# @return [Array<Hash>] Array of routine hashes with :schema, :name, :definition
|
|
180
|
+
def fetch_functions(connection)
|
|
181
|
+
query = <<~SQL.squish
|
|
182
|
+
SELECT
|
|
183
|
+
ROUTINE_NAME,
|
|
184
|
+
ROUTINE_TYPE
|
|
185
|
+
FROM information_schema.ROUTINES
|
|
186
|
+
WHERE ROUTINE_SCHEMA = DATABASE()
|
|
187
|
+
AND ROUTINE_TYPE IN ('PROCEDURE', 'FUNCTION')
|
|
188
|
+
ORDER BY ROUTINE_NAME
|
|
189
|
+
SQL
|
|
190
|
+
|
|
191
|
+
connection.execute(query).map do |row|
|
|
192
|
+
routine_name = row[0]
|
|
193
|
+
routine_type = row[1]
|
|
194
|
+
|
|
195
|
+
# Get complete CREATE statement using SHOW CREATE
|
|
196
|
+
create_query = if routine_type == 'PROCEDURE'
|
|
197
|
+
"SHOW CREATE PROCEDURE `#{routine_name}`"
|
|
198
|
+
else
|
|
199
|
+
"SHOW CREATE FUNCTION `#{routine_name}`"
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
create_result = connection.execute(create_query).first
|
|
203
|
+
# SHOW CREATE returns: [procedure_name, sql_mode, create_statement, ...]
|
|
204
|
+
create_statement = create_result[2] if create_result
|
|
205
|
+
|
|
206
|
+
{
|
|
207
|
+
schema: 'public',
|
|
208
|
+
name: routine_name,
|
|
209
|
+
definition: create_statement
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Fetch sequences (not supported in MySQL)
|
|
215
|
+
#
|
|
216
|
+
# @param _connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection (unused)
|
|
217
|
+
# @return [Array] Empty array as MySQL doesn't support sequences (uses AUTO_INCREMENT instead)
|
|
218
|
+
def fetch_sequences(_connection)
|
|
219
|
+
# MySQL doesn't have sequences (uses AUTO_INCREMENT)
|
|
220
|
+
[]
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
# Fetch all triggers from the current database
|
|
224
|
+
#
|
|
225
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
226
|
+
# @return [Array<Hash>] Array of trigger hashes with :table, :name, :event, :timing, :definition
|
|
227
|
+
def fetch_triggers(connection)
|
|
228
|
+
query = <<~SQL.squish
|
|
229
|
+
SELECT
|
|
230
|
+
TRIGGER_NAME,
|
|
231
|
+
EVENT_MANIPULATION,
|
|
232
|
+
EVENT_OBJECT_TABLE,
|
|
233
|
+
ACTION_TIMING
|
|
234
|
+
FROM information_schema.TRIGGERS
|
|
235
|
+
WHERE TRIGGER_SCHEMA = DATABASE()
|
|
236
|
+
ORDER BY EVENT_OBJECT_TABLE, TRIGGER_NAME
|
|
237
|
+
SQL
|
|
238
|
+
|
|
239
|
+
connection.execute(query).map do |row|
|
|
240
|
+
trigger_name = row[0]
|
|
241
|
+
|
|
242
|
+
# Get complete CREATE statement using SHOW CREATE
|
|
243
|
+
create_result = connection.execute("SHOW CREATE TRIGGER `#{trigger_name}`").first
|
|
244
|
+
# SHOW CREATE TRIGGER returns: [trigger_name, sql_mode, create_statement, ...]
|
|
245
|
+
create_statement = create_result[2] if create_result
|
|
246
|
+
|
|
247
|
+
{
|
|
248
|
+
table: row[2],
|
|
249
|
+
name: trigger_name,
|
|
250
|
+
event: row[1], # INSERT, UPDATE, DELETE
|
|
251
|
+
timing: row[3], # BEFORE, AFTER
|
|
252
|
+
definition: create_statement
|
|
253
|
+
}
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
# Capability methods - MySQL feature support
|
|
258
|
+
|
|
259
|
+
# Indicates whether MySQL supports extensions
|
|
260
|
+
#
|
|
261
|
+
# @return [Boolean] Always false for MySQL
|
|
262
|
+
def supports_extensions?
|
|
263
|
+
false
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
# Indicates whether MySQL supports materialized views
|
|
267
|
+
#
|
|
268
|
+
# @return [Boolean] Always false for MySQL
|
|
269
|
+
def supports_materialized_views?
|
|
270
|
+
false
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
# Indicates whether MySQL supports custom types
|
|
274
|
+
#
|
|
275
|
+
# @return [Boolean] Always false (ENUM/SET are inline with columns, not custom types)
|
|
276
|
+
def supports_custom_types?
|
|
277
|
+
false # ENUM/SET are inline with columns, not custom types
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Indicates whether MySQL supports domains
|
|
281
|
+
#
|
|
282
|
+
# @return [Boolean] Always false for MySQL
|
|
283
|
+
def supports_domains?
|
|
284
|
+
false
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Indicates whether MySQL supports stored procedures and functions
|
|
288
|
+
#
|
|
289
|
+
# @return [Boolean] Always true for MySQL
|
|
290
|
+
def supports_functions?
|
|
291
|
+
true # Stored procedures and functions
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
# Indicates whether MySQL supports triggers
|
|
295
|
+
#
|
|
296
|
+
# @return [Boolean] Always true for MySQL
|
|
297
|
+
def supports_triggers?
|
|
298
|
+
true
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
# Indicates whether MySQL supports sequences
|
|
302
|
+
#
|
|
303
|
+
# @return [Boolean] Always false (uses AUTO_INCREMENT instead)
|
|
304
|
+
def supports_sequences?
|
|
305
|
+
false # Uses AUTO_INCREMENT instead
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# Indicates whether MySQL supports check constraints
|
|
309
|
+
#
|
|
310
|
+
# @return [Boolean] True for MySQL 8.0.16+, false for earlier versions
|
|
311
|
+
def supports_check_constraints?
|
|
312
|
+
version_at_least?(database_version, '8.0.16')
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Version detection
|
|
316
|
+
|
|
317
|
+
# Get the current MySQL database version
|
|
318
|
+
#
|
|
319
|
+
# @return [String] Normalized version string (e.g., "8.0.35")
|
|
320
|
+
def database_version
|
|
321
|
+
@database_version ||= begin
|
|
322
|
+
version_string = connection.select_value('SELECT VERSION()')
|
|
323
|
+
parse_version(version_string)
|
|
324
|
+
end
|
|
325
|
+
end
|
|
326
|
+
|
|
327
|
+
# Parse MySQL version string into normalized format
|
|
328
|
+
#
|
|
329
|
+
# @param version_string [String] Raw version string from MySQL (e.g., "8.0.35" or "5.7.44-log")
|
|
330
|
+
# @return [String] Normalized version (e.g., "8.0.35") or "unknown" if parsing fails
|
|
331
|
+
def parse_version(version_string)
|
|
332
|
+
# Example: "8.0.35" or "5.7.44-log"
|
|
333
|
+
# Extract major.minor.patch version
|
|
334
|
+
match = version_string.match(/(\d+\.\d+\.\d+)/)
|
|
335
|
+
return 'unknown' unless match
|
|
336
|
+
|
|
337
|
+
match[1]
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
private
|
|
341
|
+
|
|
342
|
+
# Helper methods for introspection
|
|
343
|
+
|
|
344
|
+
# Fetch columns for a specific table
|
|
345
|
+
#
|
|
346
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
347
|
+
# @param table_name [String] Name of the table
|
|
348
|
+
# @return [Array<Hash>] Array of column hashes with :name, :type, :nullable, :default, etc.
|
|
349
|
+
def fetch_columns(connection, table_name)
|
|
350
|
+
query = <<~SQL.squish
|
|
351
|
+
SELECT
|
|
352
|
+
COLUMN_NAME,
|
|
353
|
+
DATA_TYPE,
|
|
354
|
+
IS_NULLABLE,
|
|
355
|
+
COLUMN_DEFAULT,
|
|
356
|
+
CHARACTER_MAXIMUM_LENGTH,
|
|
357
|
+
NUMERIC_PRECISION,
|
|
358
|
+
NUMERIC_SCALE,
|
|
359
|
+
COLUMN_TYPE,
|
|
360
|
+
EXTRA
|
|
361
|
+
FROM information_schema.COLUMNS
|
|
362
|
+
WHERE TABLE_NAME = #{connection.quote(table_name)}
|
|
363
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
364
|
+
ORDER BY ORDINAL_POSITION
|
|
365
|
+
SQL
|
|
366
|
+
|
|
367
|
+
connection.execute(query).map do |row|
|
|
368
|
+
{
|
|
369
|
+
name: row[0],
|
|
370
|
+
type: resolve_column_type(row),
|
|
371
|
+
nullable: row[2] == 'YES',
|
|
372
|
+
default: row[3],
|
|
373
|
+
length: row[4],
|
|
374
|
+
precision: row[5],
|
|
375
|
+
scale: row[6],
|
|
376
|
+
column_type: row[7], # Full type with ENUM values, etc.
|
|
377
|
+
extra: row[8] # AUTO_INCREMENT, etc.
|
|
378
|
+
}
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Fetch primary key columns for a specific table
|
|
383
|
+
#
|
|
384
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
385
|
+
# @param table_name [String] Name of the table
|
|
386
|
+
# @return [Array<String>] Array of primary key column names
|
|
387
|
+
def fetch_primary_key(connection, table_name)
|
|
388
|
+
query = <<~SQL.squish
|
|
389
|
+
SELECT COLUMN_NAME
|
|
390
|
+
FROM information_schema.KEY_COLUMN_USAGE
|
|
391
|
+
WHERE TABLE_NAME = #{connection.quote(table_name)}
|
|
392
|
+
AND TABLE_SCHEMA = DATABASE()
|
|
393
|
+
AND CONSTRAINT_NAME = 'PRIMARY'
|
|
394
|
+
ORDER BY ORDINAL_POSITION
|
|
395
|
+
SQL
|
|
396
|
+
|
|
397
|
+
connection.execute(query).pluck(0)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
# Fetch check constraints for a specific table (MySQL 8.0.16+)
|
|
401
|
+
#
|
|
402
|
+
# @param connection [ActiveRecord::ConnectionAdapters::AbstractAdapter] Database connection
|
|
403
|
+
# @param table_name [String] Name of the table
|
|
404
|
+
# @return [Array<Hash>] Array of constraint hashes with :name, :definition, :type
|
|
405
|
+
def fetch_constraints(connection, table_name)
|
|
406
|
+
# MySQL 8.0.16+ supports check constraints
|
|
407
|
+
return [] unless supports_check_constraints?
|
|
408
|
+
|
|
409
|
+
query = <<~SQL.squish
|
|
410
|
+
SELECT
|
|
411
|
+
CONSTRAINT_NAME,
|
|
412
|
+
CHECK_CLAUSE
|
|
413
|
+
FROM information_schema.CHECK_CONSTRAINTS
|
|
414
|
+
WHERE CONSTRAINT_SCHEMA = DATABASE()
|
|
415
|
+
AND TABLE_NAME = #{connection.quote(table_name)}
|
|
416
|
+
ORDER BY CONSTRAINT_NAME
|
|
417
|
+
SQL
|
|
418
|
+
|
|
419
|
+
connection.execute(query).map do |row|
|
|
420
|
+
{
|
|
421
|
+
name: row[0],
|
|
422
|
+
definition: row[1],
|
|
423
|
+
type: :check
|
|
424
|
+
}
|
|
425
|
+
end
|
|
426
|
+
rescue StandardError
|
|
427
|
+
# If CHECK_CONSTRAINTS table doesn't exist (MySQL < 8.0.16), return empty
|
|
428
|
+
[]
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
# Resolve MySQL column type into normalized format
|
|
432
|
+
#
|
|
433
|
+
# @param row [Array] Column information row from information_schema.COLUMNS
|
|
434
|
+
# @return [String] Normalized column type with length/precision if applicable
|
|
435
|
+
def resolve_column_type(row)
|
|
436
|
+
data_type = row[1]
|
|
437
|
+
column_type = row[7] # Full type definition
|
|
438
|
+
|
|
439
|
+
case data_type
|
|
440
|
+
when 'varchar', 'char'
|
|
441
|
+
# Use CHARACTER_MAXIMUM_LENGTH
|
|
442
|
+
length = row[4]
|
|
443
|
+
length ? "#{data_type}(#{length})" : data_type
|
|
444
|
+
when 'decimal', 'numeric'
|
|
445
|
+
precision = row[5]
|
|
446
|
+
scale = row[6]
|
|
447
|
+
if precision && scale
|
|
448
|
+
"#{data_type}(#{precision},#{scale})"
|
|
449
|
+
else
|
|
450
|
+
data_type
|
|
451
|
+
end
|
|
452
|
+
when 'enum', 'set'
|
|
453
|
+
# Return full column_type which includes values: enum('admin','user','guest')
|
|
454
|
+
column_type
|
|
455
|
+
when 'int', 'integer'
|
|
456
|
+
'int'
|
|
457
|
+
when 'bigint'
|
|
458
|
+
'bigint'
|
|
459
|
+
when 'tinyint'
|
|
460
|
+
# Check if it's boolean (tinyint(1))
|
|
461
|
+
column_type.include?('tinyint(1)') ? 'boolean' : 'tinyint'
|
|
462
|
+
when 'datetime', 'timestamp'
|
|
463
|
+
data_type
|
|
464
|
+
when 'text', 'mediumtext', 'longtext'
|
|
465
|
+
data_type
|
|
466
|
+
when 'blob', 'mediumblob', 'longblob'
|
|
467
|
+
data_type
|
|
468
|
+
when 'json'
|
|
469
|
+
'json'
|
|
470
|
+
else
|
|
471
|
+
data_type
|
|
472
|
+
end
|
|
473
|
+
end
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module BetterStructureSql
|
|
4
|
+
module Adapters
|
|
5
|
+
# MySQL-specific configuration settings
|
|
6
|
+
#
|
|
7
|
+
# Provides configuration options specific to MySQL database adapter.
|
|
8
|
+
# These settings control which MySQL features are included in schema dumps
|
|
9
|
+
# and how they are generated.
|
|
10
|
+
class MysqlConfig
|
|
11
|
+
# @return [Boolean] Whether to include stored procedures in schema dump
|
|
12
|
+
# @return [Boolean] Whether to include triggers in schema dump
|
|
13
|
+
# @return [Boolean] Whether to include views in schema dump
|
|
14
|
+
# @return [Boolean] Whether to use SHOW CREATE statements instead of information_schema
|
|
15
|
+
# @return [String] Default character set for MySQL (default: 'utf8mb4')
|
|
16
|
+
# @return [String] Default collation for MySQL (default: 'utf8mb4_unicode_ci')
|
|
17
|
+
# @return [String] Minimum MySQL version required (default: '8.0')
|
|
18
|
+
attr_accessor :include_stored_procedures, :include_triggers, :include_views, :use_show_create, :charset, :collation, :min_version
|
|
19
|
+
|
|
20
|
+
# Initialize MySQL configuration with default values
|
|
21
|
+
def initialize
|
|
22
|
+
@include_stored_procedures = true
|
|
23
|
+
@include_triggers = true
|
|
24
|
+
@include_views = true
|
|
25
|
+
@use_show_create = false # Use information_schema by default
|
|
26
|
+
@charset = 'utf8mb4'
|
|
27
|
+
@collation = 'utf8mb4_unicode_ci'
|
|
28
|
+
@min_version = '8.0'
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|