groonga-schema 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,211 @@
1
+ # Copyright (C) 2016 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ module GroongaSchema
18
+ class Column
19
+ attr_reader :table_name
20
+ attr_reader :name
21
+ attr_accessor :type
22
+ attr_accessor :flags
23
+ attr_accessor :value_type
24
+ attr_accessor :sources
25
+ attr_writer :reference_value_type
26
+ attr_accessor :related_columns
27
+ def initialize(table_name, name)
28
+ @table_name = table_name
29
+ @name = name
30
+ @type = :scalar
31
+ @flags = []
32
+ @value_type = "ShortText"
33
+ @sources = []
34
+ @reference_value_type = false
35
+ @related_columns = []
36
+ end
37
+
38
+ def reference_value_type?
39
+ @reference_value_type
40
+ end
41
+
42
+ def apply_command(command)
43
+ applier = CommandApplier.new(self, command)
44
+ applier.apply
45
+ end
46
+
47
+ def apply_column(column)
48
+ self.type = column.type
49
+ self.flags = column.flags
50
+ self.value_type = column.value_type
51
+ self.sources = column.sources
52
+ self.reference_value_type = column.reference_value_type?
53
+ end
54
+
55
+ def ==(other)
56
+ return false unless other.is_a?(self.class)
57
+
58
+ @table_name == other.table_name and
59
+ @name == other.name and
60
+ @type == other.type and
61
+ @flags.sort == other.flags.sort and
62
+ @value_type == other.value_type and
63
+ @sources == other.sources
64
+ end
65
+
66
+ def to_create_groonga_command
67
+ column_create_command(@name)
68
+ end
69
+
70
+ def to_remove_groonga_command
71
+ column_remove_command(@name)
72
+ end
73
+
74
+ def to_copy_groonga_command(to_table_name, to_name)
75
+ column_copy_command(to_table_name, to_name)
76
+ end
77
+
78
+ def to_migrate_start_groonga_commands
79
+ commands = []
80
+ commands << column_create_command(new_name)
81
+ if type != :index
82
+ commands << column_copy_command(@table_name, new_name)
83
+ end
84
+ commands << column_rename_command(@name, old_name)
85
+ commands << column_rename_command(new_name, @name)
86
+ commands
87
+ end
88
+
89
+ def to_migrate_finish_groonga_commands
90
+ [
91
+ column_remove_command(old_name),
92
+ ]
93
+ end
94
+
95
+ private
96
+ def old_name
97
+ "#{@name}_old"
98
+ end
99
+
100
+ def new_name
101
+ "#{@name}_new"
102
+ end
103
+
104
+ def column_create_command(name)
105
+ flags_value = [type_flag, *flags].join("|")
106
+ sources_value = @sources.join(",")
107
+ sources_value = nil if sources_value.empty?
108
+ arguments = {
109
+ "table" => @table_name,
110
+ "name" => name,
111
+ "flags" => flags_value,
112
+ "type" => @value_type,
113
+ "source" => sources_value,
114
+ }
115
+ Groonga::Command::ColumnCreate.new(arguments)
116
+ end
117
+
118
+ def column_remove_command(name)
119
+ arguments = {
120
+ "table" => @table_name,
121
+ "name" => name,
122
+ }
123
+ Groonga::Command::ColumnRemove.new(arguments)
124
+ end
125
+
126
+ def column_copy_command(to_table_name, to_name)
127
+ arguments = {
128
+ "from_table" => @table_name,
129
+ "from_name" => @name,
130
+ "to_table" => to_table_name,
131
+ "to_name" => to_name,
132
+ }
133
+ Groonga::Command::ColumnCopy.new(arguments)
134
+ end
135
+
136
+ def column_rename_command(name, new_name)
137
+ arguments = {
138
+ "table" => @table_name,
139
+ "name" => name,
140
+ "new_name" => new_name,
141
+ }
142
+ Groonga::Command::ColumnRename.new(arguments)
143
+ end
144
+
145
+ def type_flag
146
+ case @type
147
+ when :scalar
148
+ "COLUMN_SCALAR"
149
+ when :vector
150
+ "COLUMN_VECTOR"
151
+ when :index
152
+ "COLUMN_INDEX"
153
+ else
154
+ "COLUMN_SCALAR"
155
+ end
156
+ end
157
+
158
+ class CommandApplier
159
+ def initialize(column, command)
160
+ @column = column
161
+ @command = command
162
+ end
163
+
164
+ def apply
165
+ apply_flags
166
+ apply_value_type
167
+ apply_sources
168
+ end
169
+
170
+ private
171
+ def apply_flags
172
+ @type = :scalar
173
+ @flags = []
174
+ @command.flags.each do |flag|
175
+ parse_flag(flag)
176
+ end
177
+
178
+ @column.type = @type
179
+ @column.flags = @flags
180
+ end
181
+
182
+ def parse_flag(flag)
183
+ case flag
184
+ when "COLUMN_SCALAR"
185
+ @type = :scalar
186
+ when "COLUMN_VECTOR"
187
+ @type = :vector
188
+ when "COLUMN_INDEX"
189
+ @type = :index
190
+ else
191
+ @flags << flag
192
+ end
193
+ end
194
+
195
+ def apply_value_type
196
+ # TODO: Validate for index column. Index column must have table as
197
+ # value type.
198
+ @column.value_type = @command.type
199
+ end
200
+
201
+ def apply_sources
202
+ case @type
203
+ when :index
204
+ @column.sources = @command.sources
205
+ else
206
+ @column.sources = []
207
+ end
208
+ end
209
+ end
210
+ end
211
+ end
@@ -0,0 +1,112 @@
1
+ # Copyright (C) 2016 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "optparse"
18
+ require "uri"
19
+ require "open-uri"
20
+
21
+ require "groonga/command/parser"
22
+
23
+ require "groonga-schema/differ"
24
+
25
+ module GroongaSchema
26
+ module CommandLine
27
+ class GroongaSchemaDiff
28
+ class << self
29
+ def run(args=ARGV)
30
+ new(args).run
31
+ end
32
+ end
33
+
34
+ def initialize(args)
35
+ @args = args
36
+ @format = :command
37
+ end
38
+
39
+ def run
40
+ parse_arguments
41
+
42
+ from_schema = parse_schema(@from)
43
+ to_schema = parse_schema(@to)
44
+ differ = GroongaSchema::Differ.new(from_schema, to_schema)
45
+ diff = differ.diff
46
+ $stdout.print(diff.to_groonga_command_list(:format => @format))
47
+
48
+ if diff.same?
49
+ 0
50
+ else
51
+ 1
52
+ end
53
+ end
54
+
55
+ private
56
+ def parse_arguments
57
+ parser = OptionParser.new
58
+ parser.banner += " FROM_SCHEMA TO_SCHEMA"
59
+
60
+ available_formats = [:command, :uri]
61
+ parser.on("--format=FORMAT", available_formats,
62
+ "Specify output Groonga command format.",
63
+ "Available formats: #{available_formats.join(", ")}",
64
+ "(#{@format})") do |format|
65
+ @format = format
66
+ end
67
+
68
+ rest_args = parser.parse(@args)
69
+
70
+ if rest_args.size != 2
71
+ $stderr.puts("Error: Both FROM_SCHEMA and TO_SCHEMA are required.")
72
+ $stderr.puts(parser.help)
73
+ exit(false)
74
+ end
75
+ @from, @to = rest_args
76
+ end
77
+
78
+ def parse_schema(resource_path)
79
+ open_resource(resource_path) do |resource|
80
+ schema = GroongaSchema::Schema.new
81
+ parser = Groonga::Command::Parser.new
82
+ parser.on_command do |command|
83
+ schema.apply_command(command)
84
+ end
85
+ resource.each_line do |line|
86
+ parser << line
87
+ end
88
+ parser.finish
89
+ schema
90
+ end
91
+ end
92
+
93
+ def open_resource(resource_path)
94
+ uri = nil
95
+ begin
96
+ uri = URI.parse(resource_path)
97
+ rescue URI::InvalidURIError
98
+ end
99
+
100
+ if uri and uri.respond_to?(:open)
101
+ uri.open do |response|
102
+ yield(response)
103
+ end
104
+ else
105
+ File.open(resource_path) do |file|
106
+ yield(file)
107
+ end
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,268 @@
1
+ # Copyright (C) 2016 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ module GroongaSchema
18
+ class Diff
19
+ attr_reader :removed_plugins
20
+ attr_reader :added_plugins
21
+
22
+ attr_reader :removed_tables
23
+ attr_reader :added_tables
24
+ attr_reader :changed_tables
25
+
26
+ attr_reader :removed_columns
27
+ attr_reader :added_columns
28
+ attr_reader :changed_columns
29
+ def initialize
30
+ @removed_plugins = []
31
+ @added_plugins = []
32
+
33
+ @removed_tables = {}
34
+ @added_tables = {}
35
+ @changed_tables = {}
36
+
37
+ @removed_columns = {}
38
+ @added_columns = {}
39
+ @changed_columns = {}
40
+ end
41
+
42
+ def ==(other)
43
+ return false unless other.is_a?(self.class)
44
+
45
+ @removed_plugins == other.removed_plugins and
46
+ @added_plugins == other.added_plugins and
47
+ @removed_tables == other.removed_tables and
48
+ @added_tables == other.added_tables and
49
+ @changed_tables == other.changed_tables and
50
+ @removed_columns == other.removed_columns and
51
+ @added_columns == other.added_columns and
52
+ @changed_columns == other.changed_columns
53
+ end
54
+
55
+ def same?
56
+ @removed_plugins.empty? and
57
+ @added_plugins.empty? and
58
+ @removed_tables.empty? and
59
+ @added_tables.empty? and
60
+ @changed_tables.empty? and
61
+ @removed_columns.empty? and
62
+ @added_columns.empty? and
63
+ @changed_columns.empty?
64
+ end
65
+
66
+ def to_groonga_command_list(options={})
67
+ converter = GroongaCommandListConverter.new(self, options)
68
+ converter.convert
69
+ end
70
+
71
+ class GroongaCommandListConverter
72
+ def initialize(diff, options={})
73
+ @diff = diff
74
+ @options = options
75
+ @grouped_list = []
76
+ end
77
+
78
+ def convert
79
+ @grouped_list.clear
80
+
81
+ convert_added_plugins
82
+ convert_added_tables
83
+ convert_removed_columns
84
+ convert_removed_tables
85
+ convert_removed_plugins
86
+ convert_changed_tables
87
+
88
+ meaningful_grouped_list = @grouped_list.reject do |group|
89
+ group.empty?
90
+ end
91
+ formatted_grouped_list = meaningful_grouped_list.collect do |group|
92
+ command_list = ""
93
+ group.each do |command|
94
+ command_list << "#{format_command(command)}\n"
95
+ end
96
+ command_list
97
+ end
98
+ formatted_grouped_list.join("\n")
99
+ end
100
+
101
+ private
102
+ def convert_added_plugins
103
+ sorted_plugins = @diff.added_plugins.sort_by(&:name)
104
+ @grouped_list << sorted_plugins.collect(&:to_register_groonga_command)
105
+ end
106
+
107
+ def convert_added_tables
108
+ reference_table_names = []
109
+ no_reference_table_names = []
110
+ @diff.added_tables.each do |name, table|
111
+ if table.reference_key_type?
112
+ reference_table_names << name
113
+ else
114
+ no_reference_table_names << name
115
+ end
116
+ end
117
+ no_reference_table_names |=
118
+ (@diff.added_columns.keys - reference_table_names)
119
+
120
+ sorted_reference_table_names = reference_table_names.sort
121
+ sorted_no_reference_table_names = no_reference_table_names.sort
122
+
123
+ sorted_table_names =
124
+ sorted_no_reference_table_names +
125
+ sorted_reference_table_names
126
+
127
+ sorted_table_names.each do |name|
128
+ group = []
129
+ table = @diff.added_tables[name]
130
+ group << table.to_create_groonga_command if table
131
+ group.concat(convert_added_columns(name, false))
132
+ @grouped_list << group
133
+ end
134
+
135
+ sorted_table_names.each do |name|
136
+ @grouped_list << convert_added_columns(name, true)
137
+ end
138
+ end
139
+
140
+ def convert_added_columns(name, target_is_reference_type)
141
+ columns = @diff.added_columns[name]
142
+ return [] if columns.nil?
143
+
144
+ sorted_columns = columns.sort_by do |column_name,|
145
+ column_name
146
+ end
147
+
148
+ group = []
149
+ sorted_columns.each do |column_name, column|
150
+ if target_is_reference_type
151
+ next unless column.reference_value_type?
152
+ else
153
+ next if column.reference_value_type?
154
+ end
155
+ group << column.to_create_groonga_command
156
+ end
157
+ group
158
+ end
159
+
160
+ def convert_removed_columns
161
+ sorted_removed_columns = @diff.removed_columns.sort_by do |table_name,|
162
+ table_name
163
+ end
164
+
165
+ column_groups = []
166
+ sorted_removed_columns.each do |table_name, columns|
167
+ group = []
168
+ columns.each do |column_name, column|
169
+ group << column unless column.sources.empty?
170
+ end
171
+ next if group.empty?
172
+ column_groups << group
173
+ end
174
+ sorted_removed_columns.each do |table_name, columns|
175
+ group = []
176
+ columns.each do |column_name, column|
177
+ group << column if column.sources.empty?
178
+ end
179
+ next if group.empty?
180
+ column_groups << group
181
+ end
182
+
183
+ column_groups.each do |columns|
184
+ sorted_columns = columns.sort_by do |column|
185
+ column.name
186
+ end
187
+ group = sorted_columns.collect do |column|
188
+ column.to_remove_groonga_command
189
+ end
190
+ @grouped_list << group
191
+ end
192
+ end
193
+
194
+ def convert_removed_tables
195
+ sorted_tables = @diff.removed_tables.sort_by do |name, table|
196
+ [
197
+ table.reference_key_type? ? 0 : 1,
198
+ table.name,
199
+ ]
200
+ end
201
+
202
+ sorted_tables.each do |name, table|
203
+ @grouped_list << [table.to_remove_groonga_command]
204
+ end
205
+ end
206
+
207
+ def convert_removed_plugins
208
+ sorted_plugins = @diff.removed_plugins.sort_by(&:name)
209
+ @grouped_list << sorted_plugins.collect(&:to_unregister_groonga_command)
210
+ end
211
+
212
+ def convert_changed_tables
213
+ sorted_tables = @diff.changed_tables.sort_by do |name, table|
214
+ [
215
+ table.reference_key_type? ? 1 : 0,
216
+ table.name,
217
+ ]
218
+ end
219
+
220
+ sorted_tables.each do |name, table|
221
+ @grouped_list << table.to_migrate_start_groonga_commands
222
+ end
223
+ convert_changed_columns
224
+ sorted_tables.each do |name, table|
225
+ @grouped_list << table.to_migrate_finish_groonga_commands
226
+ end
227
+ end
228
+
229
+ def convert_changed_columns
230
+ all_columns = []
231
+ @diff.changed_columns.each do |table_name, columns|
232
+ all_columns.concat(columns.values)
233
+ end
234
+
235
+ sorted_columns = all_columns.sort_by do |column|
236
+ [
237
+ (column.type == :index) ? 1 : 0,
238
+ column.table_name,
239
+ column.name,
240
+ ]
241
+ end
242
+ sorted_columns.each do |column|
243
+ @grouped_list << column.to_migrate_start_groonga_commands
244
+ end
245
+
246
+ sorted_columns = all_columns.sort_by do |column|
247
+ [
248
+ (column.type == :index) ? 0 : 1,
249
+ column.table_name,
250
+ column.name,
251
+ ]
252
+ end
253
+ sorted_columns.each do |column|
254
+ @grouped_list << column.to_migrate_finish_groonga_commands
255
+ end
256
+ end
257
+
258
+ def format_command(command)
259
+ case @options[:format]
260
+ when :uri
261
+ command.to_uri_format
262
+ else
263
+ command.to_command_format
264
+ end
265
+ end
266
+ end
267
+ end
268
+ end
@@ -0,0 +1,120 @@
1
+ # Copyright (C) 2016 Kouhei Sutou <kou@clear-code.com>
2
+ #
3
+ # This library is free software; you can redistribute it and/or
4
+ # modify it under the terms of the GNU Lesser General Public
5
+ # License as published by the Free Software Foundation; either
6
+ # version 2.1 of the License, or (at your option) any later version.
7
+ #
8
+ # This library is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
11
+ # Lesser General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU Lesser General Public
14
+ # License along with this library; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
16
+
17
+ require "groonga-schema/diff"
18
+
19
+ module GroongaSchema
20
+ class Differ
21
+ # @param from [Schema] The original schema.
22
+ # @param to [Schema] The changed schema.
23
+ def initialize(from, to)
24
+ @from = from
25
+ @to = to
26
+ end
27
+
28
+ def diff
29
+ diff = Diff.new
30
+ diff_plugins(diff)
31
+ diff_tables(diff)
32
+ diff_columns(diff)
33
+ diff
34
+ end
35
+
36
+ private
37
+ def diff_plugins(diff)
38
+ diff.removed_plugins.concat(@from.plugins - @to.plugins)
39
+ diff.added_plugins.concat(@to.plugins - @from.plugins)
40
+ end
41
+
42
+ def diff_tables(diff)
43
+ from_tables = @from.tables
44
+ to_tables = @to.tables
45
+
46
+ from_tables.each do |name, from_table|
47
+ to_table = to_tables[name]
48
+ if to_table.nil?
49
+ diff.removed_tables[from_table.name] = from_table
50
+ elsif from_table != to_table
51
+ diff_changed_table(diff, to_table)
52
+ end
53
+ end
54
+
55
+ to_tables.each do |name, to_table|
56
+ from_table = from_tables[name]
57
+ if from_table.nil?
58
+ diff.added_tables[name] = to_table
59
+ end
60
+ end
61
+ end
62
+
63
+ def diff_changed_table(diff, to_table)
64
+ diff.changed_tables[to_table.name] = to_table
65
+
66
+ to_table.related_tables.each do |table|
67
+ diff.changed_tables[table.name] = table
68
+ end
69
+
70
+ to_table.related_columns.each do |column|
71
+ table_name = column.table_name
72
+ diff.changed_columns[table_name] ||= {}
73
+ diff.changed_columns[table_name][column.name] = column
74
+ end
75
+ end
76
+
77
+ def diff_columns(diff)
78
+ @from.columns.each do |table_name, from_columns|
79
+ to_columns = @to.columns[table_name]
80
+ next if to_columns.nil?
81
+
82
+ from_columns.each do |name, from_column|
83
+ to_column = to_columns[name]
84
+ if to_column.nil?
85
+ diff.removed_columns[table_name] ||= {}
86
+ diff.removed_columns[table_name][name] = from_column
87
+ elsif from_column != to_column
88
+ diff_changed_column(diff, to_column)
89
+ end
90
+ end
91
+ end
92
+
93
+ @to.columns.each do |table_name, to_columns|
94
+ from_columns = @from.columns[table_name] || {}
95
+ to_columns.each do |name, to_column|
96
+ from_column = from_columns[name]
97
+ if from_column.nil?
98
+ diff.added_columns[table_name] ||= {}
99
+ diff.added_columns[table_name][name] = to_column
100
+ end
101
+ end
102
+ end
103
+ end
104
+
105
+ def diff_changed_column(diff, to_column)
106
+ table_name = to_column.table_name
107
+ name = to_column.name
108
+
109
+ diff.changed_columns[table_name] ||= {}
110
+ diff.changed_columns[table_name][name] = to_column
111
+
112
+ to_column.related_columns.each do |related_column|
113
+ related_table_name = related_column.table_name
114
+ related_name = related_column.name
115
+ diff.changed_columns[related_table_name] ||= {}
116
+ diff.changed_columns[related_table_name][related_name] = related_column
117
+ end
118
+ end
119
+ end
120
+ end