groonga-schema 1.0.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.
@@ -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