lib/groonga/schema.rb in groonga-0.0.5 vs lib/groonga/schema.rb in groonga-0.0.6

- old
+ new

@@ -17,10 +17,33 @@ module Groonga # groongaのスキーマ(データ構造)を管理するクラス。 + # + # Groonga::Schemaを使うことにより簡単にテーブルやカラムを + # 追加・削除することができる。 + # + # http://qwik.jp/senna/senna2.files/rect4605.png + # のようなスキーマを定義する場合は以下のようになる。 + # + # Groonga::Schema.define do |schema| + # schema.create_table("items") do |table| + # table.short_text("title") + # end + # + # schema.create_table("users") do |table| + # table.short_text("name") + # end + # + # schema.create_table("comments") do |table| + # table.reference("item", "items") + # table.reference("author", "users") + # table.text("content") + # table.time("issued") + # end + # end class Schema class << self # call-seq: # Groonga::Schema.define(options={}) {|schema| ...} @@ -68,43 +91,67 @@ # [+:persistent+] # テーブルを永続テーブルとする。+:path:+を省略した場 # 合はパス名は自動的に作成される。デフォルトでは永続 # テーブルとなる。 # - # [+:value_size+] - # 値のサイズを指定する。デフォルトは0。 + # [+:value_type+] + # 値の型を指定する。省略すると値のための領域を確保しない。 + # 値を保存したい場合は必ず指定すること。 def create_table(name, options={}, &block) define do |schema| schema.create_table(name, options, &block) end end + # 名前が_name_のテーブルを削除する。 + # _options_に指定可能な値は以下の通り。 + # + # [+:context+] + # スキーマ定義時に使用するGroonga::Contextを指定する。 + # 省略した場合はGroonga::Context.defaultを使用する。 + def remove_table(name, options={}) + define do |schema| + schema.remove_table(name, options) + end + end + + def change_table(name, options={}, &block) + define do |schema| + schema.change_table(name, options, &block) + end + end + # スキーマの内容を文字列で返す。返された値は - # Groonga::Schema#restoreすることによりスキーマ内に組 + # Groonga::Schema.restoreすることによりスキーマ内に組 # み込むことができる。 # # dump.rb: # File.open("/tmp/groonga-schema.rb", "w") do |schema| # dumped_text = Groonga::Schema.dump # end # # restore.rb: # dumped_text = Groonga::Schema.dump # Groonga::Database.create(:path => "/tmp/new-db.grn") - # Groonga::Schema.define do |schema| - # schema.restore(dumped_text) - # end + # Groonga::Schema.restore(dumped_text) # # _options_に指定可能な値は以下の通り。 # # [+:context+] # スキーマ定義時に使用するGroonga::Contextを指定する。 # 省略した場合はGroonga::Context.defaultを使用する。 def dump(options={}) Dumper.new(options).dump end + # Groonga::Schema.dumpで文字列化したスキーマを組み込む。 + def restore(dumped_text, options={}) + define(options) do |schema| + schema.load(dumped_text) + end + end + def normalize_type(type) # :nodoc: return type if type.nil? return type if type.is_a?(Groonga::Object) case type.to_s when "string" @@ -150,11 +197,11 @@ # Groonga::Schema.dumpで返されたスキーマの内容を読み込む。 # # 読み込まれた内容は#defineを呼び出すまでは実行されない # ことに注意すること。 - def restore(dumped_text) + def load(dumped_text) instance_eval(dumped_text) end # 名前が_name_のテーブルを作成する。 # @@ -176,43 +223,104 @@ # [+:persistent+] # テーブルを永続テーブルとする。+:path:+を省略した場 # 合はパス名は自動的に作成される。デフォルトでは永続 # テーブルとなる。 # - # [+:value_size+] - # 値のサイズを指定する。デフォルトは0。 + # [+:value_type+] + # 値の型を指定する。省略すると値のための領域を確保しな + # い。値を保存したい場合は必ず指定すること。 + # + # 参考: Groonga::Type.new def create_table(name, options={}) definition = TableDefinition.new(name, @options.merge(options || {})) yield(definition) @definitions << definition end + def remove_table(name, options={}) + definition = TableRemoveDefinition.new(name, @options.merge(options || {})) + @definitions << definition + end + + def change_table(name, options={}) + options = @options.merge(options || {}).merge(:change => true) + definition = TableDefinition.new(name, options) + yield(definition) + @definitions << definition + end + class TableDefinition + attr_reader :name + def initialize(name, options) @name = name @name = @name.to_s if @name.is_a?(Symbol) - @columns = [] + @definitions = [] + validate_options(options) @options = options @table_type = table_type end def define - table = @table_type.create(create_options) - @columns.each do |column| - column.define(table) + if @options[:change] + table = context[@name] + else + table = context[@name] + if table and @options[:force] + table.remove + end + table = @table_type.create(create_options) end + @definitions.each do |definition| + definition.define(table) + end table end def column(name, type, options={}) - column = self[name] || ColumnDefinition.new(name, options) - column.type = type - column.options.merge!(options) - @columns << column unless @columns.include?(column) + definition = self[name, ColumnDefinition] + if definition.nil? + definition = ColumnDefinition.new(name, options) + update_definition(name, ColumnDefinition, definition) + end + definition.type = type + definition.options.merge!(column_options.merge(options)) self end + def remove_column(name, options={}) + definition = self[name, ColumnRemoveDefinition] + if definition.nil? + definition = ColumnRemoveDefinition.new(name, options) + update_definition(name, ColumnRemoveDefinition, definition) + end + definition.options.merge!(options) + self + end + + def index(target_column, options={}) + name = options.delete(:name) + if name.nil? + target_column_name = nil + if target_column.is_a?(Groonga::Column) + target_column_name = target_column.name + else + target_column_name = target_column + end + name = target_column_name.gsub(/\./, "_") + end + + definition = self[name, IndexColumnDefinition] + if definition.nil? + definition = IndexColumnDefinition.new(name, options) + update_definition(name, IndexColumnDefinition, definition) + end + definition.target = target_column + definition.options.merge!(column_options.merge(options)) + self + end + def integer32(name, options={}) column(name, "Int32", options) end alias_method :integer, :integer32 alias_method :int32, :integer32 @@ -252,23 +360,51 @@ def long_text(name, options={}) column(name, "LongText", options) end - def index(name, target_column, options={}) - column = self[name] || IndexColumnDefinition.new(name, options) - column.target = target_column - column.options.merge!(options) - @columns << column unless @columns.include?(column) - self + def reference(name, table, options={}) + column(name, table, options) end - def [](name) - @columns.find {|column| column.name == name} + def [](name, definition_class=nil) + @definitions.find do |definition| + definition.name.to_s == name.to_s and + (definition_class.nil? or definition.is_a?(definition_class)) + end end + def context + @options[:context] || Groonga::Context.default + end + private + def update_definition(name, definition_class, definition) + old_definition = self[name, definition_class] + if old_definition + index = @definitions.index(old_definition) + @definitions[index] = definition + else + @definitions << definition + end + end + + AVAILABLE_OPTION_KEYS = [:context, :change, :force, + :type, :path, :persistent, + :key_type, :value_type, :sub_records, + :default_tokenizer, + :key_normalize, :key_with_sis] + def validate_options(options) + return if options.nil? + unknown_keys = options.keys - AVAILABLE_OPTION_KEYS + unless unknown_keys.empty? + message = "unknown keys are specified: #{unknown_keys.inspect}" + message << ": available keys: #{AVAILABLE_OPTION_KEYS.inspect}" + raise ArgumentError, message + end + end + def table_type type = @options[:type] case type when :array, nil Groonga::Array @@ -284,12 +420,13 @@ def create_options common = { :name => @name, :path => @options[:path], :persistent => persistent?, - :value_size => @options[:value_size], + :value_type => @options[:value_type], :context => context, + :sub_records => @options[:sub_records], } key_support_table_common = { :key_type => Schema.normalize_type(@options[:key_type]), :default_tokenizer => @options[:default_tokenizer], } @@ -307,17 +444,29 @@ else raise ArgumentError, "unknown table type: #{@table_type.inspect}" end end + def column_options + {:persistent => persistent?} + end + def persistent? @options[:persistent].nil? ? true : @options[:persistent] end + end - def context - @options[:context] || Groonga::Context.default + class TableRemoveDefinition + def initialize(name, options={}) + @name = name + @options = options end + + def define + context = @options[:context] || Groonga::Context.default + context[@name].remove + end end class ColumnDefinition attr_accessor :name, :type attr_reader :options @@ -334,10 +483,25 @@ Schema.normalize_type(@type), @options) end end + class ColumnRemoveDefinition + attr_accessor :name + attr_reader :options + + def initialize(name, options={}) + @name = name + @name = @name.to_s if @name.is_a?(Symbol) + @options = (options || {}).dup + end + + def define(table) + table.column(@name).remove + end + end + class IndexColumnDefinition attr_accessor :name, :target attr_reader :options def initialize(name, options={}) @@ -348,10 +512,13 @@ end def define(table) target = @target target = context[target] unless target.is_a?(Groonga::Object) + if target.nil? + raise ArgumentError, "Unknown index target: #{@target.inspect}" + end index = table.define_index_column(@name, target.table, @options) index.source = target index @@ -371,27 +538,52 @@ def dump context = @options[:context] || Groonga::Context.default database = context.database return nil if database.nil? - schema = "" + reference_columns = [] + definitions = [] database.each do |object| next unless object.is_a?(Groonga::Table) - next if object.name == "<ranguba:objects>" - schema << "create_table(#{object.name.inspect}) do |table|\n" - object.columns.each do |column| - type = column_method(column) + schema = "create_table(#{object.name.inspect}) do |table|\n" + object.columns.sort_by {|column| column.local_name}.each do |column| + if column.range.is_a?(Groonga::Table) + reference_columns << column + else + type = column_method(column) + name = column.local_name + schema << " table.#{type}(#{name.inspect})\n" + end + end + schema << "end" + definitions << schema + end + + reference_columns.group_by do |column| + column.table + end.each do |table, columns| + schema = "change_table(#{table.name.inspect}) do |table|\n" + columns.each do |column| name = column.local_name - schema << " table.#{type}(#{name.inspect})\n" + reference = column.range + schema << " table.reference(#{name.inspect}, " + + "#{reference.name.inspect})\n" end - schema << "end\n" + schema << "end" + definitions << schema end - schema + + if definitions.empty? + "" + else + definitions.join("\n\n") + "\n" + end end private def column_method(column) - case column.range.name + range = column.range + case range.name when "Int32" "integer32" when "Int64" "integer64" when "UInt32"