migsupo 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 +246 -0
- data/lib/migsupo/configuration.rb +19 -0
- data/lib/migsupo/differ/diff.rb +22 -0
- data/lib/migsupo/differ/diff_calculator.rb +123 -0
- data/lib/migsupo/differ/operations/add_column.rb +27 -0
- data/lib/migsupo/differ/operations/add_index.rb +27 -0
- data/lib/migsupo/differ/operations/change_column.rb +33 -0
- data/lib/migsupo/differ/operations/create_table.rb +30 -0
- data/lib/migsupo/differ/operations/drop_table.rb +30 -0
- data/lib/migsupo/differ/operations/remove_column.rb +31 -0
- data/lib/migsupo/differ/operations/remove_index.rb +27 -0
- data/lib/migsupo/differ/operations/rename_column.rb +28 -0
- data/lib/migsupo/generator/migration_builder.rb +185 -0
- data/lib/migsupo/generator/migration_generator.rb +65 -0
- data/lib/migsupo/generator/naming.rb +45 -0
- data/lib/migsupo/loader/active_record_loader.rb +97 -0
- data/lib/migsupo/loader/schema_rb_loader.rb +24 -0
- data/lib/migsupo/parser/dsl_context.rb +49 -0
- data/lib/migsupo/parser/schemafile_parser.rb +18 -0
- data/lib/migsupo/parser/table_dsl_context.rb +92 -0
- data/lib/migsupo/railtie.rb +11 -0
- data/lib/migsupo/schema/column_definition.rb +42 -0
- data/lib/migsupo/schema/index_definition.rb +44 -0
- data/lib/migsupo/schema/schema_definition.rb +20 -0
- data/lib/migsupo/schema/table_definition.rb +31 -0
- data/lib/migsupo/tasks/migsupo.rake +56 -0
- data/lib/migsupo/version.rb +3 -0
- data/lib/migsupo.rb +75 -0
- data/migsupo.gemspec +23 -0
- metadata +156 -0
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
require_relative "../differ/operations/create_table"
|
|
2
|
+
require_relative "../differ/operations/drop_table"
|
|
3
|
+
require_relative "../differ/operations/add_column"
|
|
4
|
+
require_relative "../differ/operations/remove_column"
|
|
5
|
+
require_relative "../differ/operations/change_column"
|
|
6
|
+
require_relative "../differ/operations/rename_column"
|
|
7
|
+
require_relative "../differ/operations/add_index"
|
|
8
|
+
require_relative "../differ/operations/remove_index"
|
|
9
|
+
|
|
10
|
+
module Migsupo
|
|
11
|
+
module Generator
|
|
12
|
+
class MigrationBuilder
|
|
13
|
+
def initialize(rails_version: nil)
|
|
14
|
+
@rails_version = rails_version
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def build(operations, class_name:)
|
|
18
|
+
all_reversible = operations.all?(&:reversible?)
|
|
19
|
+
|
|
20
|
+
body = if all_reversible
|
|
21
|
+
indent("def change\n#{indent(render_operations(operations))}\nend", 2)
|
|
22
|
+
else
|
|
23
|
+
up = indent("def up\n#{indent(render_operations(operations, direction: :up))}\nend", 2)
|
|
24
|
+
down = indent("def down\n#{indent(render_operations(operations, direction: :down))}\nend", 2)
|
|
25
|
+
"#{up}\n\n#{down}"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
<<~RUBY
|
|
29
|
+
class #{class_name} < ActiveRecord::Migration#{version_suffix}
|
|
30
|
+
#{body}
|
|
31
|
+
end
|
|
32
|
+
RUBY
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def version_suffix
|
|
38
|
+
return "" unless @rails_version
|
|
39
|
+
|
|
40
|
+
"[#{@rails_version}]"
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def render_operations(operations, direction: :change)
|
|
44
|
+
operations.map { |op| render_operation(op, direction: direction) }.join("\n")
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def render_operation(op, direction:)
|
|
48
|
+
case op
|
|
49
|
+
when Differ::Operations::CreateTable
|
|
50
|
+
render_create_table(op)
|
|
51
|
+
when Differ::Operations::DropTable
|
|
52
|
+
render_drop_table(op, direction: direction)
|
|
53
|
+
when Differ::Operations::AddColumn
|
|
54
|
+
direction == :down ? render_remove_column_from_add(op) : render_add_column(op)
|
|
55
|
+
when Differ::Operations::RemoveColumn
|
|
56
|
+
direction == :down ? render_add_column_from_remove(op) : render_remove_column(op)
|
|
57
|
+
when Differ::Operations::ChangeColumn
|
|
58
|
+
direction == :down ? render_change_column(op, column: op.old_column) : render_change_column(op, column: op.new_column)
|
|
59
|
+
when Differ::Operations::RenameColumn
|
|
60
|
+
direction == :down ? render_rename_column(op, reverse: true) : render_rename_column(op)
|
|
61
|
+
when Differ::Operations::AddIndex
|
|
62
|
+
direction == :down ? render_remove_index(op.table_name, op.index) : render_add_index(op.table_name, op.index)
|
|
63
|
+
when Differ::Operations::RemoveIndex
|
|
64
|
+
direction == :down ? render_add_index(op.table_name, op.index) : render_remove_index(op.table_name, op.index)
|
|
65
|
+
else
|
|
66
|
+
"# Unknown operation: #{op.class}"
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def render_create_table(op)
|
|
71
|
+
table = op.table
|
|
72
|
+
opts = table_options_str(table.options.reject { |k, _| k == :force })
|
|
73
|
+
columns = table.columns.map { |col| " #{render_column_in_table(col)}" }.join("\n")
|
|
74
|
+
|
|
75
|
+
idx_lines = table.indexes.map { |idx| render_add_index(table.name, idx) }.join("\n")
|
|
76
|
+
|
|
77
|
+
lines = ["create_table #{table.name.inspect}#{opts} do |t|"]
|
|
78
|
+
lines << collapse_timestamps(table.columns, columns)
|
|
79
|
+
lines << "end"
|
|
80
|
+
lines << idx_lines unless idx_lines.empty?
|
|
81
|
+
lines.join("\n")
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def collapse_timestamps(columns, rendered)
|
|
85
|
+
col_names = columns.map(&:name)
|
|
86
|
+
if col_names.include?("created_at") && col_names.include?("updated_at")
|
|
87
|
+
created = columns.find { |c| c.name == "created_at" }
|
|
88
|
+
updated = columns.find { |c| c.name == "updated_at" }
|
|
89
|
+
if created.type == :datetime && updated.type == :datetime &&
|
|
90
|
+
created.comparable_options.empty? && updated.comparable_options.empty?
|
|
91
|
+
rendered
|
|
92
|
+
.gsub(/\s*t\.datetime :created_at[^\n]*\n/, "")
|
|
93
|
+
.gsub(/\s*t\.datetime :updated_at[^\n]*/, "")
|
|
94
|
+
.sub(/\n*$/, "\n\n t.timestamps")
|
|
95
|
+
else
|
|
96
|
+
rendered
|
|
97
|
+
end
|
|
98
|
+
else
|
|
99
|
+
rendered
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def render_drop_table(op, direction:)
|
|
104
|
+
if direction == :down
|
|
105
|
+
render_create_table(Operations::CreateTable.new(op.table))
|
|
106
|
+
else
|
|
107
|
+
"drop_table #{op.table_name.inspect}"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def render_add_column(op)
|
|
112
|
+
col = op.column
|
|
113
|
+
opts = column_options_str(col.options)
|
|
114
|
+
"add_column #{op.table_name.inspect}, #{col.name.inspect}, :#{col.type}#{opts}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def render_remove_column(op)
|
|
118
|
+
col = op.column
|
|
119
|
+
opts = column_options_str(col.options.merge(type: col.type))
|
|
120
|
+
"remove_column #{op.table_name.inspect}, #{col.name.inspect}#{opts}"
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def render_remove_column_from_add(op)
|
|
124
|
+
"remove_column #{op.table_name.inspect}, #{op.column.name.inspect}"
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def render_add_column_from_remove(op)
|
|
128
|
+
col = op.column
|
|
129
|
+
opts = column_options_str(col.options)
|
|
130
|
+
"add_column #{op.table_name.inspect}, #{col.name.inspect}, :#{col.type}#{opts}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def render_change_column(op, column:)
|
|
134
|
+
opts = column_options_str(column.options)
|
|
135
|
+
"change_column #{op.table_name.inspect}, #{column.name.inspect}, :#{column.type}#{opts}"
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def render_rename_column(op, reverse: false)
|
|
139
|
+
old_name = reverse ? op.new_name : op.old_name
|
|
140
|
+
new_name = reverse ? op.old_name : op.new_name
|
|
141
|
+
"rename_column #{op.table_name.inspect}, #{old_name.inspect}, #{new_name.inspect}"
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def render_add_index(table_name, idx)
|
|
145
|
+
cols = idx.columns.map(&:inspect).join(", ")
|
|
146
|
+
opts = index_options_str(idx)
|
|
147
|
+
"add_index #{table_name.inspect}, [#{cols}]#{opts}"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def render_remove_index(table_name, idx)
|
|
151
|
+
"remove_index #{table_name.inspect}, name: #{idx.name.inspect}"
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def render_column_in_table(col)
|
|
155
|
+
opts = column_options_str(col.options)
|
|
156
|
+
"t.#{col.type} #{col.name.inspect}#{opts}"
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def column_options_str(opts)
|
|
160
|
+
return "" if opts.nil? || opts.empty?
|
|
161
|
+
|
|
162
|
+
pairs = opts.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
163
|
+
", #{pairs}"
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def table_options_str(opts)
|
|
167
|
+
return "" if opts.nil? || opts.empty?
|
|
168
|
+
|
|
169
|
+
pairs = opts.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
170
|
+
", #{pairs}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def index_options_str(idx)
|
|
174
|
+
opts = idx.comparable_options
|
|
175
|
+
opts[:name] = idx.name
|
|
176
|
+
pairs = opts.map { |k, v| "#{k}: #{v.inspect}" }.join(", ")
|
|
177
|
+
", #{pairs}"
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def indent(str, spaces = 2)
|
|
181
|
+
str.lines.map { |line| line.chomp.empty? ? line : (" " * (spaces / 2)) + line }.join
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
require_relative "naming"
|
|
2
|
+
require_relative "migration_builder"
|
|
3
|
+
require_relative "../differ/operations/create_table"
|
|
4
|
+
require_relative "../differ/operations/drop_table"
|
|
5
|
+
|
|
6
|
+
module Migsupo
|
|
7
|
+
module Generator
|
|
8
|
+
class MigrationGenerator
|
|
9
|
+
def initialize(rails_version: nil)
|
|
10
|
+
@rails_version = rails_version
|
|
11
|
+
@builder = MigrationBuilder.new(rails_version: rails_version)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def generate(diff, output_dir:, dry_run: false)
|
|
15
|
+
return [] if diff.empty?
|
|
16
|
+
|
|
17
|
+
base_ts = base_timestamp
|
|
18
|
+
groups = group_operations(diff.operations)
|
|
19
|
+
files = []
|
|
20
|
+
|
|
21
|
+
groups.each_with_index do |ops, i|
|
|
22
|
+
class_name = Naming.class_name(ops)
|
|
23
|
+
timestamp = (base_ts + i).to_s
|
|
24
|
+
file_name = Naming.file_name(timestamp: timestamp, class_name: class_name)
|
|
25
|
+
content = @builder.build(ops, class_name: class_name)
|
|
26
|
+
|
|
27
|
+
if dry_run
|
|
28
|
+
puts "# #{file_name}"
|
|
29
|
+
puts content
|
|
30
|
+
puts
|
|
31
|
+
else
|
|
32
|
+
file_path = File.join(output_dir, file_name)
|
|
33
|
+
File.write(file_path, content)
|
|
34
|
+
files << file_path
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
files
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def group_operations(operations)
|
|
44
|
+
groups = []
|
|
45
|
+
|
|
46
|
+
create_ops = operations.select { |op| op.is_a?(Differ::Operations::CreateTable) }
|
|
47
|
+
drop_ops = operations.select { |op| op.is_a?(Differ::Operations::DropTable) }
|
|
48
|
+
other_ops = operations.reject { |op| op.is_a?(Differ::Operations::CreateTable) || op.is_a?(Differ::Operations::DropTable) }
|
|
49
|
+
|
|
50
|
+
create_ops.each { |op| groups << [op] }
|
|
51
|
+
drop_ops.each { |op| groups << [op] }
|
|
52
|
+
|
|
53
|
+
other_ops.group_by(&:table_name).each_value do |ops|
|
|
54
|
+
groups << ops
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
groups
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def base_timestamp
|
|
61
|
+
Time.now.strftime("%Y%m%d%H%M%S").to_i
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
module Migsupo
|
|
2
|
+
module Generator
|
|
3
|
+
module Naming
|
|
4
|
+
module_function
|
|
5
|
+
|
|
6
|
+
def class_name(operations)
|
|
7
|
+
types = operations.map(&:migration_type).uniq
|
|
8
|
+
tables = operations.map(&:table_name).uniq
|
|
9
|
+
|
|
10
|
+
if tables.size > 1
|
|
11
|
+
"SchemaChanges"
|
|
12
|
+
elsif types == [:create_table]
|
|
13
|
+
"Create#{camelize(tables.first)}"
|
|
14
|
+
elsif types == [:drop_table]
|
|
15
|
+
"Drop#{camelize(tables.first)}"
|
|
16
|
+
elsif types.all? { |t| t == :add_column }
|
|
17
|
+
"AddColumnsTo#{camelize(tables.first)}"
|
|
18
|
+
elsif types.all? { |t| t == :remove_column }
|
|
19
|
+
"RemoveColumnsFrom#{camelize(tables.first)}"
|
|
20
|
+
elsif types.all? { |t| t == :add_index }
|
|
21
|
+
"AddIndexesTo#{camelize(tables.first)}"
|
|
22
|
+
elsif types.all? { |t| t == :remove_index }
|
|
23
|
+
"RemoveIndexesFrom#{camelize(tables.first)}"
|
|
24
|
+
else
|
|
25
|
+
"Modify#{camelize(tables.first)}"
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def file_name(timestamp:, class_name:)
|
|
30
|
+
"#{timestamp}_#{underscore(class_name)}.rb"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def camelize(str)
|
|
34
|
+
str.to_s.split("_").map(&:capitalize).join
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def underscore(str)
|
|
38
|
+
str.to_s
|
|
39
|
+
.gsub(/([A-Z]+)([A-Z][a-z])/, '\1_\2')
|
|
40
|
+
.gsub(/([a-z\d])([A-Z])/, '\1_\2')
|
|
41
|
+
.downcase
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
require_relative "../schema/column_definition"
|
|
2
|
+
require_relative "../schema/index_definition"
|
|
3
|
+
require_relative "../schema/table_definition"
|
|
4
|
+
require_relative "../schema/schema_definition"
|
|
5
|
+
|
|
6
|
+
module Migsupo
|
|
7
|
+
module Loader
|
|
8
|
+
class ActiveRecordLoader
|
|
9
|
+
def initialize(ignored_tables: [])
|
|
10
|
+
@ignored_tables = ignored_tables
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def load_schema
|
|
14
|
+
tables = {}
|
|
15
|
+
|
|
16
|
+
connection.tables.each do |table_name|
|
|
17
|
+
next if @ignored_tables.include?(table_name)
|
|
18
|
+
|
|
19
|
+
columns = load_columns(table_name)
|
|
20
|
+
indexes = load_indexes(table_name)
|
|
21
|
+
options = load_table_options(table_name)
|
|
22
|
+
|
|
23
|
+
tables[table_name] = Schema::TableDefinition.new(
|
|
24
|
+
name: table_name,
|
|
25
|
+
columns: columns,
|
|
26
|
+
indexes: indexes,
|
|
27
|
+
options: options
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
Schema::SchemaDefinition.new(tables: tables)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def connection
|
|
37
|
+
ActiveRecord::Base.connection
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def load_columns(table_name)
|
|
41
|
+
connection.columns(table_name).filter_map do |col|
|
|
42
|
+
next if col.name == "id" && primary_key_column?(table_name, col)
|
|
43
|
+
|
|
44
|
+
Schema::ColumnDefinition.new(
|
|
45
|
+
name: col.name,
|
|
46
|
+
type: col.type,
|
|
47
|
+
options: column_options(col)
|
|
48
|
+
)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def column_options(col)
|
|
53
|
+
opts = {}
|
|
54
|
+
opts[:null] = col.null unless col.null == true
|
|
55
|
+
opts[:default] = col.default unless col.default.nil?
|
|
56
|
+
opts[:limit] = col.limit if col.limit
|
|
57
|
+
opts[:precision] = col.precision if col.precision
|
|
58
|
+
opts[:scale] = col.scale if col.scale
|
|
59
|
+
opts[:comment] = col.comment if col.respond_to?(:comment) && col.comment
|
|
60
|
+
opts
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def primary_key_column?(table_name, col)
|
|
64
|
+
pk = connection.primary_key(table_name)
|
|
65
|
+
pk == col.name
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def load_indexes(table_name)
|
|
69
|
+
connection.indexes(table_name).map do |idx|
|
|
70
|
+
Schema::IndexDefinition.new(
|
|
71
|
+
table_name: table_name,
|
|
72
|
+
columns: idx.columns,
|
|
73
|
+
name: idx.name,
|
|
74
|
+
options: index_options(idx)
|
|
75
|
+
)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def index_options(idx)
|
|
80
|
+
opts = {}
|
|
81
|
+
opts[:unique] = true if idx.unique
|
|
82
|
+
opts[:where] = idx.where if idx.where
|
|
83
|
+
opts[:using] = idx.using if idx.using && idx.using.to_sym != :btree
|
|
84
|
+
opts
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def load_table_options(table_name)
|
|
88
|
+
opts = {}
|
|
89
|
+
if connection.respond_to?(:table_comment)
|
|
90
|
+
comment = connection.table_comment(table_name)
|
|
91
|
+
opts[:comment] = comment if comment
|
|
92
|
+
end
|
|
93
|
+
opts
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
require_relative "../parser/dsl_context"
|
|
2
|
+
|
|
3
|
+
module Migsupo
|
|
4
|
+
module Loader
|
|
5
|
+
# Loads the current schema state from db/schema.rb without a live DB connection.
|
|
6
|
+
# db/schema.rb uses the same create_table DSL as Schemafile, so we reuse DslContext.
|
|
7
|
+
class SchemaRbLoader
|
|
8
|
+
def initialize(schema_rb_path:, ignored_tables: [])
|
|
9
|
+
@schema_rb_path = schema_rb_path
|
|
10
|
+
@ignored_tables = ignored_tables
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def load_schema
|
|
14
|
+
source = File.read(@schema_rb_path)
|
|
15
|
+
context = Parser::DslContext.new
|
|
16
|
+
context.instance_eval(source, @schema_rb_path, 1)
|
|
17
|
+
schema = context.to_schema_definition
|
|
18
|
+
|
|
19
|
+
filtered_tables = schema.tables.reject { |name, _| @ignored_tables.include?(name) }
|
|
20
|
+
Schema::SchemaDefinition.new(tables: filtered_tables)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
require_relative "table_dsl_context"
|
|
2
|
+
require_relative "../schema/index_definition"
|
|
3
|
+
require_relative "../schema/schema_definition"
|
|
4
|
+
|
|
5
|
+
module Migsupo
|
|
6
|
+
module Parser
|
|
7
|
+
class DslContext
|
|
8
|
+
def initialize
|
|
9
|
+
@tables = {}
|
|
10
|
+
@indexes = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def create_table(name, **options, &block)
|
|
14
|
+
ctx = TableDslContext.new(name, options)
|
|
15
|
+
ctx.instance_eval(&block) if block
|
|
16
|
+
table_def = ctx.to_table_definition
|
|
17
|
+
|
|
18
|
+
# merge standalone indexes into the table definition later via SchemaDefinition
|
|
19
|
+
@tables[name.to_s] = table_def
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_index(table_name, columns, **options)
|
|
23
|
+
@indexes << Schema::IndexDefinition.new(
|
|
24
|
+
table_name: table_name.to_s,
|
|
25
|
+
columns: Array(columns).map(&:to_s),
|
|
26
|
+
name: options.delete(:name),
|
|
27
|
+
options: options
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def to_schema_definition
|
|
32
|
+
tables = @tables.transform_values do |table_def|
|
|
33
|
+
standalone = @indexes.select { |i| i.table_name == table_def.name }
|
|
34
|
+
next table_def if standalone.empty?
|
|
35
|
+
|
|
36
|
+
merged_indexes = (table_def.indexes + standalone).uniq(&:name)
|
|
37
|
+
Schema::TableDefinition.new(
|
|
38
|
+
name: table_def.name,
|
|
39
|
+
columns: table_def.columns,
|
|
40
|
+
indexes: merged_indexes,
|
|
41
|
+
options: table_def.options
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
Schema::SchemaDefinition.new(tables: tables)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
require_relative "dsl_context"
|
|
2
|
+
|
|
3
|
+
module Migsupo
|
|
4
|
+
module Parser
|
|
5
|
+
class SchemafileParser
|
|
6
|
+
def self.parse(path)
|
|
7
|
+
source = File.read(path)
|
|
8
|
+
parse_string(source, path)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.parse_string(source, filename = "(string)")
|
|
12
|
+
context = DslContext.new
|
|
13
|
+
context.instance_eval(source, filename, 1)
|
|
14
|
+
context.to_schema_definition
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
require_relative "../schema/column_definition"
|
|
2
|
+
require_relative "../schema/index_definition"
|
|
3
|
+
require_relative "../schema/table_definition"
|
|
4
|
+
|
|
5
|
+
module Migsupo
|
|
6
|
+
module Parser
|
|
7
|
+
class TableDslContext
|
|
8
|
+
COLUMN_TYPES = %i[
|
|
9
|
+
string text integer bigint float decimal boolean
|
|
10
|
+
date datetime time timestamp binary blob json jsonb
|
|
11
|
+
uuid hstore inet cidr macaddr bit varbit
|
|
12
|
+
virtual primary_key
|
|
13
|
+
].freeze
|
|
14
|
+
|
|
15
|
+
def initialize(table_name, options = {})
|
|
16
|
+
@table_name = table_name.to_s
|
|
17
|
+
@options = options
|
|
18
|
+
@columns = []
|
|
19
|
+
@indexes = []
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
COLUMN_TYPES.each do |type|
|
|
23
|
+
define_method(type) do |*names, **opts|
|
|
24
|
+
names.each { |name| add_column(name, type, opts) }
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def column(name, type, **opts)
|
|
29
|
+
add_column(name, type, opts)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def references(name, **opts)
|
|
33
|
+
polymorphic = opts.delete(:polymorphic)
|
|
34
|
+
index_opt = opts.delete(:index) { true }
|
|
35
|
+
foreign_key = opts.delete(:foreign_key)
|
|
36
|
+
|
|
37
|
+
col_type = opts.delete(:type) { :bigint }
|
|
38
|
+
add_column("#{name}_id", col_type, opts)
|
|
39
|
+
|
|
40
|
+
if polymorphic
|
|
41
|
+
add_column("#{name}_type", :string, {})
|
|
42
|
+
if index_opt
|
|
43
|
+
add_index_entry([@table_name, "#{name}_type", "#{name}_id"],
|
|
44
|
+
name: "index_#{@table_name}_on_#{name}_type_and_#{name}_id")
|
|
45
|
+
end
|
|
46
|
+
elsif index_opt
|
|
47
|
+
idx_opts = index_opt.is_a?(Hash) ? index_opt : {}
|
|
48
|
+
add_index_entry(["#{name}_id"], idx_opts)
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
alias belongs_to references
|
|
53
|
+
|
|
54
|
+
def timestamps(**opts)
|
|
55
|
+
precision = opts.fetch(:precision, nil)
|
|
56
|
+
col_opts = precision ? { precision: precision } : {}
|
|
57
|
+
col_opts[:null] = opts[:null] if opts.key?(:null)
|
|
58
|
+
add_column("created_at", :datetime, col_opts)
|
|
59
|
+
add_column("updated_at", :datetime, col_opts)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def index(columns, **opts)
|
|
63
|
+
add_index_entry(Array(columns).map(&:to_s), opts)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def to_table_definition
|
|
67
|
+
Schema::TableDefinition.new(
|
|
68
|
+
name: @table_name,
|
|
69
|
+
columns: @columns,
|
|
70
|
+
indexes: @indexes,
|
|
71
|
+
options: @options
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def add_column(name, type, opts)
|
|
78
|
+
@columns << Schema::ColumnDefinition.new(name: name.to_s, type: type, options: opts)
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def add_index_entry(columns, opts)
|
|
82
|
+
columns = columns.map(&:to_s)
|
|
83
|
+
@indexes << Schema::IndexDefinition.new(
|
|
84
|
+
table_name: @table_name,
|
|
85
|
+
columns: columns,
|
|
86
|
+
name: opts.delete(:name),
|
|
87
|
+
options: opts
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Migsupo
|
|
2
|
+
module Schema
|
|
3
|
+
class ColumnDefinition
|
|
4
|
+
attr_reader :name, :type, :options
|
|
5
|
+
|
|
6
|
+
COMPARABLE_OPTIONS = %i[null default limit precision scale comment].freeze
|
|
7
|
+
|
|
8
|
+
def initialize(name:, type:, options: {})
|
|
9
|
+
@name = name.to_s
|
|
10
|
+
@type = type.to_sym
|
|
11
|
+
@options = normalize_options(options)
|
|
12
|
+
freeze
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ==(other)
|
|
16
|
+
return false unless other.is_a?(ColumnDefinition)
|
|
17
|
+
|
|
18
|
+
name == other.name && type == other.type && comparable_options == other.comparable_options
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
alias eql? ==
|
|
22
|
+
|
|
23
|
+
def hash
|
|
24
|
+
[name, type, comparable_options].hash
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def to_h
|
|
28
|
+
{ name: name, type: type, options: options }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def comparable_options
|
|
32
|
+
options.slice(*COMPARABLE_OPTIONS).reject { |_, v| v.nil? }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def normalize_options(opts)
|
|
38
|
+
opts.transform_keys(&:to_sym).freeze
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
module Migsupo
|
|
2
|
+
module Schema
|
|
3
|
+
class IndexDefinition
|
|
4
|
+
attr_reader :table_name, :columns, :name, :options
|
|
5
|
+
|
|
6
|
+
def initialize(table_name:, columns:, name: nil, options: {})
|
|
7
|
+
@table_name = table_name.to_s
|
|
8
|
+
@columns = Array(columns).map(&:to_s).freeze
|
|
9
|
+
@options = options.transform_keys(&:to_sym).freeze
|
|
10
|
+
@name = (name || generate_name).to_s
|
|
11
|
+
freeze
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def ==(other)
|
|
15
|
+
return false unless other.is_a?(IndexDefinition)
|
|
16
|
+
|
|
17
|
+
name == other.name &&
|
|
18
|
+
table_name == other.table_name &&
|
|
19
|
+
columns == other.columns &&
|
|
20
|
+
comparable_options == other.comparable_options
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
alias eql? ==
|
|
24
|
+
|
|
25
|
+
def hash
|
|
26
|
+
[table_name, name].hash
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def to_h
|
|
30
|
+
{ table_name: table_name, columns: columns, name: name, options: options }
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def comparable_options
|
|
34
|
+
options.reject { |k, _| k == :name }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def generate_name
|
|
40
|
+
"index_#{table_name}_on_#{columns.join('_and_')}"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
module Migsupo
|
|
2
|
+
module Schema
|
|
3
|
+
class SchemaDefinition
|
|
4
|
+
attr_reader :tables
|
|
5
|
+
|
|
6
|
+
def initialize(tables: {})
|
|
7
|
+
@tables = tables.freeze
|
|
8
|
+
freeze
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def table(table_name)
|
|
12
|
+
@tables[table_name.to_s]
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def to_h
|
|
16
|
+
{ tables: tables.transform_values(&:to_h) }
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|