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"