fast_schema_dumper 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/README.md +33 -0
- data/Rakefile +4 -0
- data/exe/fast_schema_dumper +4 -0
- data/lib/fast_schema_dumper/cli.rb +33 -0
- data/lib/fast_schema_dumper/fast_dumper.rb +464 -0
- data/lib/fast_schema_dumper/ridgepole.rb +39 -0
- data/lib/fast_schema_dumper/version.rb +3 -0
- data/lib/fast_schema_dumper.rb +5 -0
- metadata +64 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 6d00d757f9b258b9b438f8166e9eb8f20fffbdf85ff3c2ca0f139d3869569f46
|
4
|
+
data.tar.gz: e84597beb407fc62ac5a2befcc958de139e749ad450721dab80f85df1b98ed6c
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 3caede5f1ffbababb0bcdb98c6c12ca7fe2abaaed60abb9b9cc2faffb125e23303a16b733cd3e5c0049d434397ed13eedda4bd7e0b5266cd349514ff0832267a
|
7
|
+
data.tar.gz: 1fb86fe0fed6f402817d9716c7e2180da3618a31a5388888bf9b9762be10671c37046e0908cee3a3c4488d53a9787bced38aa24459948495aea7a1589c27d403
|
data/README.md
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# FastSchemaDumper
|
2
|
+
|
3
|
+
TODO: Delete this and the text below, and describe your gem
|
4
|
+
|
5
|
+
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/fast_schema_dumper`. To experiment with that code, run `bin/console` for an interactive prompt.
|
6
|
+
|
7
|
+
## Installation
|
8
|
+
|
9
|
+
Install the gem and add to the application's Gemfile by executing:
|
10
|
+
|
11
|
+
```bash
|
12
|
+
bundle add fast_schema_dumper
|
13
|
+
```
|
14
|
+
|
15
|
+
If bundler is not being used to manage dependencies, install the gem by executing:
|
16
|
+
|
17
|
+
```bash
|
18
|
+
gem install fast_schema_dumper
|
19
|
+
```
|
20
|
+
|
21
|
+
## Usage
|
22
|
+
|
23
|
+
TODO: Write usage instructions here
|
24
|
+
|
25
|
+
## Development
|
26
|
+
|
27
|
+
After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
28
|
+
|
29
|
+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
30
|
+
|
31
|
+
## Contributing
|
32
|
+
|
33
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/fast_schema_dumper.
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'erb'
|
2
|
+
require 'active_record'
|
3
|
+
require 'active_record/database_configurations'
|
4
|
+
|
5
|
+
require_relative './fast_dumper'
|
6
|
+
|
7
|
+
module FastSchemaDumper
|
8
|
+
class CLI
|
9
|
+
def self.run(...)
|
10
|
+
new.run(...)
|
11
|
+
end
|
12
|
+
|
13
|
+
def run(argv)
|
14
|
+
argv = argv.dup
|
15
|
+
|
16
|
+
env = ENV['RAILS_ENV'] || 'development'
|
17
|
+
|
18
|
+
database_yml_path = File.join(Dir.pwd, 'config', 'database.yml')
|
19
|
+
database_yml = Psych.safe_load(ERB.new(File.read(database_yml_path)).result, aliases: true)
|
20
|
+
config = database_yml[env]
|
21
|
+
# Override pool size to 1 for faster startup
|
22
|
+
config['pool'] = 1
|
23
|
+
|
24
|
+
# Prepare the ActiveRecord connection configuration
|
25
|
+
hash_config = ActiveRecord::DatabaseConfigurations::HashConfig.new(env, 'primary', config)
|
26
|
+
ActiveRecord::Base.establish_connection(hash_config)
|
27
|
+
|
28
|
+
SchemaDumper.dump
|
29
|
+
|
30
|
+
return 0
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,464 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module FastSchemaDumper
|
4
|
+
class SchemaDumper
|
5
|
+
class << self
|
6
|
+
def dump(pool = ActiveRecord::Base.connection_pool, stream = $stdout, config = ActiveRecord::Base)
|
7
|
+
new.dump(pool, stream, config)
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
def dump(pool = ActiveRecord::Base.connection_pool, stream = $stdout, config = ActiveRecord::Base)
|
12
|
+
conn = ActiveRecord::Base.connection
|
13
|
+
|
14
|
+
@output = []
|
15
|
+
|
16
|
+
# Get all tables (excluding ar_internal_metadata and schema_migrations)
|
17
|
+
tables = conn.exec_query("
|
18
|
+
SELECT TABLE_NAME
|
19
|
+
FROM INFORMATION_SCHEMA.TABLES
|
20
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
21
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
22
|
+
AND TABLE_NAME NOT IN ('ar_internal_metadata', 'schema_migrations')
|
23
|
+
ORDER BY TABLE_NAME
|
24
|
+
").map { |row| row['TABLE_NAME'] }
|
25
|
+
|
26
|
+
# Get all columns
|
27
|
+
columns_data = conn.exec_query("
|
28
|
+
select
|
29
|
+
TABLE_NAME
|
30
|
+
, COLUMN_NAME
|
31
|
+
, ORDINAL_POSITION
|
32
|
+
, COLUMN_DEFAULT
|
33
|
+
, IS_NULLABLE
|
34
|
+
, DATA_TYPE
|
35
|
+
, CHARACTER_MAXIMUM_LENGTH
|
36
|
+
, NUMERIC_PRECISION
|
37
|
+
, NUMERIC_SCALE
|
38
|
+
, COLUMN_TYPE
|
39
|
+
, EXTRA
|
40
|
+
, COLUMN_COMMENT
|
41
|
+
, DATETIME_PRECISION
|
42
|
+
, COLLATION_NAME
|
43
|
+
from INFORMATION_SCHEMA.COLUMNS
|
44
|
+
where
|
45
|
+
TABLE_SCHEMA = database()
|
46
|
+
order by TABLE_NAME, ORDINAL_POSITION
|
47
|
+
")
|
48
|
+
|
49
|
+
# Get all indexes
|
50
|
+
indexes_data = conn.exec_query("
|
51
|
+
SELECT
|
52
|
+
s.TABLE_NAME,
|
53
|
+
s.INDEX_NAME,
|
54
|
+
s.NON_UNIQUE,
|
55
|
+
s.COLUMN_NAME,
|
56
|
+
s.SEQ_IN_INDEX,
|
57
|
+
s.INDEX_COMMENT,
|
58
|
+
s.COLLATION
|
59
|
+
FROM INFORMATION_SCHEMA.STATISTICS s
|
60
|
+
WHERE s.TABLE_SCHEMA = DATABASE()
|
61
|
+
ORDER BY s.TABLE_NAME, s.INDEX_NAME, s.SEQ_IN_INDEX
|
62
|
+
")
|
63
|
+
|
64
|
+
# Aggregate table information
|
65
|
+
# Organize indexes by table
|
66
|
+
indexes_by_table = indexes_data.each_with_object({}) do |idx, hash|
|
67
|
+
hash[idx['TABLE_NAME']] ||= {}
|
68
|
+
hash[idx['TABLE_NAME']][idx['INDEX_NAME']] ||= {
|
69
|
+
columns: [],
|
70
|
+
unique: idx['NON_UNIQUE'] == 0,
|
71
|
+
# length
|
72
|
+
orders: {},
|
73
|
+
# opclass
|
74
|
+
# where
|
75
|
+
# using
|
76
|
+
# include
|
77
|
+
# nulls_not_distinct
|
78
|
+
# type
|
79
|
+
comment: idx['INDEX_COMMENT'],
|
80
|
+
# enabled
|
81
|
+
}
|
82
|
+
hash[idx['TABLE_NAME']][idx['INDEX_NAME']][:columns] << idx['COLUMN_NAME']
|
83
|
+
# Track descending order columns (COLLATION = 'D')
|
84
|
+
if idx['COLLATION'] == 'D'
|
85
|
+
hash[idx['TABLE_NAME']][idx['INDEX_NAME']][:orders][idx['COLUMN_NAME']] = :desc
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Get table options
|
90
|
+
table_options = conn.exec_query("
|
91
|
+
SELECT
|
92
|
+
TABLE_NAME,
|
93
|
+
TABLE_COLLATION,
|
94
|
+
TABLE_COMMENT
|
95
|
+
FROM INFORMATION_SCHEMA.TABLES
|
96
|
+
WHERE TABLE_SCHEMA = DATABASE()
|
97
|
+
AND TABLE_TYPE = 'BASE TABLE'
|
98
|
+
").each_with_object({}) do |row, hash|
|
99
|
+
hash[row['TABLE_NAME']] = {
|
100
|
+
collation: row['TABLE_COLLATION'],
|
101
|
+
comment: row['TABLE_COMMENT'],
|
102
|
+
}
|
103
|
+
end
|
104
|
+
|
105
|
+
# Get foreign keys
|
106
|
+
foreign_keys_data = conn.exec_query("
|
107
|
+
SELECT
|
108
|
+
kcu.TABLE_NAME,
|
109
|
+
kcu.CONSTRAINT_NAME,
|
110
|
+
kcu.COLUMN_NAME,
|
111
|
+
kcu.REFERENCED_TABLE_NAME,
|
112
|
+
kcu.REFERENCED_COLUMN_NAME,
|
113
|
+
rc.DELETE_RULE,
|
114
|
+
rc.UPDATE_RULE
|
115
|
+
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE kcu
|
116
|
+
JOIN INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS rc
|
117
|
+
ON kcu.CONSTRAINT_SCHEMA = rc.CONSTRAINT_SCHEMA
|
118
|
+
AND kcu.CONSTRAINT_NAME = rc.CONSTRAINT_NAME
|
119
|
+
WHERE kcu.TABLE_SCHEMA = DATABASE()
|
120
|
+
AND kcu.REFERENCED_TABLE_NAME IS NOT NULL
|
121
|
+
ORDER BY kcu.TABLE_NAME, kcu.CONSTRAINT_NAME
|
122
|
+
")
|
123
|
+
|
124
|
+
# Organize columns by table
|
125
|
+
columns_by_table = columns_data.each_with_object({}) do |col, hash|
|
126
|
+
hash[col['TABLE_NAME']] ||= []
|
127
|
+
hash[col['TABLE_NAME']] << col
|
128
|
+
end
|
129
|
+
|
130
|
+
# Generate schema for each table
|
131
|
+
tables.each do |table_name|
|
132
|
+
dump_table(
|
133
|
+
table_name,
|
134
|
+
columns: columns_by_table[table_name] || [],
|
135
|
+
indexes: indexes_by_table[table_name] || {},
|
136
|
+
options: table_options[table_name]
|
137
|
+
)
|
138
|
+
@output << ""
|
139
|
+
end
|
140
|
+
|
141
|
+
# Remove trailing empty line
|
142
|
+
@output.pop if @output.last == ""
|
143
|
+
|
144
|
+
@output << ""
|
145
|
+
|
146
|
+
# Foreign keys
|
147
|
+
# ordered by table_name and constraint_name
|
148
|
+
|
149
|
+
foreign_keys_by_table = foreign_keys_data.each_with_object({}) do |fk, hash|
|
150
|
+
hash[fk['TABLE_NAME']] ||= {}
|
151
|
+
hash[fk['TABLE_NAME']][fk['CONSTRAINT_NAME']] ||= {
|
152
|
+
column: fk['COLUMN_NAME'],
|
153
|
+
referenced_table: fk['REFERENCED_TABLE_NAME'],
|
154
|
+
referenced_column: fk['REFERENCED_COLUMN_NAME'],
|
155
|
+
constraint_name: fk['CONSTRAINT_NAME'],
|
156
|
+
}
|
157
|
+
end
|
158
|
+
|
159
|
+
all_foreign_keys = []
|
160
|
+
foreign_keys_by_table.each do |table_name, foreign_keys|
|
161
|
+
foreign_keys.each do |constraint_name, fk_data|
|
162
|
+
all_foreign_keys << {
|
163
|
+
table_name: table_name,
|
164
|
+
constraint_name: constraint_name,
|
165
|
+
fk_data: fk_data,
|
166
|
+
}
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
# Sort by table_name first, then by referenced_table name, then by column name
|
171
|
+
all_foreign_keys.sort_by { |fk| [fk[:table_name], fk[:fk_data][:referenced_table], fk[:fk_data][:column]] }.each do |fk|
|
172
|
+
fk_line = "add_foreign_key \"#{fk[:table_name]}\", \"#{fk[:fk_data][:referenced_table]}\""
|
173
|
+
|
174
|
+
# Determine if we need column: or name: option
|
175
|
+
# Rails tries to infer the column name from the table name
|
176
|
+
# For simple cases: "users" -> "user_id"
|
177
|
+
# But it also handles more complex cases
|
178
|
+
|
179
|
+
inferred_column = "#{singularize(fk[:fk_data][:referenced_table])}_id"
|
180
|
+
|
181
|
+
# Check if column name matches what Rails would infer
|
182
|
+
if fk[:fk_data][:column] != inferred_column
|
183
|
+
# Column name is custom, need to specify it
|
184
|
+
fk_line += ", column: \"#{fk[:fk_data][:column]}\""
|
185
|
+
else
|
186
|
+
# Column matches default, check if constraint name is custom
|
187
|
+
# Rails generates constraint names starting with "fk_rails_"
|
188
|
+
if !fk[:fk_data][:constraint_name].start_with?("fk_rails_")
|
189
|
+
fk_line += ", name: \"#{fk[:fk_data][:constraint_name]}\""
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
@output << fk_line
|
194
|
+
end
|
195
|
+
|
196
|
+
stream.print @output.join("\n")
|
197
|
+
end
|
198
|
+
|
199
|
+
private
|
200
|
+
|
201
|
+
def escape_string(str)
|
202
|
+
str.gsub("\\", "\\\\\\\\").gsub('"', '\"').gsub("\n", "\\n").gsub("\r", "\\r").gsub("\t", "\\t")
|
203
|
+
end
|
204
|
+
|
205
|
+
def singularize(str)
|
206
|
+
# Simple singularization rules
|
207
|
+
case str
|
208
|
+
when 'news'
|
209
|
+
'news' # news is both singular and plural
|
210
|
+
when /ies$/
|
211
|
+
str.sub(/ies$/, 'y')
|
212
|
+
when /ses$/
|
213
|
+
str.sub(/es$/, '')
|
214
|
+
when /s$/
|
215
|
+
str.sub(/s$/, '')
|
216
|
+
else
|
217
|
+
str
|
218
|
+
end
|
219
|
+
end
|
220
|
+
|
221
|
+
def dump_table(table_name, columns:, indexes:, options:)
|
222
|
+
table_def = "create_table \"#{table_name}\""
|
223
|
+
|
224
|
+
# id (primary key)
|
225
|
+
primary_key = indexes.delete('PRIMARY')
|
226
|
+
if primary_key && primary_key[:columns].size == 1 && primary_key[:columns].first == 'id'
|
227
|
+
id_column = columns.find { |c| c['COLUMN_NAME'] == 'id' }
|
228
|
+
if id_column
|
229
|
+
id_options = []
|
230
|
+
|
231
|
+
needs_id_options = false
|
232
|
+
|
233
|
+
# type
|
234
|
+
if id_column['DATA_TYPE'] != 'bigint'
|
235
|
+
id_options << "type: :#{id_column['DATA_TYPE']}"
|
236
|
+
needs_id_options = true
|
237
|
+
end
|
238
|
+
|
239
|
+
# comment
|
240
|
+
if id_column['COLUMN_COMMENT'] && !id_column['COLUMN_COMMENT'].empty?
|
241
|
+
id_options << "comment: \"#{escape_string(id_column['COLUMN_COMMENT'])}\""
|
242
|
+
needs_id_options = true
|
243
|
+
end
|
244
|
+
|
245
|
+
# unsigned
|
246
|
+
if id_column['COLUMN_TYPE'].include?('unsigned')
|
247
|
+
id_options << "unsigned: true"
|
248
|
+
needs_id_options = true
|
249
|
+
end
|
250
|
+
|
251
|
+
# type
|
252
|
+
if needs_id_options && id_column['DATA_TYPE'] == 'bigint'
|
253
|
+
id_options.unshift("type: :bigint")
|
254
|
+
end
|
255
|
+
|
256
|
+
table_def += ", id: { #{id_options.join(', ')} }" if needs_id_options
|
257
|
+
end
|
258
|
+
elsif primary_key.nil? || (primary_key && primary_key[:columns].first != 'id')
|
259
|
+
table_def += ", id: false"
|
260
|
+
end
|
261
|
+
|
262
|
+
# charset, collation
|
263
|
+
if options && options[:collation]
|
264
|
+
charset = options[:collation].split('_').first
|
265
|
+
table_def += ", charset: \"#{charset}\""
|
266
|
+
table_def += ", collation: \"#{options[:collation]}\""
|
267
|
+
end
|
268
|
+
|
269
|
+
# comment
|
270
|
+
if options && options[:comment] && !options[:comment].empty?
|
271
|
+
table_def += ", comment: \"#{escape_string(options[:comment])}\""
|
272
|
+
end
|
273
|
+
|
274
|
+
table_def += ", force: :cascade do |t|"
|
275
|
+
@output << table_def
|
276
|
+
|
277
|
+
# columns
|
278
|
+
columns.reject { |c| c['COLUMN_NAME'] == 'id' }.each do |column|
|
279
|
+
@output << " #{format_column(column)}"
|
280
|
+
end
|
281
|
+
|
282
|
+
# Indexes
|
283
|
+
# Rails orders indexes lexicographically by their column arrays
|
284
|
+
# Example: ["a", "b"] < ["a"] < ["b", "c"] < ["b"] < ["d"]
|
285
|
+
sorted_indexes = indexes.reject { |name, _| name == 'PRIMARY' }.sort_by do |index_name, index_data|
|
286
|
+
# Create an array padded with high values for comparison
|
287
|
+
# This ensures that missing columns sort after existing ones
|
288
|
+
max_cols = indexes.values.map { |data| data[:columns].size }.max || 1
|
289
|
+
cols = index_data[:columns].dup
|
290
|
+
# Pad with a string that sorts after any real column name
|
291
|
+
cols += ["\xFF" * 100] * (max_cols - cols.size)
|
292
|
+
cols
|
293
|
+
end
|
294
|
+
|
295
|
+
sorted_indexes.each do |index_name, index_data|
|
296
|
+
@output << " #{format_index(index_name, index_data)}"
|
297
|
+
end
|
298
|
+
|
299
|
+
@output << "end"
|
300
|
+
end
|
301
|
+
|
302
|
+
def format_column(column)
|
303
|
+
col_def = "t.#{map_column_type(column)} \"#{column['COLUMN_NAME']}\""
|
304
|
+
|
305
|
+
# limit (varchar, char)
|
306
|
+
if ['varchar', 'char'].include?(column['DATA_TYPE']) && column['CHARACTER_MAXIMUM_LENGTH'] &&
|
307
|
+
column['CHARACTER_MAXIMUM_LENGTH'] != 255
|
308
|
+
col_def += ", limit: #{column['CHARACTER_MAXIMUM_LENGTH']}"
|
309
|
+
end
|
310
|
+
|
311
|
+
# limit (integers)
|
312
|
+
case column['DATA_TYPE']
|
313
|
+
when 'tinyint'
|
314
|
+
# Always add limit: 1 for tinyint unless it's tinyint(1) which is boolean
|
315
|
+
col_def += ", limit: 1" unless column['COLUMN_TYPE'] == 'tinyint(1)'
|
316
|
+
when 'smallint'
|
317
|
+
col_def += ", limit: 2"
|
318
|
+
when 'mediumint'
|
319
|
+
col_def += ", limit: 3"
|
320
|
+
end
|
321
|
+
|
322
|
+
# size (text)
|
323
|
+
if column['DATA_TYPE'] == 'mediumtext'
|
324
|
+
col_def += ", size: :medium"
|
325
|
+
elsif column['DATA_TYPE'] == 'longtext'
|
326
|
+
col_def += ", size: :long"
|
327
|
+
end
|
328
|
+
|
329
|
+
# precision (datetime)
|
330
|
+
if column['DATA_TYPE'] == 'datetime' && column['DATETIME_PRECISION']
|
331
|
+
precision = column['DATETIME_PRECISION'].to_i
|
332
|
+
col_def += ", precision: nil" if precision == 0
|
333
|
+
end
|
334
|
+
|
335
|
+
# precision, scale (decimal)
|
336
|
+
if column['DATA_TYPE'] == 'decimal' && column['NUMERIC_PRECISION']
|
337
|
+
col_def += ", precision: #{column['NUMERIC_PRECISION']}"
|
338
|
+
col_def += ", scale: #{column['NUMERIC_SCALE']}" if column['NUMERIC_SCALE']
|
339
|
+
end
|
340
|
+
|
341
|
+
# default
|
342
|
+
if column['COLUMN_DEFAULT']
|
343
|
+
default = format_default_value(column['COLUMN_DEFAULT'], column['DATA_TYPE'], column['COLUMN_TYPE'])
|
344
|
+
col_def += ", default: #{default}" unless default.nil?
|
345
|
+
end
|
346
|
+
|
347
|
+
# null
|
348
|
+
col_def += ", null: false" if column['IS_NULLABLE'] == 'NO'
|
349
|
+
|
350
|
+
# comment
|
351
|
+
if column['COLUMN_COMMENT'] && !column['COLUMN_COMMENT'].empty?
|
352
|
+
col_def += ", comment: \"#{escape_string(column['COLUMN_COMMENT'])}\""
|
353
|
+
end
|
354
|
+
|
355
|
+
# unsigned
|
356
|
+
if column['COLUMN_TYPE'].include?('unsigned')
|
357
|
+
col_def += ", unsigned: true"
|
358
|
+
end
|
359
|
+
|
360
|
+
# collation
|
361
|
+
if column['COLLATION_NAME'] && column['DATA_TYPE'] =~ /char|text/
|
362
|
+
# Check if it's different from the table's default collation
|
363
|
+
# For now, just check if it's utf8mb4_bin which seems to be the special case
|
364
|
+
if column['COLLATION_NAME'] == 'utf8mb4_bin'
|
365
|
+
col_def += ", collation: \"#{column['COLLATION_NAME']}\""
|
366
|
+
end
|
367
|
+
end
|
368
|
+
|
369
|
+
col_def
|
370
|
+
end
|
371
|
+
|
372
|
+
def map_column_type(column)
|
373
|
+
# Check for boolean (tinyint(1))
|
374
|
+
if column['COLUMN_TYPE'] == 'tinyint(1)'
|
375
|
+
return 'boolean'
|
376
|
+
end
|
377
|
+
|
378
|
+
case column['DATA_TYPE']
|
379
|
+
when 'varchar', 'char'
|
380
|
+
'string'
|
381
|
+
when 'int', 'tinyint', 'smallint', 'mediumint'
|
382
|
+
'integer'
|
383
|
+
when 'bigint'
|
384
|
+
'bigint'
|
385
|
+
when 'text', 'tinytext', 'mediumtext', 'longtext'
|
386
|
+
'text'
|
387
|
+
when 'datetime', 'timestamp'
|
388
|
+
'datetime'
|
389
|
+
when 'date'
|
390
|
+
'date'
|
391
|
+
when 'time'
|
392
|
+
'time'
|
393
|
+
when 'decimal'
|
394
|
+
'decimal'
|
395
|
+
when 'float', 'double'
|
396
|
+
'float'
|
397
|
+
when 'json'
|
398
|
+
'json'
|
399
|
+
when 'binary', 'varbinary'
|
400
|
+
'binary'
|
401
|
+
when 'blob', 'tinyblob', 'mediumblob', 'longblob'
|
402
|
+
'binary'
|
403
|
+
else
|
404
|
+
column['DATA_TYPE']
|
405
|
+
end
|
406
|
+
end
|
407
|
+
|
408
|
+
def format_default_value(default, data_type, column_type = nil)
|
409
|
+
return nil if default == 'NULL' || default.nil?
|
410
|
+
|
411
|
+
# Special handling for boolean (tinyint(1))
|
412
|
+
if column_type == 'tinyint(1)'
|
413
|
+
return default == '1' ? 'true' : 'false'
|
414
|
+
end
|
415
|
+
|
416
|
+
case data_type
|
417
|
+
when 'varchar', 'char', 'text'
|
418
|
+
"\"#{escape_string(default)}\""
|
419
|
+
when 'int', 'tinyint', 'smallint', 'mediumint', 'bigint'
|
420
|
+
default
|
421
|
+
when 'datetime', 'timestamp'
|
422
|
+
return '-> { "CURRENT_TIMESTAMP" }' if default == 'CURRENT_TIMESTAMP'
|
423
|
+
"\"#{default}\""
|
424
|
+
when 'json'
|
425
|
+
default == "'[]'" ? '[]' : '{}'
|
426
|
+
else
|
427
|
+
default =~ /^'.*'$/ ? "\"#{default[1..-2]}\"" : default
|
428
|
+
end
|
429
|
+
end
|
430
|
+
|
431
|
+
def format_index(index_name, index_data)
|
432
|
+
idx_def = "t.index "
|
433
|
+
|
434
|
+
if index_data[:columns].size == 1
|
435
|
+
idx_def += "[\"#{index_data[:columns].first}\"]"
|
436
|
+
else
|
437
|
+
idx_def += "[#{index_data[:columns].map { |c| "\"#{c}\"" }.join(', ')}]"
|
438
|
+
end
|
439
|
+
|
440
|
+
idx_def += ", name: \"#{index_name}\""
|
441
|
+
idx_def += ", unique: true" if index_data[:unique]
|
442
|
+
|
443
|
+
# order
|
444
|
+
if index_data[:orders] && !index_data[:orders].empty?
|
445
|
+
order_hash = index_data[:columns].each_with_object({}) do |col, hash|
|
446
|
+
if index_data[:orders][col]
|
447
|
+
hash[col.to_sym] = index_data[:orders][col]
|
448
|
+
end
|
449
|
+
end
|
450
|
+
|
451
|
+
unless order_hash.empty?
|
452
|
+
idx_def += ", order: { #{order_hash.map { |k, v| "#{k}: :#{v}" }.join(', ')} }"
|
453
|
+
end
|
454
|
+
end
|
455
|
+
|
456
|
+
# comment
|
457
|
+
if index_data[:comment] && !index_data[:comment].empty?
|
458
|
+
idx_def += ", comment: \"#{escape_string(index_data[:comment])}\""
|
459
|
+
end
|
460
|
+
|
461
|
+
idx_def
|
462
|
+
end
|
463
|
+
end
|
464
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative './fast_dumper'
|
2
|
+
|
3
|
+
# Loading this file will overwrite `Ridgepole::Dumper.dump`.
|
4
|
+
|
5
|
+
# This file must be loaded after the ridgepole gem is loaded.
|
6
|
+
raise "Ridgepole is not defined. Require ridgepole before loading this file." if !defined?(Ridgepole)
|
7
|
+
|
8
|
+
module Ridgepole
|
9
|
+
class Dumper
|
10
|
+
alias_method :original_dump, :dump
|
11
|
+
|
12
|
+
def dump
|
13
|
+
case ENV['FAST_SCHEMA_DUMPER_MODE']
|
14
|
+
in 'disabled'
|
15
|
+
original_dump
|
16
|
+
in 'verify'
|
17
|
+
puts "Warning: fast_schema_dumper is enabled in verify mode" unless ENV['FAST_SCHEMA_DUMPER_SUPPRESS_MESSAGE'] == '1'
|
18
|
+
original_results = original_dump
|
19
|
+
fast_results = fast_dump
|
20
|
+
if original_results != fast_results
|
21
|
+
File.write("orig.txt", original_results)
|
22
|
+
File.write("fast.txt", fast_results)
|
23
|
+
raise "Dumped schema do not match between ActiveRecord::SchemaDumper and fast_schema_dumper. This is a fast_schema_dumper bug."
|
24
|
+
end
|
25
|
+
fast_results
|
26
|
+
else
|
27
|
+
puts "Warning: fast_schema_dumper is enabled" unless ENV['FAST_SCHEMA_DUMPER_SUPPRESS_MESSAGE'] == '1'
|
28
|
+
fast_dump
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def fast_dump
|
33
|
+
s = StringIO.new
|
34
|
+
FastSchemaDumper::SchemaDumper.dump(ActiveRecord::Base.connection_pool, s)
|
35
|
+
s.rewind
|
36
|
+
s.read
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
metadata
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fast_schema_dumper
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daisuke Aritomo
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: activerecord
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - ">="
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - ">="
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0'
|
26
|
+
email:
|
27
|
+
- osyoyu@osyoyu.com
|
28
|
+
executables:
|
29
|
+
- fast_schema_dumper
|
30
|
+
extensions: []
|
31
|
+
extra_rdoc_files: []
|
32
|
+
files:
|
33
|
+
- README.md
|
34
|
+
- Rakefile
|
35
|
+
- exe/fast_schema_dumper
|
36
|
+
- lib/fast_schema_dumper.rb
|
37
|
+
- lib/fast_schema_dumper/cli.rb
|
38
|
+
- lib/fast_schema_dumper/fast_dumper.rb
|
39
|
+
- lib/fast_schema_dumper/ridgepole.rb
|
40
|
+
- lib/fast_schema_dumper/version.rb
|
41
|
+
homepage: https://github.com/osyoyu/fast_schema_dumper
|
42
|
+
licenses: []
|
43
|
+
metadata:
|
44
|
+
allowed_push_host: https://rubygems.org
|
45
|
+
homepage_uri: https://github.com/osyoyu/fast_schema_dumper
|
46
|
+
source_code_uri: https://github.com/osyoyu/fast_schema_dumper
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: 3.2.0
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.7.0.dev
|
62
|
+
specification_version: 4
|
63
|
+
summary: A fast alternative to ActiveRecord::SchemaDumper
|
64
|
+
test_files: []
|