# require File.expand_path(File.dirname(__FILE__)) + '/types' module Groovy class Schema SEARCH_TABLE_NAME = 'Terms'.freeze COLUMN_DEFAULTS = { compress: :zstandard }.freeze TYPES = { 'string' => 'short_text', 'text' => 'text', 'float' => 'float', # 'Bool' => 'boolean', 'boolean' => 'boolean', 'integer' => 'int32', 'big_integer' => 'int64', 'date' => 'date', 'time' => 'time', 'datetime' => 'time' }.freeze attr_reader :index_columns def initialize(context, table_name, opts = {}) @context, @table_name, @opts = context, table_name, opts || {} @spec, @index_columns = {}, [] @cache = {} end def table @table ||= context[table_name] end def search_table @cache[:search_table] ||= context[SEARCH_TABLE_NAME] end def column_names @cache[:column_names] ||= get_names(table.columns) end def singular_references @cache[:singular_references] ||= get_names(table.columns.select(&:reference_column?).reject(&:vector?)) end def plural_references @cache[:plural_references] ||= get_names(table.columns.select(&:vector?)) end def attribute_columns @cache[:attribute_columns] ||= get_names(table.columns.select { |c| c.column? && !c.reference_column? && !c.vector? }) end def time_columns @cache[:time_columns] ||= columns_by_type('Time') end def integer_columns @cache[:integer_columns] ||= columns_by_type('Int32') end def boolean_columns @cache[:boolean_columns] ||= columns_by_type('Bool') end def columns_by_type(type) get_names(table.columns.select { |c| c.column? && c.range.name == type }) end def reload @cache = {} end def rebuild! log("Rebuilding!") # remove_table! if table remove_columns_not_in([]) sync end def column(name, type, options = {}) # groonga_type = Types.map(type) groonga_type = type @index_columns.push(name) if options.delete(:index) @spec[name.to_sym] = { type: groonga_type, args: [COLUMN_DEFAULTS.merge(options)] } end TYPES.each do |from_type, to_type| define_method(from_type) do |*args| column(args.shift, to_type, (args.shift || {})) end end def reference(name, table_name = nil, options = {}) table_name = "#{name}s" if table_name.nil? @spec[name.to_sym] = { type: :reference, args: [table_name, options] } end def timestamps(opts = {}) column(:created_at, 'time') column(:updated_at, 'time') end def sync(db_context = nil) switch_to_context(db_context) if db_context ensure_created! remove_columns_not_in(@spec.keys) @spec.each do |col, spec| check_and_add_column(col, spec[:type], spec[:args]) end @index_columns.each do |col| add_index_on(col) end reload self end private attr_reader :context, :table_name def switch_to_context(db_context) log("Switching context to #{db_context}") @context = db_context @table = @search_table = nil # clear cached vars end def add_index_on(col, opts = {}) ensure_search_table! return false if search_table.have_column?([table_name, col].join('_')) name_col = [table_name, col].join('.') log "Adding index on #{name_col}" Groonga::Schema.change_table(SEARCH_TABLE_NAME, context: context) do |table| # table.index(name_col, name: name_col, with_position: true, with_section: true) table.index(name_col, name: name_col.sub('.', '_')) end end def ensure_search_table! return if search_table opts = (@opts[:search_table] || {}).merge({ type: :patricia_trie, normalizer: :NormalizerAuto, key_type: "ShortText", default_tokenizer: "TokenBigram" }) log("Creating search table with options: #{opts.inspect}") Groonga::Schema.create_table(SEARCH_TABLE_NAME, opts.merge(context: context)) end def remove_columns_not_in(list) column_names.each do |col| remove_column(col) unless list.include?(col) end end def ensure_created! create_table! if table.nil? end def check_and_add_column(name, type, options) add_column(name, type, options) unless has_column?(name, type) end def has_column?(name, type = nil) table.columns.any? do |col| col && col.name == "#{table_name}.#{name}" # && (type.nil? or col.type == type) end end def add_column(name, type, args = []) log "Adding column #{name} with type #{type}, args: #{args.inspect}" Groonga::Schema.change_table(table_name, context: context) do |table| table.public_send(type, name, *args) end end def remove_column(name) log "Removing column #{name}" Groonga::Schema.change_table(table_name, context: context) do |table| table.remove_column(name) end end def create_table! log "Creating table #{table_name}!" Groonga::Schema.create_table(table_name, context: context) end def remove_table! log "Removing table #{table_name}!" Groonga::Schema.remove_table(table_name, context: context) @table = nil end def get_names(columns) columns.map { |col| col.name.split('.').last.to_sym } end def log(str) puts "[#{table_name}] #{str}" # if ENV['DEBUG'] end end end