OSDN Git Service

Initial commit master
authorIwauo Tajima <iwauo.tajima@gmail.com>
Wed, 3 Feb 2010 12:33:07 +0000 (21:33 +0900)
committerIwauo Tajima <iwauo.tajima@gmail.com>
Wed, 3 Feb 2010 12:33:07 +0000 (21:33 +0900)
26 files changed:
gemspec.rb [new file with mode: 0644]
lib/hdboo.rb [new file with mode: 0644]
lib/hdboo/Makefile [new file with mode: 0644]
lib/hdboo/_search.c [new file with mode: 0644]
lib/hdboo/base.rb [new file with mode: 0644]
lib/hdboo/crawler.rb [new file with mode: 0644]
lib/hdboo/database.rb [new file with mode: 0644]
lib/hdboo/extconf.rb [new file with mode: 0644]
lib/hdboo/html.rb [new file with mode: 0644]
lib/hdboo/mkmf.log [new file with mode: 0644]
lib/hdboo/rest.rb [new file with mode: 0644]
lib/hdboo/search.rb [new file with mode: 0644]
lib/hdboo/sql.rb [new file with mode: 0644]
lib/hdboo/sqlparser.rb [new file with mode: 0644]
lib/hdboo/sqlparser.tab.rb [new file with mode: 0644]
lib/hdboo/sqlparser.y [new file with mode: 0644]
lib/hdboo/stub/smtp.rb [new file with mode: 0644]
test/suite.rb [new file with mode: 0644]
test/tc_base.rb [new file with mode: 0644]
test/tc_crawler.rb [new file with mode: 0644]
test/tc_database.rb [new file with mode: 0644]
test/tc_html.rb [new file with mode: 0644]
test/tc_mime.rb [new file with mode: 0644]
test/tc_rest.rb [new file with mode: 0644]
test/tc_search.rb [new file with mode: 0644]
test/tc_sql.rb [new file with mode: 0644]

diff --git a/gemspec.rb b/gemspec.rb
new file mode 100644 (file)
index 0000000..09f1030
--- /dev/null
@@ -0,0 +1,17 @@
+require 'rubygems'
+
+SPEC = Gem::Specification.new do |s|
+  s.name         = 'Hdboo'
+  s.version      = '0.0.1'
+  s.author       = 'Iwauo Tajima'
+  s.email        = 'iwauo.tajima@gmail.com'
+  s.homepage     = 'http://sourceforge.jp/projects/hdboo/'
+  s.platform     = Gem::Platform::RUBY
+  s.summary      = 'An HTTP, HTML, relational DataBase, ' +
+                   'and Object-Oriented architecture based web framework.'
+  s.files        = Dir.glob('{lib,docs,test}/**/*')
+  s.require_path = 'lib'
+  s.extensions   = ['lib/hdboo/extconf.rb']
+  s.test_files   = Dir.glob('test/tc_*.rb')
+  s.has_rdoc     = false
+end
diff --git a/lib/hdboo.rb b/lib/hdboo.rb
new file mode 100644 (file)
index 0000000..83f9cd4
--- /dev/null
@@ -0,0 +1,3 @@
+require 'hdboo/base'
+require 'hdboo/sql'
+require 'hdboo/rest'
diff --git a/lib/hdboo/Makefile b/lib/hdboo/Makefile
new file mode 100644 (file)
index 0000000..4d2798c
--- /dev/null
@@ -0,0 +1,157 @@
+
+SHELL = /bin/sh
+
+#### Start of system configuration section. ####
+
+srcdir = .
+topdir = /usr/lib/ruby/1.8/arm-linux-eabi
+hdrdir = $(topdir)
+VPATH = $(srcdir):$(topdir):$(hdrdir)
+exec_prefix = $(prefix)
+prefix = $(DESTDIR)/usr
+sharedstatedir = $(prefix)/com
+mandir = $(prefix)/share/man
+psdir = $(docdir)
+oldincludedir = $(DESTDIR)/usr/include
+localedir = $(datarootdir)/locale
+bindir = $(exec_prefix)/bin
+libexecdir = $(prefix)/lib/ruby1.8
+sitedir = $(DESTDIR)/usr/local/lib/site_ruby
+htmldir = $(docdir)
+vendorarchdir = $(vendorlibdir)/$(sitearch)
+includedir = $(prefix)/include
+infodir = $(prefix)/share/info
+vendorlibdir = $(vendordir)/$(ruby_version)
+sysconfdir = $(DESTDIR)/etc
+libdir = $(exec_prefix)/lib
+sbindir = $(exec_prefix)/sbin
+rubylibdir = $(libdir)/ruby/$(ruby_version)
+docdir = $(datarootdir)/doc/$(PACKAGE)
+dvidir = $(docdir)
+vendordir = $(libdir)/ruby/vendor_ruby
+datarootdir = $(prefix)/share
+pdfdir = $(docdir)
+archdir = $(rubylibdir)/$(arch)
+sitearchdir = $(sitelibdir)/$(sitearch)
+datadir = $(datarootdir)
+localstatedir = $(DESTDIR)/var
+sitelibdir = $(sitedir)/$(ruby_version)
+
+CC = cc
+LIBRUBY = $(LIBRUBY_SO)
+LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
+LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
+LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)-static
+
+RUBY_EXTCONF_H = 
+CFLAGS   =  -fPIC -fno-strict-aliasing -g -g -O2  -fPIC $(cflags) 
+INCFLAGS = -I. -I. -I/usr/lib/ruby/1.8/arm-linux-eabi -I.
+DEFS     = -D_FILE_OFFSET_BITS=64
+CPPFLAGS = -DHAVE_MAGIC_H  -D_FILE_OFFSET_BITS=64 
+CXXFLAGS = $(CFLAGS) 
+ldflags  = -L. -Wl,-Bsymbolic-functions -rdynamic -Wl,-export-dynamic
+dldflags = 
+archflag = 
+DLDFLAGS = $(ldflags) $(dldflags) $(archflag)
+LDSHARED = $(CC) -shared
+AR = ar
+EXEEXT = 
+
+RUBY_INSTALL_NAME = ruby1.8
+RUBY_SO_NAME = ruby1.8
+arch = arm-linux-eabi
+sitearch = arm-linux-eabi
+ruby_version = 1.8
+ruby = /usr/bin/ruby1.8
+RUBY = $(ruby)
+RM = rm -f
+MAKEDIRS = mkdir -p
+INSTALL = /usr/bin/install -c
+INSTALL_PROG = $(INSTALL) -m 0755
+INSTALL_DATA = $(INSTALL) -m 644
+COPY = cp
+
+#### End of system configuration section. ####
+
+preload = 
+
+libpath = . $(libdir)
+LIBPATH =  -L. -L$(libdir)
+DEFFILE = 
+
+CLEANFILES = mkmf.log
+DISTCLEANFILES = 
+
+extout = 
+extout_prefix = 
+target_prefix = /hdboo
+LOCAL_LIBS = 
+LIBS = $(LIBRUBYARG_SHARED) -lmagic  -lpthread -ldl -lcrypt -lm   -lc
+SRCS = _search.c
+OBJS = _search.o
+TARGET = _search
+DLLIB = $(TARGET).so
+EXTSTATIC = 
+STATIC_LIB = 
+
+BINDIR        = $(bindir)
+RUBYCOMMONDIR = $(sitedir)$(target_prefix)
+RUBYLIBDIR    = $(sitelibdir)$(target_prefix)
+RUBYARCHDIR   = $(sitearchdir)$(target_prefix)
+
+TARGET_SO     = $(DLLIB)
+CLEANLIBS     = $(TARGET).so $(TARGET).il? $(TARGET).tds $(TARGET).map
+CLEANOBJS     = *.o *.a *.s[ol] *.pdb *.exp *.bak
+
+all:           $(DLLIB)
+static:                $(STATIC_LIB)
+
+clean:
+               @-$(RM) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES)
+
+distclean:     clean
+               @-$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
+               @-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)
+
+realclean:     distclean
+install: install-so install-rb
+
+install-so: $(RUBYARCHDIR)
+install-so: $(RUBYARCHDIR)/$(DLLIB)
+$(RUBYARCHDIR)/$(DLLIB): $(DLLIB)
+       $(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
+install-rb: pre-install-rb install-rb-default
+install-rb-default: pre-install-rb-default
+pre-install-rb: Makefile
+pre-install-rb-default: Makefile
+$(RUBYARCHDIR):
+       $(MAKEDIRS) $@
+
+site-install: site-install-so site-install-rb
+site-install-so: install-so
+site-install-rb: install-rb
+
+.SUFFIXES: .c .m .cc .cxx .cpp .C .o
+
+.cc.o:
+       $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<
+
+.cxx.o:
+       $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<
+
+.cpp.o:
+       $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<
+
+.C.o:
+       $(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<
+
+.c.o:
+       $(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) -c $<
+
+$(DLLIB): $(OBJS)
+       @-$(RM) $@
+       $(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)
+
+
+
+$(OBJS): ruby.h defines.h
diff --git a/lib/hdboo/_search.c b/lib/hdboo/_search.c
new file mode 100644 (file)
index 0000000..20679e7
--- /dev/null
@@ -0,0 +1,27 @@
+#include <ruby.h>
+#include <rubyio.h>
+#include <magic.h>
+
+static magic_t magic = NULL;
+
+static const char*
+_mime_type(const int file_desc) {
+    magic = magic_open(MAGIC_MIME);
+    magic_load(magic, NULL);
+    return magic_descriptor(magic, dup(file_desc));
+}
+
+static VALUE
+rb_mime_type(VALUE self) {
+    const int file_desc = fileno(RFILE(self)->fptr->f);
+    const char* result = _mime_type(file_desc);
+    if (result == NULL) {
+        rb_raise(rb_eIOError, magic_error(magic));
+    }
+    return rb_str_new2(result);
+}
+
+void
+Init__search() {
+    rb_define_method(rb_cFile, "mime_type", rb_mime_type, 0);
+}
diff --git a/lib/hdboo/base.rb b/lib/hdboo/base.rb
new file mode 100644 (file)
index 0000000..fb6a86e
--- /dev/null
@@ -0,0 +1,621 @@
+require 'monitor'
+require 'jcode'
+require 'stringio'
+require 'base64'
+require 'erb'
+
+module Hdboo
+  class UserError < StandardError
+    attr_reader :messages
+    def initialize(messages)
+      @messages = messages.is_a?(Hash)\
+                ? messages\
+                : {'*' => [messages.to_s]}
+    end
+
+    def message
+      messages.map {|k,v| "#{k}:  #{v.join(', ')}"}.join("\n")
+    end
+    
+    def self.exception(messages)
+      new(messages)
+    end
+  end
+  class InvalidArgument < UserError; end
+  class NoDataFound     < UserError; end
+  class DataConflict    < UserError; end
+  
+  class ::ERB
+    attr_accessor :template_src
+  end
+  
+  class TemplateContext
+    include ERB::Util
+    @base_url = nil
+    
+    def self.base_url=(url)
+      @base_url = url
+    end
+    
+    def self.base_url
+      @base_url
+    end
+    
+    def base_url
+      TemplateContext.base_url
+    end
+    
+    def self.pre_process(src)
+      overwrite_base_url(src)
+    end
+    
+    def self.overwrite_base_url(src)
+      @base_url.nil? \
+        ? src \
+        : src.sub(/(\<base\s+href\s*=\s*['"])(.*)(['"])/) do 
+            $1 + @base_url + $3
+          end
+    end
+    
+    def get_binding
+      binding
+    end
+    
+    def base64(data)
+      Base64.encode64(data).gsub(/\n/, '')
+    end
+    alias_method :b, :base64
+    
+    def multi_line_markup(str)
+      h(str).gsub(/\n/, '<br/>')
+    end
+    alias_method :m, :multi_line_markup
+  end
+
+  def template(*args, &block)
+    Hdboo.template(*args, &block)
+  end
+  
+  def self.template(template_name, &block)
+    template = resource(template_name) do |src|
+      src = TemplateContext.pre_process(src)
+      erb = ERB.new(src)
+      erb.template_src = src
+      erb.filename = template_name
+      erb
+    end
+    context = TemplateContext.new
+    context.instance_eval(&block) if block_given?
+    template.result(context.get_binding)
+  end
+  
+  def resource_exist?(*args)
+    Hdboo.resource_exist?(*args)
+  end
+  
+  def self.resource_exist?(resource_name)
+    search_in_lib_path(resource_name) do
+      return true
+    end
+    false
+  end
+  
+  def resource(*args)
+    Hdboo.resource(*args)
+  end
+  
+  def self.resource(resource_name, reload=false, &block)
+    if !reload && @resource_cache.has_key?(resource_name)
+      return @resource_cache[resource_name]
+    end
+    @resource_cache.synchronize do
+      break if !reload && @resource_cache.has_key?(resource_name)
+      result = resource_nocache(resource_name, &block)
+      @resource_cache[resource_name] = result
+    end
+    @resource_cache[resource_name]
+  end
+  @resource_cache = {}
+  @resource_cache.extend(MonitorMixin)
+  
+  def resource_nocache(*args)
+    Hdboo.resource_nocache(*args)
+  end
+  
+  def self.resource_nocache(resource_name, &block)
+    result = nil
+    found  = false
+    search_in_lib_path(resource_name) do |path|
+      result = open(path).read
+      found  = true
+      break
+    end
+    unless found
+      raise RuntimeError, "no such resource: #{resource_name}", caller[2..-1]
+    end
+    if block_given?
+      result = block.call(result)
+    end
+    result
+  end
+  
+  def self.search_in_lib_path(resource_name, &block)
+    rel_path = File.join(*resource_name.split('/'))
+    $:.each do |lib_path|
+      path = File.join(lib_path, rel_path)
+      if File.exists?(path)
+        block.call(path)
+      end
+    end
+  end
+  
+  def escape_html(hash)
+#TODO
+    require 'cgi'
+    result = {}
+    hash.each do |key, val|
+      result[key.to_s] = CGI.escapeHTML(val.to_s)
+      if (val == true || val == 1)
+        result[key.to_s.sub(/^is_/, '') + '_is_true'] = val
+      end
+    end
+    result
+  end
+  module_function :escape_html
+
+  def uncamel(str)
+    words = []
+    str[0] = str[0..0].upcase
+    str.scan(UNCAMEL_REGEX) do |match|
+      word = match[0]
+      word = (word.upcase == word) ? word : word.downcase
+      words << word
+    end
+    words.join('_')
+  end
+  UNCAMEL_REGEX = /([A-Z]{2,}|[A-Z][a-z0-9]+)/
+  module_function :uncamel
+
+  def singleton_eval(clazz, code=nil, &block)
+    clazz.module_eval do
+      singleton_class = (class << self; self; end)
+      if code
+        singleton_class.module_eval(code)
+      else
+        singleton_class.module_eval(&block)
+      end
+    end
+  end
+  module_function :singleton_eval
+
+  def normalize_key(hash)
+    result = {}
+    hash.each do |k, v|
+      result[k.to_sym] = v
+    end
+    result
+  end
+  module_function :normalize_key
+  
+  class HashAsObject < Hash
+    def initialize(kargs={})
+      replace(Hdboo.normalize_key(kargs))
+    end
+    
+    def method_missing(name)
+      org_arg = name
+      returns_bool = (name.to_s =~ /^(.+?)\?$/)
+      if !has_key?(name) && returns_bool
+        name = has_key?($1) ? $1 : ('is_' + $1)
+      end
+      
+      unless has_key?(name)
+        raise NoMethodError, "undefined method: #{org_arg}", caller
+      end
+      
+      result = self[name]
+      if returns_bool
+        return (result == 0) ? false : result
+      end
+      return result
+    end
+    
+    def [](key)
+      super(key.to_sym)
+    end
+    
+    def []=(key, value)
+      super(key.to_sym, value)
+    end
+    
+    def has_key?(key)
+      super(key.to_sym)
+    end
+  end
+
+  module Validatable
+    def validators_for(key)
+      raise "#{self.class.name}#validators_for(key)? must be implemented."
+    end
+    
+    def invalid?(values=nil)
+      values = values || self.to_hash
+      result = {}
+      values.each do |key, value|
+        key = key.to_sym
+        validators = Validator::ValidatorList.new(validators_for(key))
+        if (errors = validators.invalid?(value, key))
+          result.merge!(errors)
+        end
+      end
+      result.empty? ? nil\
+                    : result
+    end
+    
+    def validate(values=nil)
+      if errors = invalid?(values)
+        raise InvalidArgument, errors
+      end
+      return self
+    end
+  end
+  
+  module Validator
+    def validate(val, key='*')
+      if errors = invalid?(val, key)
+        raise InvalidArgument, errors
+      end
+    end
+    
+    def pass?(val)
+      raise "#{self.class.name}#pass? must be implemented."
+    end
+    
+    def invalid?(val, key='*')
+      key = key.to_sym
+      pass?(val) ? nil\
+                 : {key=>[message(val)]}
+    end
+    
+    def message
+      raise "#{self.class.name}#message must be implemented."
+    end
+    
+    class ValidatorList < Array
+      include Validator
+      def initialize(validators=[])
+        replace(validators)
+      end
+      
+      def invalid?(val, key='*')
+        errors = []
+        key = key.to_sym
+        self.each do |validator|
+          errors << validator.message(val) unless validator.pass?(val)
+        end
+        errors.empty? ? nil \
+                      : {key => errors}
+      end
+      
+      def pass?
+        self.all?{|all| all.pass?(vall)}
+      end
+      
+      def message(val)
+        errors = invalid?(val)
+        errors ? errors.join(', ')\
+               : ''
+      end
+    end
+
+    class NotNil
+      include Validator
+      def pass?(val)
+        !val.nil?
+      end
+      
+      def message(val)
+        "must not be nil."
+      end
+    end
+    
+    class IsA
+      include Validator
+      def initialize(clazz)
+        @clazz = clazz
+      end
+
+      def pass?(val)
+        val.nil? || val.is_a?(@clazz)
+      end
+      
+      def message(val)
+        "must be instance of #{@clazz.name}, but of #{val.class.name}." 
+      end
+    end
+    
+    class NotBlank
+      include Validator
+      def pass?(val)
+        val.nil? || val.to_s.length > 0
+      end
+      
+      def message(val)
+        "must not be blank."
+      end
+    end
+    
+    class Bool
+      include Validator
+      def pass?(val)
+        if val.nil? || val.is_a?(TrueClass) || val.is_a?(FalseClass)
+          return true
+        end
+        if (val == 0 || val == 1)
+          return true
+        end
+        false
+      end
+      
+      def message(val)
+        "must be '0', '1', true or false, but was #{val}."
+      end
+    end
+    
+    class MaxLength
+      include Validator
+      def initialize(max)
+        @max = max
+      end
+      
+      def pass?(val)
+        if val.respond_to?(:jlength)
+          return val.jlength <= @max
+        elsif val.respond_to?(:length)
+          return val.length <= @max
+        end
+        true
+      end
+      
+      def message(val)
+        length = val.respond_to?(:jlength) ? val.jlength : val.length
+        "must not be longer than #{@max}, but was #{length}."
+      end
+    end
+    
+    class MinLength
+      include Validator
+      def initialize(min)
+        @min = min
+      end
+      
+      def pass?(val)
+        if val.respond_to?(:jlength)
+          return val.jlength == 0 || val.jlength >= @min
+        elsif val.respond_to?(:length)
+          return val.length == 0 || val.length >= @min
+        end
+        true
+      end
+      
+      def message(val)
+        length = val.respond_to?(:jlength) ? val.jlength : val.length
+        "must not be shorter than #{@min} but was #{length}."
+      end
+    end
+    
+    class In
+      include Validator
+      def initialize(range)
+        @range = range
+      end
+      
+      def pass?(val)
+        val.nil? || @range.include?(val)
+      end
+      
+      def message(val)
+        "must be in range #{@range}, but was #{val}."
+      end
+    end
+    
+    class Max
+      include Validator
+      def initialize(max)
+        @max = max.to_i
+      end
+      
+      def pass?(val)
+        val.nil? || @max >= val.to_i
+      end
+      
+      def message(val)
+        "must not be greater than #{@max.to_s}, but was #{val.to_s}."
+      end
+    end
+    
+    class Min
+      include Validator
+      def initialize(min)
+        @min = min.to_i
+      end
+      
+      def pass?(val)
+        val.nil? || @min <= val.to_i
+      end
+      
+      def message(val)
+        "must not be smaller than #{@min.to_s}, but was #{val.to_s}."
+      end
+    end
+    
+    class RegexAssertion
+      include Validator
+      def pass?(val)
+        if val.nil? || val.to_s.length == 0
+          return true
+        end
+        return val =~ regex
+      end
+      
+      def regex
+        raise 'RegexAssertion#regex must be implemented.'
+      end
+      
+      def message(val)
+        "must be #{regex.to_s}, but was #{val}."
+      end
+      
+      def self.regex_for_unicode_code_point_range(code_point_ranges)
+        character_class = code_point_ranges.collect do |range|
+          first_glyph = [range.first.to_i(16)].pack('U')
+          last_glyph  = [range.last.to_i(16)].pack('U')
+          "#{first_glyph}-#{last_glyph}"
+        end.join
+        /^[#{character_class}]+$/x
+      end
+    end
+    
+    class Match < RegexAssertion
+      attr_reader :regex
+      def initialize(regex)
+        @regex = Regexp.compile(regex)
+      end
+    end
+    
+    class Printable < RegexAssertion
+      CHARACTER_CLASS = %q/\x21-\x7E/
+      REGEX = /^[#{CHARACTER_CLASS}]+$/x
+      def regex
+        REGEX
+      end
+    end
+    
+    class Word < RegexAssertion
+      CHARACTER_CLASS = %q/0-9a-zA-Z_/
+      REGEX = /^[#{CHARACTER_CLASS}]+$/x
+      def regex
+        REGEX
+      end
+    end
+    
+    class Hiragana < RegexAssertion
+      REGEX = self.regex_for_unicode_code_point_range(['3040'..'309F'])
+      def regex
+        REGEX
+      end
+    end
+    
+    class Katakana < RegexAssertion
+      REGEX = self.regex_for_unicode_code_point_range(
+                ['30A0'..'30FF', '31F0'..'31FF']        
+              )
+      def regex
+        REGEX
+      end
+    end
+    
+    class Kanji < RegexAssertion
+      REGEX = self.regex_for_unicode_code_point_range([
+                'F900'..'FAFF', # cjk 互換漢字
+                '4E00'..'9FFF', # cjk 統合漢字
+                '3400'..'4DBF'  # cjk 統合漢字拡張A
+              ])
+      def regex
+        REGEX
+      end
+    end
+    
+    class Kana < RegexAssertion
+      REGEX = self.regex_for_unicode_code_point_range([
+                '3040'..'309F', #ひらがな
+                '30A0'..'30FF', '31F0'..'31FF' #カタカナ
+              ])
+      def regex
+        REGEX
+      end      
+    end
+    
+    class KanaKanji < RegexAssertion
+      REGEX = self.regex_for_unicode_code_point_range([
+                '3040'..'309F', #ひらがな
+                '30A0'..'30FF', '31F0'..'31FF', #カタカナ
+                'F900'..'FAFF', # cjk 互換漢字
+                '4E00'..'9FFF', # cjk 統合漢字
+                '3400'..'4DBF'  # cjk 統合漢字拡張A
+              ])
+      def regex
+        REGEX
+      end
+      
+      def message(val)
+        "must be japanese kana or kanji, but was #{val}."
+      end
+    end
+    
+    class DomainName < RegexAssertion
+      label_lower   = %q/[a-z]+(?:[0-9a-z\-]*[a-z0-9]+)*/
+      label_upper   = label_lower.gsub(/a-z/, 'A-Z')
+      REGEX_STR     = %Q/(?:
+                        #{label_lower}(?:\\.#{label_lower})*
+                        |
+                        #{label_upper}(?:\\.#{label_upper})*
+                      )/
+      REGEX = /^#{REGEX_STR}$/x
+      
+      def regex
+        REGEX
+      end
+      
+      def message(val)
+        "must be valid domain name, but was #{val}."
+      end
+    end
+    
+    class MailAddress < RegexAssertion
+      atom          = %q/a-zA-Z0-9!#$%&'*+\-\/=?^_`{\|}~/
+      dot_atom      = %Q/[#{atom}]+(?:\\.[#{atom}]+)*/
+      qcontent      = %q/\x21\x23-\x5B\x5D-\x7E/  # not including '\' and '"'
+      quoted_string = %Q/"[ #{qcontent}]*"/
+      local_part    = %Q/(?:
+                        #{dot_atom}(?=@)
+                        |
+                        #{quoted_string}(?=@)
+                      )/
+      domain_name   = DomainName::REGEX_STR
+      REGEX_STR     = "#{local_part}@#{domain_name}"
+      REGEX         = /^#{REGEX_STR}$/x
+      
+      def regex
+        REGEX
+      end
+      
+      def message(val)
+        "must be valid mail address, but was #{val}."
+      end
+    end
+    
+    class ZipCode < RegexAssertion
+      def regex
+        /[\d]{7}/
+      end
+      
+      def message(val)
+        "must be 7 digits, but was #{val}."
+      end
+    end
+    
+    class URL < RegexAssertion
+      domain_name = DomainName::REGEX_STR
+      REGEX = /^https?\:\/\/#{domain_name}/x
+      
+      def regex
+        REGEX
+      end
+      
+      def message(val)
+        "must be valid url, but was #{val}."
+      end
+    end
+  end
+end
diff --git a/lib/hdboo/crawler.rb b/lib/hdboo/crawler.rb
new file mode 100644 (file)
index 0000000..42869c8
--- /dev/null
@@ -0,0 +1,58 @@
+require 'open-uri'
+
+module Hdboo
+  class Crawler
+    public
+    def initialize(kargs={})
+    end
+
+    public
+    def crawl(*path_list, &action)
+      @path_stack = []
+      stack_path(path_list)
+      until @path_stack.empty? do
+        path = @path_stack.pop
+        process(path, &action)
+      end
+      self
+    end
+
+    protected
+    def stack_path(path_list)
+      @path_stack.push(*path_list.compact.uniq)
+    end
+
+    protected
+    def process(path, &action)
+      raise "#{self.class.name}#process_path(path, &action) must be implemented."
+    end
+  end
+
+  class FileSystemCrawler < Crawler
+    protected
+    def stack_path(path_list)
+      path_list = path_list.map do |path|
+        Dir.glob(path)
+      end
+      path_list.flatten!
+      path_list.compact!
+      super(path_list)
+    end
+
+    protected
+    def process(path, &action)
+      if File.file?(path)
+        open(path, &action)
+      end
+    end
+  end
+
+  class WebCrawler < Crawler
+    def process(path, &action)
+      open(path, &action)
+    end
+  end
+
+  class DatabaseCarwler
+  end
+end
diff --git a/lib/hdboo/database.rb b/lib/hdboo/database.rb
new file mode 100644 (file)
index 0000000..3079878
--- /dev/null
@@ -0,0 +1,160 @@
+require 'time'
+require 'mysql'
+require 'hdboo/base'
+require 'hdboo/sql'
+
+module Hdboo
+class Database
+  ADMINISTRATIVE_DB_NAME = 'mysql'
+  HOST = 'localhost'
+  USER = 'root'
+  PASSWORD = ''
+  PORT = nil
+  SOCKET = '/var/run/mysqld/mysqld.sock'
+  #SOCKET = '/tmp/mysql.sock'
+  
+  @instances = Hash.new do |hash, dbname|
+    hash[dbname] = new(dbname)
+  end
+  
+  def self.[](dbname)
+    @instances[dbname.to_sym]
+  end
+  
+  attr_reader :name
+  private
+  def initialize(name)
+    @name = name
+    ObjectSpace.define_finalizer(self, lambda{self.close})
+  end
+  
+  public
+  def connect(dbname=@name)
+    unless @conn
+      @conn = Mysql.connect(
+        HOST,USER, PASSWORD, dbname.to_s, PORT, SOCKET,
+        Mysql::CLIENT_MULTI_RESULTS 
+      )
+      @conn.reconnect = true
+    end
+    self
+  end
+  
+  def transaction(&block)
+    connect unless @conn
+    @conn.autocommit(false)
+    begin
+      Thread.current[:rdboo_transaction_on_thread] = @conn
+      result = block.call
+      Database.raise_error_if_it_occur
+      @conn.commit
+      return result
+    rescue
+      begin
+        @conn.rollback
+      rescue
+        #nothing todo
+      end
+      raise $!
+    ensure
+      Thread.current[:rdboo_transaction_on_thread] = nil
+    end
+    self
+  end
+  
+  def execute(sql_script)
+    ddl_file_name = "#{name}.ddl"
+    File.open(ddl_file_name, 'w+') do |file|
+      file.puts "CREATE DATABASE IF NOT EXISTS #{name} CHARACTER SET utf8;"
+      file.puts "USE #{name};"
+      file.puts sql_script
+    end
+    err = nil
+    open("|mysql -u #{USER} < #{ddl_file_name}") do |pipe|
+    end
+    self
+  end
+  
+  def self.call_procedure(procedure_name, args, &block)
+    args = args.collect {|arg| sql_value_of(arg)}
+    sql_script = "CALL #{procedure_name} (#{args.join(',')})"
+    query(sql_script, &block) 
+  end
+  
+  def self.query(sql_script, &block)
+    unless conn = Thread.current[:rdboo_transaction_on_thread]
+      raise 'No transaction has been started.'
+    end
+#$stderr << sql_script.inspect
+    result_set_list = []
+    conn.query(sql_script) do |rows|
+      raise_error_if_it_occur
+      result_set = SQL::ResultSet.new
+      rows.each do |row|
+        row_hash = SQL::Record.new
+        row.each_with_index do |col_value, col_index|
+          field = rows.fetch_field_direct(col_index)
+          row_hash[field.name.to_sym] = rb_value_of(col_value, field)
+        end
+        result_set << row_hash
+      end
+#$stderr << 'result_set ==> ' + result_set.inspect
+      result_set_list << result_set
+    end
+    
+    if block_given?
+      result_set_list.each do |result_set|
+        block.call(result_set)
+      end
+    end
+    result_set_list.last || SQL::ResultSet.new
+  end
+  
+  def self.sql_value_of(val)
+    case val
+      when NilClass   then "NULL"
+      when TrueClass  then "1"
+      when FalseClass then "0"
+      when String     then '"' + Mysql.quote(val) + '"'
+      when Array      then '"' + val.map{|v| Mysql.quote(v)}.join(',') + '"'
+      when Time       then val.strftime('%Y-%m-%d %H:%M:%S').inspect
+      else val.inspect
+    end
+  end
+  
+  def self.rb_value_of(val, field)
+    return nil if val.nil?
+    case field.type
+    when Mysql::Field::TYPE_DECIMAL, Mysql::Field::TYPE_TINY,
+         Mysql::Field::TYPE_SHORT,   Mysql::Field::TYPE_LONG,
+         Mysql::Field::TYPE_LONGLONG
+      val.to_i
+    when Mysql::Field::TYPE_FLOAT,   Mysql::Field::TYPE_DOUBLE
+      val.to_f
+    when 16 #Mysql::Field::TYPE_BIT
+      val == "\x00" ? false : true
+    when Mysql::Field::TYPE_STRING, Mysql::Field::TYPE_VAR_STRING,
+         Mysql::Field::TYPE_CHAR
+      val.to_s
+    when Mysql::Field::TYPE_BLOB
+      val
+    when Mysql::Field::TYPE_DATETIME, Mysql::Field::TYPE_TIMESTAMP
+      Time.xmlschema(val.sub(/\s/, 'T'))
+    else raise TypeError, "Type: #{field.type}, Value: #{val}", caller
+    end
+  end
+  
+  def self.raise_error_if_it_occur
+    conn = Thread.current[:rdboo_transaction_on_thread]
+    if conn.errno > 0
+      raise Mysql::Error, conn.errno, conn.error
+    end
+  end
+  
+  def close
+    @conn.close if @conn
+    @conn = nil
+    self
+  end
+end
+end
diff --git a/lib/hdboo/extconf.rb b/lib/hdboo/extconf.rb
new file mode 100644 (file)
index 0000000..63dda0f
--- /dev/null
@@ -0,0 +1,5 @@
+require 'mkmf'
+unless have_header('magic.h') && have_library('magic', 'magic_open')
+  raise 'libmagic-dev must be installed.'
+end
+create_makefile('hdboo/_search')
diff --git a/lib/hdboo/html.rb b/lib/hdboo/html.rb
new file mode 100644 (file)
index 0000000..88467c0
--- /dev/null
@@ -0,0 +1,63 @@
+require 'hdboo/base'
+
+module Hdboo::HTML
+  INDENTATION_STR = '  '
+  def self.indent(lines, depth=1)
+    unless lines.is_a?(Array)
+      lines = lines.to_s.split("\n")
+    end
+    lines.map!{|line| (INDENTATION_STR * depth) + line}
+  end
+end
+
+class String
+  def indent(depth)
+    Hdboo::HTML.indent(self, depth).join("\n")
+  end
+end
+
+class Array
+  def indent(depth)
+    Hdboo::HTML.indent(self, depth)
+  end
+  
+  def to_html(profile=nil, depth=0)
+    buff = []
+    buff << '<ol>'.indent(depth)
+    each do |item|
+      buff.concat([
+        '<li>'.indent(depth+1),
+          item.to_html(profile, depth+2),
+        '</li>'.indent(depth+1)
+      ])
+    end
+    buff << '</ol>'.indent(depth)
+    buff.join("\n")
+  end
+end
+
+class Object
+  def to_html(profile=nil, depth=0)
+    respond_to?(:to_hash) ? to_hash.to_html(profile, depth)\
+                          : to_s.indent(depth)
+  end
+end
+
+class Hash
+  def to_html(profile=nil, depth=0)
+    buff = []
+    buff << '<dl>'.indent(depth)
+    each do |key, val|
+      buff.concat([
+        '<dt>'.indent(depth+1),
+          key.to_html(profile, depth+2),
+        '</dt>'.indent(depth+1),
+        '<dd>'.indent(depth+1),
+          val.to_html(profile, depth+2),
+        '</dd>'.indent(depth+1)
+      ])
+    end
+    buff << '</dl>'.indent(depth)
+    buff.join("\n")
+  end
+end
diff --git a/lib/hdboo/mkmf.log b/lib/hdboo/mkmf.log
new file mode 100644 (file)
index 0000000..38548b3
--- /dev/null
@@ -0,0 +1,34 @@
+have_header: checking for magic.h... -------------------- yes
+
+"cc -E -I. -I/usr/lib/ruby/1.8/arm-linux-eabi -I.  -D_FILE_OFFSET_BITS=64  -fno-strict-aliasing -g -g -O2  -fPIC    conftest.c -o conftest.i"
+checked program was:
+/* begin */
+1: #include <magic.h>
+/* end */
+
+--------------------
+
+have_library: checking for magic_open() in -lmagic... -------------------- yes
+
+"cc -o conftest -I. -I/usr/lib/ruby/1.8/arm-linux-eabi -I.  -D_FILE_OFFSET_BITS=64  -fno-strict-aliasing -g -g -O2  -fPIC   conftest.c  -L. -L/usr/lib -L. -Wl,-Bsymbolic-functions -rdynamic -Wl,-export-dynamic     -lruby1.8-static -lmagic  -lpthread -ldl -lcrypt -lm   -lc"
+conftest.c: In function ‘t’:
+conftest.c:3: error: ‘magic_open’ undeclared (first use in this function)
+conftest.c:3: error: (Each undeclared identifier is reported only once
+conftest.c:3: error: for each function it appears in.)
+checked program was:
+/* begin */
+1: /*top*/
+2: int main() { return 0; }
+3: int t() { void ((*volatile p)()); p = (void ((*)()))magic_open; return 0; }
+/* end */
+
+"cc -o conftest -I. -I/usr/lib/ruby/1.8/arm-linux-eabi -I.  -D_FILE_OFFSET_BITS=64  -fno-strict-aliasing -g -g -O2  -fPIC   conftest.c  -L. -L/usr/lib -L. -Wl,-Bsymbolic-functions -rdynamic -Wl,-export-dynamic     -lruby1.8-static -lmagic  -lpthread -ldl -lcrypt -lm   -lc"
+checked program was:
+/* begin */
+1: /*top*/
+2: int main() { return 0; }
+3: int t() { magic_open(); return 0; }
+/* end */
+
+--------------------
+
diff --git a/lib/hdboo/rest.rb b/lib/hdboo/rest.rb
new file mode 100644 (file)
index 0000000..99b6d3d
--- /dev/null
@@ -0,0 +1,607 @@
+require 'cgi'
+require 'tempfile'
+require 'digest/md5'
+require 'fcgi'
+require 'json'
+require 'hdboo/html'
+
+module Hdboo
+module REST
+  class Server
+    class Request
+      attr_reader :params, :env
+      def initialize(params, env, server)
+        @params = params
+        @env    = env
+        @server = server
+      end
+      
+      def secure?
+        server_name = @env['SERVER_NAME']
+        if server_name =~ /^(localhost|192\.168\.\d{1,3}.\d{1,3}|127\.0\.0\.1)$/
+          return true
+        end
+        @env['SERVER_PORT'].to_i == 443
+      end
+      
+      def secure_url
+        server_name = @env['SERVER_NAME'] || ''
+        script_name = @env['SCRIPT_NAME'] || ''
+        'https://' + uri
+      end
+      
+      def uri
+        server_name = @env['SERVER_NAME'] || ''
+        script_name = @env['SCRIPT_NAME'] || ''
+        server_name + script_name
+      end
+      
+      def remote_user
+        result = @env['REMOTE_USER'] || @env['HTTP_AUTHORIZATION_USERNAME']
+        if result.nil? || result.length == 0
+          return nil
+        end
+        return result
+      end
+      
+      def authenticated?
+        return false unless remote_user
+        authenticated_as?(remote_user)
+      end
+      
+      def authenticated_as?(username)
+        ha1 = @server.password_digest(username)
+        unless ha1 && ha1.length == 32
+          return false #unknown user
+        end
+        realm   = @server.base_realm
+        nonce   = @env['HTTP_AUTHORIZATION_NONCE'] || ''
+        qop     = 'auth'  #"auth-int" isn't supported
+        nc      = @env['HTTP_AUTHORIZATION_NC'] || ''
+        cnonce  = @env['HTTP_AUTHORIZATION_CNONCE'] || ''
+        response= @env['HTTP_AUTHORIZATION_RESPONSE'] || ''
+        method  = @env['REQUEST_METHOD'] || ''
+        path    = @env['HTTP_AUTHORIZATION_URI'] || ''
+        ha2     = Digest::MD5::hexdigest(method + ':' + path)
+        answer  = Digest::MD5::hexdigest([
+          ha1, nonce, nc, cnonce, qop, ha2
+        ].join(':'))
+        response == answer
+      end
+    end
+    
+    module Response
+      attr_reader :status, :header
+      def initialize(status, body=nil, header={})
+        @status = status
+        @body   = body
+        @header = {'status'=>status.to_s}.merge(header)
+      end
+      
+      def body
+        case header['type']
+        when 'application/json' then json
+        when 'text/html'        then html
+        else @body
+        end
+      end
+      
+      def html
+        buff = []
+        unless has_head_tag? 
+          status_text = self.class.name.split('::').last
+          buff.concat([
+            "<head>",
+            "  <title>#{status.to_s}: #{status_text}</title>",
+            "</head>"
+          ])
+        end
+        buff.concat(
+          has_body_tag?\
+            ? [@body.to_html]\
+            : ['<body>',
+                 @body.to_html(nil, 1),
+               '</body>']
+        )
+        buff.join("\n")
+      end
+      
+      def has_head_tag?
+        @body.is_a?(String) && @body =~ /<head(\b|>)/
+      end
+      
+      def has_body_tag?
+        @body.is_a?(String) && @body =~ /<body(\b|>)/
+      end
+      
+      def json
+        @body.to_json
+      end
+      
+      class Success
+        include Response
+      end
+      
+      def OK(body, header={})
+        header = header.is_a?(String)\
+               ? {'type' => header}\
+               : header
+        Success.new(200, body, header)
+      end
+      
+      def Created(body, header={})
+        Success.new(201, body, header)
+      end
+      
+      def Accepted(body, header={})
+        Success.new(202, body, header)
+      end
+      
+      class Redirection
+        include Response
+        def initialize(status, uri)
+          super(status, uri, header={'Location' => uri})
+        end
+      end
+      
+      def MovedPermanently(uri)
+        Redirection.new(301, uri)
+      end
+      
+      def SeeOther(uri)
+        Redirection.new(303, uri)
+      end
+      
+      def TemporaryRedirect(uri)
+        Redirection(306, uri)
+      end
+      
+      class ClientSideError < UserError
+        include Response
+        def self.exception(messages=nil)
+          self.new(messages)
+        end
+
+        def initialize(messages)
+          @status = status
+          @body   = messages
+          @header = {'status'=>status.to_s}
+        end
+
+        def status
+          self.class.status
+        end
+        
+        def status_text
+          self.class.status_text
+        end
+      end
+      
+      class BadRequest < ClientSideError
+        def self.status
+          400
+        end
+      end
+      
+      class Unauthorized < ClientSideError
+        def self.base_realm=(realm)
+          @base_realm = realm
+        end
+        
+        def self.base_realm
+          @base_realm
+        end
+        
+        def self.status
+          401
+        end
+
+        def add_authenticate_header(env)
+          nc = env['HTTP_AUTHORIZATION_NC'].to_i
+          authenticate_header = nil
+          if nc == 1
+            authenticate_header = nil
+          else
+            nonce  = Digest::MD5.hexdigest(Time.now.to_f.to_s + 'nonce')
+            opaque = Digest::MD5.hexdigest(Time.now.to_f.to_s + 'opaque')
+            stale = (nc==0) ? 'FALSE' : 'TRUE'
+            authenticate_header = 'Digest ' + [
+              %Q/realm="#{Unauthorized.base_realm}"/,
+              %Q/nonce="#{nonce}"/,
+              %Q/algorithm=MD5/,
+              %Q/qop=auth/,
+              %Q/opaque=#{opaque}/,  
+              %Q/stale=#{stale}/
+            ].join(', ')
+          end
+          
+          if authenticate_header
+            @header['WWW-Authenticate'] = authenticate_header
+          end
+          self
+        end
+      end
+      
+      class Forbidden < ClientSideError
+        def self.status
+          403
+        end
+      end
+      
+      class NotFound < ClientSideError
+        def self.status
+          404
+        end
+      end
+      
+      class MethedNotAllowed < ClientSideError
+        def self.status
+          405
+        end
+      end
+      
+      class Conflict < ClientSideError
+        def self.status
+          409
+        end
+      end
+      
+      class ServerSideError < StandardError
+        include Response
+        def self.exception(message='')
+          self.new(message)
+        end
+        
+        def self.status
+          500
+        end
+        
+        def initialize(message)
+          @status = status
+          @body   = message + "\n" + backtrace.join("\n")
+          @header = {'status'=>status.to_s}
+        end
+        
+        def status
+          self.class.status          
+        end
+      end
+      
+      class NotImplemented < ServerSideError
+        def self.status
+          501
+        end
+      end
+      
+      class ServiceUnavailable < ServerSideError
+        def self.status
+          503
+        end
+      end
+    end
+
+    class RouteConflictionError < StandardError; end
+      
+    include Hdboo
+    include Response
+    
+    def initialize
+    end
+    
+    def inspect
+      self.class.name + "\n" + @routes.inspect
+    end
+    
+    def base_realm(realm=nil)
+      if realm
+        Unauthorized.base_realm = realm
+      end
+      Unauthorized.base_realm
+    end
+    
+    def base_url(url=nil)
+      if url
+        TemplateContext.base_url = url
+      end
+      url
+    end
+    
+    def respond_to(uri_template, &block)
+      @routes = URISegment.new unless @routes
+      method, route = URISegment.parse_route(uri_template)
+      @routes.attach_action(route, method, &block)
+      self
+    end
+    
+    def before_process(uri_template, &block)
+      @routes = URISegment.new unless @routes
+      method, route = URISegment.parse_route(uri_template)
+      @routes.attach_before_filter(route, method, &block)
+    end
+    
+    def after_process(uri_template, &block)
+      @routes = URISegment.new unless @routes
+      method, route = URISegment.parse_route(uri_template)
+      @routes.attach_after_filter(route, method, &block)
+    end
+    
+    def clear_routes
+      @routes = URISegment.new
+      self
+    end
+    
+    def run
+      FCGI.each_cgi do |cgi|
+        do_cgi(cgi)
+      end
+    end
+    
+    def test_run(env_table, params={})
+      cgi = CGIStub.new(env_table, params)
+      pipe = IO.popen('-')
+      if pipe
+        Process.wait
+        pipe.gets(nil)
+      else
+        begin
+          do_cgi(cgi)
+        rescue Exception
+          $stderr << [$!.message, $!.backtrace].join("\n")
+        ensure
+          exit
+        end
+      end
+    end
+    
+    private
+    def do_cgi(cgi)
+      method = cgi.request_method
+      route  = cgi.script_name
+      action, bound_kargs = @routes.find_action(route, method)
+      unless action
+        raise NotFound, "Not Found : #{route} (#{method})"
+      end
+      params = pack_query(cgi.params).merge!(bound_kargs)
+      env    = cgi.env_table
+      request = Request.new(params, env, self)
+      response = action.call(request)
+      unless response.is_a?(Response)
+        response = OK(response)
+      end
+    rescue Unauthorized
+      response = $!
+      response.add_authenticate_header(env)
+    rescue ClientSideError
+      response = $!
+    rescue NoDataFound
+      response = NotFound.new($!.messages)
+    rescue DataConflict
+      response = Conflict.new($!.messages)
+    rescue InvalidArgument
+      response = BadRequest.new($!.messages)
+    rescue ServerSideError
+      response = $!
+      $stderr << response.body
+    rescue
+      error_message = [$!.message, $!.backtrace].join("\n")
+      $stderr << error_message
+      response = ServerSideError.new(error_message)
+    ensure
+      unless response.header.has_key?('type')
+        response.header['type'] = default_content_type(cgi.accept)
+      end
+      cgi.out(response.header) do
+        response.body
+      end
+    end
+    
+    def default_content_type(http_accept)
+      if http_accept && http_accept.include?('application/json')
+        'application/json'
+      else
+        'text/html'
+      end
+    end
+    
+    def pack_query(params)
+      result = HashAsObject.new
+      params.each do |key, value|
+        next unless key && key.to_s.length > 0
+        if value.is_a?(Array)
+          value.collect! {|val| unescape(val)}
+          result[key] = (value.length == 1)\
+                      ? value[0]\
+                      : value      
+        else
+          result[key] = unescape(value)
+        end
+      end
+      result
+    end
+    
+    def unescape(value)
+      case value
+      when String then CGI.unescape(value)
+      when StringIO, Tempfile
+        if value.content_type =~ /^image\//
+          value
+        else
+          CGI.unescape(value.read)
+        end
+      else value
+      end
+    end
+    
+    class CGIStub < ::CGI
+      attr_accessor :env_table, :params
+      def initialize(env_table, params)
+        @env_table, @params = env_table, params
+      end
+
+      def method_missing(attr)
+        attr = attr.to_s.upcase
+        @env_table[attr] || @env_table['HTTP_' + attr]
+      end
+    end
+        
+    class URISegment
+      def self::parse_route(uri_template)
+        method, path= uri_template.scan(/
+          (GET|PUT|POST|DELETE|HEAD|OPTION) #http method
+          \s*
+          (
+            (?:
+              \/[\.\w]+   # segment_name
+              |
+              \/\{[\w]+\} # {variable_name}
+            )+
+            \/?           # terminator
+          )
+        /x)[0]
+        unless method && path
+          raise ArgumentError, uri_template
+        end
+        result = path.gsub(/^\/|\/$/, '').split('/')
+        result.collect! {|segment| URISegment.new(segment)}
+        return [method, result]
+      end
+      
+      def walk_along(route, segment_name='', depth=0, &block)
+        if route.is_a?(String)
+          route = route.gsub(/^\/|\/$/, '')\
+                       .split('/')\
+                       .collect! {|seg| URISegment.new(seg)}
+        end
+        
+        result = block.call(self, segment_name, route.length != 0, depth)
+        
+        next_segment = route.shift
+        return result unless next_segment
+        
+        child = self[next_segment.name]
+        return result unless child
+        
+        child.walk_along(route,
+                         next_segment.name,
+                         depth+1,
+                         &block)
+      end
+      
+      def find_action(route, method)
+        method = method.to_sym
+        before_filter = []
+        after_filter  = []
+        bound_kargs   = {}
+        action = nil
+        
+        walk_along(route) do |segment, segment_name, has_next|
+          before_filter.concat(segment.before_filter[method])
+          after_filter.concat(segment.after_filter[method])
+          
+          if segment.variable?
+            bound_kargs[segment.name.to_sym] = CGI.unescape(segment_name)
+          end
+          
+          unless has_next
+            unless segment.action.has_key?(method.to_sym)
+              raise Server::MethedNotAllowed,
+                    "#{segment.name} : #{method}"
+            end  
+            action = lambda do |request|
+                       before_filter.each do |filter|
+                         filter.call(request)
+                       end
+                       response = segment.action[method].call(request)
+                       after_filter.each do |filter|
+                         filter.call(response)
+                       end
+                       response
+                     end
+          end
+        end
+        return [action, bound_kargs]
+      end
+      
+      def attach_before_filter(route, method, &block)
+        method = method.to_sym
+        attach_route(route) do |segment|
+          segment.before_filter[method] << block
+        end
+      end
+      
+      def attach_after_filter(route, method, &block)
+        method = method.to_sym
+        attach_route(route) do |segment|
+          segment.after_filters[method] << block
+        end
+      end
+      
+      def attach_action(route, method, &block)
+        method = method.to_sym
+        attach_route(route) do |segment|
+          segment.action[method] = block
+        end
+      end
+      
+      def attach_route(route, &block)
+        segment = route.shift
+        
+        unless @children.has_key?(segment.name)
+          @children[segment.name] = segment
+        end
+        
+        if route.empty?
+          block.call(@children[segment.name])
+        else
+          @children[segment.name].attach_route(route, &block)
+        end
+      end
+      
+      def root?
+        name == ''
+      end
+      
+      def leaf?
+        @children.empty?
+      end
+      
+      def variable?
+        @variable ? true : false
+      end
+      
+      def [](child_name)
+        if @children.has_key? child_name
+          return @children[child_name]
+        end
+        has_variable_child? ? @children.find {|k, v| v.variable?}.last \
+                            : nil
+      end
+      
+      def has_variable_child?
+        @children.any? {|name, segment| segment.variable?}
+      end
+
+      attr_reader :name, :action, :before_filter, :after_filter
+      
+      def initialize(segment='')
+        @variable = segment =~ /^\{(.*)\}$/
+        @name = variable? ? $1 : segment
+        @action = {}
+        @children = {}
+        @before_filter = Hash.new {|hash, method| hash[method] = []}
+        @after_filter  = Hash.new {|hash, method| hash[method] = []}
+      end
+      
+      def inspect(depth=1)
+        this_node = root?     ? '/':
+                    variable? ? "{#{name}}"\
+                              : name
+        allowed_methods = action.keys.join(', ')
+        result = ["#{this_node} : #{allowed_methods}"]
+        @children.each {|name, child| result << child.inspect(depth+1)}
+        result.join("\n#{'  '*depth}> ")
+      end
+    end
+  end
+end
+end
diff --git a/lib/hdboo/search.rb b/lib/hdboo/search.rb
new file mode 100644 (file)
index 0000000..767da7b
--- /dev/null
@@ -0,0 +1,145 @@
+require 'open-uri'
+require 'xapian'
+require 'hdboo/crawler'
+require 'hdboo/_search'
+
+include Hdboo
+
+module Search
+class Document
+  def initialize(io)
+    @io = io.is_a?(IO) \
+        ? io \
+        : StringIO.new(io.to_s)
+    @mime_type = @io.mime_type
+  end
+
+  def text
+    if @text.nil?
+      @text = @io.gets(nil)
+    end
+    @text
+  end
+
+  def term_list
+    if @term_list.nil?
+      @term_list = []
+    end
+    @term_list
+  end
+
+  def mime_type
+    @mime_type
+  end
+
+  class Zone
+  end
+end
+end
+=begin
+class Index
+  public
+  def self.connect(path)
+    @instances[path]
+  end
+
+  @instances = Hash.new do |hash, path|
+    path = File.expand_path
+    hash[path] = new(path)
+  end
+
+  private
+  def initialize(path)
+    Xapian::WritableDatabase.new(path, Xapian::DB_CREATE_OR_OPEN)
+  end
+
+  public
+  def close
+  end
+
+  def query(words, &block)
+    database = Xapian::Database.new('./temp/database')
+    stemmer  = Xapian::Stemmer.new('english')
+    query_parser = Xapian::QueryParser.new
+
+    query_parser.database = database
+    query_parser.stemmer  = stemmer
+    query_parser.stemming_strategy = Xapian::QueryParser::STEM_SOME
+  end
+end
+
+class Document < ObjectAsHash
+  zones (
+    :body,
+    :title
+  )
+  properties (
+    :id,
+    :mime_type,
+    :name,
+    :path
+  )
+  attr_reader(
+    :term_list
+  )
+
+  public
+  def initialize(file)
+
+  end
+end
+
+class Scanner
+  public
+  def scan(file, &block)
+    mime_type = file.mime_type
+    scrape_text_plain(file, &block)
+    self
+  end
+
+  private
+  def scan_text_html(page)
+
+  end
+
+  private
+  def scan_text_plain(file, &block)
+    Document.new(
+      :body =>
+      :doc_neme =>
+      :doc_path =>
+      :doc_id   =>
+      :created_date =>
+      :modified_date =>
+    )
+  end
+end
+
+
+class FileSystemIndexer
+  DEFAULT_INDEX_PATH = './INDEX'
+  def initialize(kargs={})
+    index_path = kargs[:index_path] || DEFAULT_INDEX_PATH
+    @index   = Index.connect(index_path)
+    @crawler = FileSystemCrawler.new
+    @scraper = Scraper.new
+  end
+
+  def crawl(*globs)
+    @crawler.crawl(*globs) do |file|
+      @scraper.scrape(file) do ||
+      end
+    end
+  end
+end
+
+class WebIndexer < WebCrawler
+  def crawl_along(uri)
+    open(uri) {|page| scrape(page)}
+  end
+
+  def scrape()
+    file.
+  end
+end
+=end
diff --git a/lib/hdboo/sql.rb b/lib/hdboo/sql.rb
new file mode 100644 (file)
index 0000000..dcf2bfa
--- /dev/null
@@ -0,0 +1,1096 @@
+require 'time'
+require 'hdboo/base'
+require 'hdboo/database'
+require 'hdboo/sqlparser'
+
+module ::Kernel
+  def require_sql(module_name, destructive=false, reload=false)
+    unless module_name =~ /([_\w]+(?:\/[_\w]+)*)@([_a-z]+)/
+      raise NameError, module_name
+    end
+    module_name = $1
+    db_name = $2
+    Hdboo.resource("#{module_name}.sql", reload) do |sql_script|
+      raise Errno::ENOENT, module_name unless sql_script
+      Hdboo::SQL.sql_eval(db_name, sql_script, destructive)
+      if Hdboo.resource_exist?("#{module_name}.rb")
+        reload ? load(module_name+'.rb')\
+               : require(module_name)
+      end
+      sql_script
+    end
+    true
+  end
+  
+  def require_sql!(module_name)
+    require_sql(module_name, true, false)
+  end
+  
+  def load_sql(module_name)
+    require_sql(module_name, false, true)
+  end
+  
+  def load_sql!(module_name)
+    require_sql(module_name, true, true)
+  end
+end
+
+module Hdboo
+module SQL
+  
+  def self.connect(database_name)
+    Database[database_name]
+  end
+  
+  def self.query(sql_script, &block)
+    Database.query(sql_script, &block)
+  end
+  
+  def self.sql_eval!(database_name, sql_script)
+    sql_eval(database_name, sql_script, true)
+    self
+  end
+  
+  def self.sql_eval(database_name, sql_script, destructive=false)
+    sql_ast    = Parser.new.parse(sql_script)
+    sql_module = nil
+    
+    Parser::ASTWalker.new\
+      .visit(:MODULE_ANNOTATION) do |node|
+        sql_module = node.children.find do |child|
+                      child.value.is_a?(Module)
+                     end.value
+        node.children.each do |child|
+          if child.value.is_a?(Include)
+            sql_module.elements << child.value.sql_element
+          end
+        end
+      end\
+      .visit(:CREATE_TABLE) do |node|
+        sql_module.elements << node.value
+      end\
+      .visit(:CREATE_VIEW) do |view|
+        sql_module.elements << node.value
+      end\
+      .visit(:CREATE_PROCEDURE) do |node|
+        sql_module.elements << node.value
+      end\
+    .walk(sql_ast)
+    
+    db = Database[database_name]
+    db.execute(sql_module.ddl) if destructive
+    db.transaction {sql_module.eval(destructive)}
+    self
+  end
+  
+  module Annotation
+    #static methods
+    def self.new(type, args)
+      type = type.to_sym
+      clazz = nil
+      annotation_libraries.each do |library|
+        if library.const_defined?(type)
+          clazz = library.const_get(type)
+          break if clazz < Annotation
+          clazz = nil
+        end
+      end
+
+      unless clazz
+        raise 'unknown annotation type: ' + type.to_s
+      end
+      begin
+        clazz.new(*args)
+      rescue
+        raise "annotation #{type.to_s} can not be initialized.: #{$!.message}"
+      end
+    end
+    
+    def self.annotation_libraries
+      unless @annotation_libraries
+        @annotation_libraries = [
+          ::Hdboo::SQL,
+          ::Hdboo::Validator,
+          ::Hdboo::SQL::Table,
+          ::Hdboo::SQL::Procedure,
+          ::Hdboo::SQL::Procedure::Intercepter,
+          ::Hdboo::SQL::Procedure::Parameter
+        ]
+        ::Hdboo::Validator.constants.each do |name|
+          validator = ::Hdboo::Validator.const_get(name)
+          if validator < ::Hdboo::Validator
+            validator.send(:include, Annotation)
+          end
+        end
+      end
+      @annotation_libraries
+    end
+  
+    #instance methods
+    attr_accessor :annotation_context
+  end
+  
+  module Element
+    attr_accessor :sql_module, :source
+    def eval
+      raise "#{self.class.name}#eval() must be implemented."
+    end
+    
+    def create 
+      raise "#{self.class.name}#create() must be implemented."
+    end
+    
+    def drop
+      raise "#{self.class.name}#drop() must be implemented."
+    end
+  end
+  
+  class Module
+    include Annotation
+    #static methods
+    @instances = {}
+    def self.[](name)
+      @instances[name.to_sym]
+    end
+    
+    def self.register(instance)
+      @instances[instance.fqn] = instance
+    end
+    
+    #instance methods   
+    attr_reader :fqn, :schema, :namespace, :elements
+    def initialize(fqn, schema='')
+      @fqn = fqn
+      @schema = schema
+      @elements = []
+    end
+    def [](symbol)
+      namespace.const_get(symbol.to_sym)
+    end
+    
+    def namespace
+      return @namespace if @namespace
+      unless fqn =~ /\w+(.\w+)*/
+        raise SyntaxError, "invalid namespace format: #{fqn}"
+      end
+      script = ''
+      segments = fqn.split(/\./)
+      segments.reverse.each_with_index do |name, depth|
+        root = (depth == segments.length-1) ? '::' : ''
+        script = [ "module #{root}#{name}",
+                     script,
+                   'end'
+                 ].join("\n")
+      end
+      Kernel.eval(script)
+      @rb_module = Kernel.eval('::' + segments.join('::'))
+      @rb_module
+    end
+    
+    def eval(destructive=false)
+      elements.each do |element|
+        element.sql_module = self
+        element.eval(destructive)
+      end
+    end
+    
+    def ddl
+      drop + "\n" + create
+    end
+    
+    def create
+      statements = elements.collect {|each| each.create}\
+                           .join("\n//\n")
+      "DELIMITER // \n#{statements}\n//"
+    end
+    
+    def drop
+      statements = elements.collect {|each| each.drop}\
+                           .reverse.join("\n//\n")
+      ['DELIMITER //',
+       'SET FOREIGN_KEY_CHECKS = 0;',
+       statements,
+       'SET FOREIGN_KEY_CHECKS = 1;'].join("\n//\n") + "\n//"
+    end
+  end
+  
+  class Include
+    include Annotation
+    
+    attr_reader :package_name
+    def initialize(package_name)
+      @package_name = package_name.to_sym
+    end
+    
+    def sql_element
+      unless Include.const_get(package_name)
+        raise "unkown sql package name: #{package_name}"
+      end
+      clazz = Include.const_get(package_name)
+      clazz.new
+    end
+    
+    #inner classes
+    class Pivot
+      include Element
+      def eval(destructive)
+        #nothing to do.
+      end
+      
+      def create
+        return [
+          'CREATE TABLE pivot (pos INTEGER) ENGINE = MYISAM',
+          (<<-END_SQL
+          INSERT INTO pivot
+          VALUES
+            (1),(2),(3),(4),(5),(6),(7),(8),(9),(10),
+            (11),(12),(13),(14),(15),(16),(17),(18),(19),(20),
+            (21),(22),(23),(24),(25),(26),(27),(28),(29),(30),
+            (31),(32)
+          ;
+          END_SQL
+          ),
+          
+          (<<-END_SQL
+          CREATE FUNCTION CSV_GET(
+            csv VARCHAR(1024),
+            pos INTEGER
+          ) RETURNS VARCHAR (1024) DETERMINISTIC
+          BEGIN
+            RETURN SUBSTRING_INDEX(SUBSTRING_INDEX(csv, ',', pos), ',', -1);
+          END
+          END_SQL
+          ),
+          
+          (<<-END_SQL
+          CREATE FUNCTION CSV_LENGTH(
+            csv VARCHAR(1024)
+          ) RETURNS INTEGER DETERMINISTIC
+          BEGIN
+            IF csv IS NULL THEN
+              RETURN 1;
+            END IF;
+            RETURN LENGTH(csv) - LENGTH(REPLACE(csv, ',', '')) + 1;
+          END
+          END_SQL
+          ) 
+        ]
+      end
+      
+      def drop
+        return [
+          'DROP TABLE IF EXISTS pivot CASCADE',
+          'DROP FUNCTION IF EXISTS CSV_GET',
+          'DROP FUNCTION IF EXISTS CSV_LENGTH',
+        ]
+      end
+    end
+  end
+  
+  class ResultSet < Array
+    def as(clazz)
+      replace(collect do |row|
+        row.as(clazz)
+      end)
+    end
+  end
+  
+  class Table
+    include Element
+    
+    #static methods
+    def self.[](name)
+      @instances[name.to_sym]      
+    end
+    def self.register(table)
+      @instances[table.name] = table      
+    end
+    @instances = {}
+    
+    #instance methods
+    attr_reader :name, :elements, :annotations, :clazz
+    def initialize(name, elements, annotations=[])
+      @name        = name.to_sym
+      @elements    = elements.to_a
+      @annotations = annotations.to_a
+      annotations.each {|each| each.annotation_context = self}
+    end
+    
+    def columns
+      unless @columns
+        @columns = elements.select {|each| each.is_a?(Column)}
+      end
+      @columns
+    end
+    
+    def constraints
+      unless @constraints
+        @constraints = elements.select {|each| each.is_a?(Constraint)}
+      end
+      @constraints
+    end
+    
+    def unique_constraints
+      constraints.select{|any| any.is_a?(Table::Constraint::Unique)}
+    end
+    
+    def create
+      [ source ]
+    end
+    
+    def drop
+      ["DROP TABLE IF EXISTS #{name} CASCADE"]
+    end
+    
+    def eval(destructive)
+      Table.register(self)
+      @clazz = sql_module.namespace.const_defined?(name)\
+             ? augment_class\
+             : define_class
+      if classified_by
+        classified_by.define_subclasses
+      end 
+      @clazz
+    end
+    
+    def define_class
+      clazz = Class.new(Record)
+      define_accessors(clazz)
+      define_class_accessors(clazz)
+      sql_module.namespace.const_set(name, clazz)
+      clazz      
+    end
+    
+    def augment_class
+      clazz = sql_module.namespace.const_get(name)
+      define_accessors(clazz)
+      define_class_accessors(clazz)
+      clazz
+    end
+    
+    def define_accessors(clazz)
+      metadata = self
+      clazz.module_eval do
+        metadata.columns.each do |column|
+          define_method(column.name) do
+            self[column.name]
+          end
+          setter_name = (column.name.to_s + '=').to_sym
+          define_method(setter_name) do |val|
+            column.validate(val)
+            self[column.name] = val
+            self
+          end
+          if column.name.to_s =~ /^is_(.+)$/
+            define_method($1 + '?') do
+              result = send(column.name)
+              return (result == 0) ? false : result
+            end
+          end
+        end
+      end
+    end
+    
+    def define_class_accessors(clazz)
+      metadata = self
+      Hdboo.singleton_eval(clazz) do
+        define_method(:metadata) do
+          metadata
+        end
+      end
+    end
+    
+    def classified_by
+      unless @classified_by
+        @classified_by = annotations.find {|each| each.is_a? ClassifiedBy}
+      end
+      @classified_by
+    end
+    
+    class Column
+      attr_reader :name, :type, :options, :annotations
+      def initialize(name, type, options=[], annotations=[])
+        @name = name.to_sym
+        @type = type
+        @options = options.to_a
+        @annotations = annotations.to_a
+        annotations.each {|each| each.annotation_context = self}
+      end
+      
+      def validators
+        unless @validators
+          validators = annotations.concat(options).push(@type)\
+                         .select{|each| each.is_a?(Validator)}
+          @validators = Validator::ValidatorList.new(validators)
+        end
+        @validators
+      end
+      
+      def validate(val)
+        validators.validate(val, name)
+      end
+    end
+    
+    module Constraint
+      #static methods
+      def self.new(type, args)
+        type = type.to_sym
+        unless self.const_defined?(type)
+          raise "unknown constraint type: #{type.to_s}"
+        end
+        clazz = self.const_get(type)
+        clazz.new(*args)
+      end
+      
+      #instance methods
+      attr_reader :name, :columns
+      attr_accessor :annotations
+      
+      #inner classes
+      class Unique
+        include Constraint
+        def initialize(name, columns)
+          @name        = name.to_sym
+          @columns     = columns.to_a
+          @annotations = []
+          annotations.each {|each| each.annotation_context = self}
+        end
+      end
+      
+      class PrimaryKey < Unique
+        include Constraint
+      end
+      
+      class ReferencesConstraint
+        include Constraint
+        attr_reader :reference_table, :reference_columns
+        def initialize(name, columns, reference_table, reference_columns)
+          @name              = name.to_sym
+          @columns           = columns.to_a
+          @reference_table   = reference_table.to_sym
+          @reference_columns = reference_columns.to_a
+          @annotations       = []
+          annotations.each {|each| each.annotation_context = self}
+        end
+      end
+    end
+    
+    class Index
+      attr_reader :name, :columns, :annotations
+      def initialize(name, columns)
+        @name        = name.to_sym
+        @columns     = columns
+        @annotations = []
+        annotations.each {|each| each.annotation_context = self}
+      end
+    end
+    
+    class ClassifiedBy
+      include Annotation
+      attr_reader :column_name
+      def initialize(column_name)
+        @column_name = column_name.to_sym
+      end
+      
+      def annotation_context=(table)
+        unless table.columns.any? {|any| any.name == column_name}
+          raise "undefined column: #{table.name}.#{@column_name}."
+        end
+        @annotation_context = table
+      end
+      
+      def define_subclasses
+        table = annotation_context
+        subtype_names.each do |subtype_name|
+          namespace = table.sql_module.namespace
+          next if namespace.const_defined?(subtype_name)
+          subclass = Class.new(table.clazz)
+          namespace.const_set(subtype_name, subclass)
+        end
+      end
+      
+      def subtype_names
+        power_type.clazz.constants.reject{|any| any == 'Enumerable'}
+      end
+      
+      def power_type
+        table = annotation_context
+        power_type_name = table.constraints.find do |constraint|
+             constraint.is_a?(Constraint::ReferencesConstraint)\
+          && constraint.columns[0].to_sym == column_name 
+        end.reference_table
+        Table[power_type_name]
+      end
+    end
+  end
+  
+  class View
+    include Element
+    attr_reader :name, :from_tables, :column_names, :annotations
+    def initialize(name, from_tables=[], column_names=[], annotations=[])
+      @name = name.to_sym
+      @from_tables = from_tables.to_a
+      @column_names = column_names.to_a
+      @annotations = annotations.to_a
+      annotations.each {|each| each.annotation_context = self}
+    end
+    
+    def eval
+      #TODO
+    end
+    
+    def create
+      [source]
+    end
+    
+    def drop
+      ["DROP VIEW IF EXISTS #{name} CASCADE"]      
+    end
+  end
+  
+  module DataType
+    include Validator
+    attr_reader :name, :length
+    
+    def self.new(type, length=nil)
+      clazz = TYPE_TO_CLASS_MAP[type.to_sym]
+      clazz.new(length)
+    end
+    
+    def initialize(length=nil)
+      @length = length
+    end
+    
+    def value_of(value)
+      raise "#{self.class.name}#value_of() must be implemented."
+    end
+    
+    class Integer
+      include DataType
+      def pass?(val)
+        if val.nil? || val.is_a?(TrueClass) || val.is_a?(FalseClass)
+          return true
+        end
+        if val.is_a?(::String) && (val =~ /([+|-]?[1-9][0-9]*|0)/)
+          val = val.to_i
+        end
+        unless val.is_a?(Numeric) && val.integer?
+          return false
+        end
+        length.nil? ? true\
+                    : val < 10^length
+      end
+      
+      def message(val)
+        length.nil?\
+          ? "must be integer, but was #{val}."\
+          : "must be integer of #{length} digits or less, but was #{val}."
+      end
+      
+      def value_of(val)
+        if val.is_a?(Hash)
+          val = val.values.first
+        elsif val.is_a?(Array)
+          val = val.first
+        end
+        val.to_i
+      end
+    end
+    
+    class Bool < Integer
+      include DataType
+      def initialize
+        @length = 1
+      end
+           
+      def value_of(val)
+        if val.is_a?(Hash)
+          val = val.values.first
+        elsif val.is_a?(Array)
+          val = val.first
+        end
+        val != 0 
+      end
+    end
+    
+    class String
+      include DataType
+      def pass?(val)
+        val.nil? || val.is_a?(::String)
+      end
+      
+      def message(val)
+        "must be String, but was #{val.class.name}."
+      end
+      def value_of(val)
+        if val.is_a?(Hash)
+          val = val.values.first
+        elsif val.is_a?(Array)
+          val = val.first
+        end
+        val.to_s
+      end
+    end
+    
+    class DateTime
+      include SQL::DataType
+      def pass?(val)
+        true
+        #TODO
+      end
+      
+      def message(val)
+        'datetime'
+        #TODO
+      end  
+    end
+    
+    TYPE_TO_CLASS_MAP = {
+      :INTEGER   => DataType::Integer,
+      :TINYINT   => DataType::Integer,
+      :SMALLINT  => DataType::Integer,
+      :BOOL      => DataType::Bool,
+      :CHAR      => DataType::String,
+      :VARCHAR   => DataType::String,
+      :TEXT      => DataType::String,
+      :DATETIME  => DataType::DateTime,
+      :TIMESTAMP => DataType::DateTime 
+    }    
+  end
+  
+  class Record < HashAsObject
+    include Validatable
+    
+    #static methods
+    def self.metadata
+      Table.new('-', [], [])
+    end
+    def metadata
+      self.class.metadata
+    end
+    
+    def self.identifier_keys
+      metadata.constraints\
+              .find {|some| some.is_a? Table::Constraint::PrimaryKey}\
+              .columns\
+              .map {|each| each.to_sym}
+    end
+    
+    def identifier
+      self.class.identifier_keys.map {|key| self[key]}
+    end
+    
+    def ==(other)
+      return super unless other.is_a? Record
+      return other == self if self.instance_of? Record
+      identifier == other.identifier
+    end
+        
+    def as(clazz)
+      metadata = clazz.metadata
+      if metadata.classified_by
+        class_name = self[metadata.classified_by.column_name]
+        clazz = metadata.sql_module.namespace.const_get(class_name)
+      end
+      clazz.new.replace(self)
+    end
+    
+    def validators_for(key)
+      result = []
+      key = key.to_sym
+      unless self.is_a?(Record)
+        result.concat(super)
+      end
+      column_for_key = metadata.columns.find{|any|any.name==key}
+      result.concat(column_for_key.validators) if column_for_key
+      result
+    end
+  end
+  
+  class Procedure
+    include Element
+    
+    #static methods
+    @instances = {}
+    def self.[](name)
+      @instances[name.to_sym]
+    end
+    
+    def self.register(procedure)
+      @instances[procedure.name.to_sym] = procedure
+    end
+    
+    #instance methods
+    attr_reader :name, :owner, :annotations, :parameters
+    def initialize(name, parameters, annotations)
+      @name = name
+      @parameters = parameters
+      parameters.each {|each| each.owner = self}
+      @annotations = annotations.to_a
+      annotations.each {|each| each.annotation_context = self}
+    end
+   
+    def create
+      [source]
+    end
+    
+    def drop
+      ["DROP PROCEDURE IF EXISTS #{name}"]
+    end
+    
+    def eval(destructive)
+      Procedure.register(self)
+      unless name =~ /^([A-Z][A-Za-z0-9]+)_([\w]+)$/
+        return nil
+      end
+      class_name  = $1
+      method_name = $2
+      clazz  = sql_module.namespace.const_get(class_name)
+      @owner = clazz.metadata
+
+      Hdboo.singleton_eval(clazz, <<-EOS)
+        def #{method_name} (*args, &block)
+          Hdboo::SQL::Procedure[:#{name}].call(*args, &block)
+        end
+      EOS
+      
+      clazz.module_eval(<<-EOS)
+        def #{method_name}(kargs={}, &block)
+          kargs = Hdboo.normalize_key(kargs)
+          kargs = self.to_hash.merge(kargs.to_hash)
+          Hdboo::SQL::Procedure[:#{name}].call(kargs, &block)
+        end
+        
+        def #{method_name}! (kargs={}, &block)
+          result = #{method_name}(kargs, &block)
+          new_status = result.is_a?(Array)\
+                     ? result[0]\
+                     : result
+          unless new_status
+            raise NoDataFound, '#{name}', caller
+          end
+          replace(new_status)
+        end
+      EOS
+      
+      if master_data_loader && destructive
+        master_data_loader.load
+      end
+      
+      if constants_definition
+        constants_definition.define_on(clazz)
+      end
+      self
+    end
+    
+    def master_data_loader
+      annotations.find {|any| any.is_a? MasterData}
+    end
+    
+    def constants_definition
+      annotations.find {|any| any.is_a? DefineConstants}
+    end
+    
+    def call(*args, &block)
+      kargs = nil
+      if args.length == 0
+        kargs = {}
+      elsif args.length == 1 && args.first.respond_to?(:to_hash)
+        kargs = Hdboo.normalize_key(args.first.to_hash)
+      else
+        kargs = {}
+        parameters.each_with_index do |param, i|
+          kargs[param.name.to_sym] = args[i]
+        end
+      end
+
+      intercepters.reverse.each do |intercepter|
+        intercepter.before(kargs, self)
+      end
+      
+      invocation = nil
+      intercepters.reverse.each do |intercepter|
+        wrapee = invocation
+        invocation = lambda do |kargs|
+          intercepter.around(wrapee, kargs, self, &block)
+        end 
+      end
+      
+      begin
+        result = invocation.call(kargs)
+      rescue
+        intercepters.each do |intercepter|
+          intercepter.after_error($!, self)
+        end
+      end
+      
+      intercepters.each do |intercepter|
+        result = intercepter.after(result, self)
+      end
+      result
+    end
+    
+    def before(kargs, procedure)
+      errors = Hash.new {|hash, key| hash[key] = []}
+      parameters.each do |parameter|
+        value = kargs[parameter.name]
+        if parameter.required? && value.nil?
+          errors[parameter.name] << 'required.'
+        else
+          parameter.validators.each do |validator|
+            unless validator.pass?(value)
+              errors[parameter.name] << validator.message(value)
+            end
+          end
+        end
+      end
+      unless errors.empty?
+        raise InvalidArgument, errors
+      end
+    end
+  
+    def around(invocation, kargs, procedure, &block)
+      args = parameters.collect {|parameter| kargs[parameter.name]}
+      Database.call_procedure(name, args, &block)
+    end
+  
+    def after(result, procedure)
+      result
+    end
+    
+    def after_error(error, procedure)
+      raise error
+    end
+        
+    def intercepters
+      unless @intercepters
+        @intercepters = @annotations\
+                          .select {|each| each.kind_of?(Intercepter)}\
+                          .unshift(self)\
+                          .reverse
+      end
+      @intercepters
+    end
+    
+    #inner classes
+    class Parameter
+      attr_reader :name, :type, :annotations
+      attr_accessor :owner
+      
+      def initialize(name, type, annotations)
+        @name        = name.sub(/^_+/, '').to_sym
+        @type        = type
+        @annotations = annotations
+        @optional    = nil
+        annotations.each {|each| each.annotation_context = self}
+      end
+      
+      def annotations= (annotations)
+        annotations.each {|each| each.annotation_context = self}
+        @annotations = annotations
+      end
+      
+      def validators
+        return @validators if @validators
+        @validators = annotations.map do |annotation|
+          if annotation.is_a?(ColumnValue)
+            annotation.validators
+          elsif annotation.is_a?(Validator)
+            annotation
+          else
+            nil
+          end
+        end.flatten.compact
+      end
+      
+      def optional?
+        if @optional.nil?
+          @optional = annotations.any? {|each| each.is_a?(Optional)}
+        end
+        @optional
+      end
+      
+      def required?
+        !optional?
+      end
+  
+      
+      class Optional
+        include Annotation
+        attr_reader :default_value
+        def intialize(default_value=nil)
+          @default_value = default_value
+        end
+      end
+      
+      class ColumnValue
+        include Annotation
+        attr_reader :table_name
+        def initialize(table_name=nil)
+          @table_name = table_name 
+        end
+        
+        def column_name
+          annotation_context.name
+        end
+        
+        def validators
+          procedure = annotation_context.owner
+          namespace = procedure.sql_module.namespace
+          table = table_name.nil?\
+                ? procedure.owner\
+                : namespace.const_get(table_name.to_sym).metadata
+          column = table.columns.find {|each| each.name == column_name}
+          @validators = column.validators
+        end
+      end
+    end
+  
+    class Intercepter
+      def before(kargs, procedure)
+      end
+      
+      def around(invocation, kargs, procedure, &block)
+        invocation.call(kargs, &block)
+      end
+      
+      def after(result, procedure)
+        result
+      end
+      
+      def after_error(error, procedure)
+        raise error
+      end
+      
+      class OnNoData < Intercepter
+        include Annotation
+        attr_reader :message_key, :message_text
+        def initialize(message_key, message_text)
+          @message_key  = message_key
+          @message_text = message_text
+        end
+        
+        def after(result, procedure)
+          if result.nil? || (result.is_a?(Array) && result.empty?)
+            raise NoDataFound.new({@message_key => [@message_text]})
+          end
+          result
+        end
+      end
+      
+      class OnExists < Intercepter
+        include Annotation
+        attr_reader :message_key, :message_text
+        def initialize(message_key, message_text)
+          @message_key  = message_key
+          @message_text = message_text
+        end
+        
+        def after(result, procedure)
+          unless result.nil? || (result.is_a?(Array) && result.empty?)
+            raise DataConflict.new({@message_key => [@message_text]})
+          end
+          result
+        end
+      end
+      
+      class OnConflict < Intercepter
+        include Annotation
+        attr_reader :constraint_name, :message_key, :message_text
+        def initialize(constraint_name, message_key, message_text)
+          @constraint_name = constraint_name
+          @message_key     = message_key
+          @message_text    = message_text
+        end
+        
+        def after_error(error, procedure)
+          return unless error.sqlstate == '23000'
+          table = procedure.owner
+          if error.message =~ /Duplicate entry '(.*?)' for key '(.+?)'/
+            #seq_num = $1.to_i - 1
+            #violated_constraint = table.unique_constraints[seq_num]
+            #if  violated_constraint.name == constraint_name.to_sym
+            
+            if constraint_name == $2
+              raise DataConflict, {@message_key => [@message_text]}
+            end
+          elsif error.message =~ /foreign key constraint fails/
+            if error.message.include?(constraint_name.to_s)
+              raise DataConflict, {@message_key => [@message_text]}
+            end
+          end
+          #raise error
+        end  
+      end
+      
+      class Returns < Intercepter
+        include Annotation
+        attr_reader :return_type
+        
+        def initialize(return_type=Record)
+          if return_type.is_a?(DataType)
+            @return_type = return_type
+          elsif return_type.is_a?(String)
+            @return_type = return_type.to_sym == :Bool\
+                         ? DataType::Bool.new\
+                         : return_type.to_sym
+          else
+            @return_type = return_type
+          end
+        end
+        
+        def after(result, procedure)
+          if return_type.is_a?(Symbol)
+            type = procedure.sql_module.namespace.const_get(return_type)
+            raise "unknown type: #{return_type.to_s}" if type.nil?
+            @return_type = type
+          end
+          return nil if result.nil?
+          unless result.is_a?(Array)
+            result = RecordSet.new([result])
+          end
+          result.replace(result.collect do |record|
+            return_type.is_a?(DataType)\
+              ? return_type.value_of(record)\
+              : record.as(return_type)
+          end)
+          result
+        end
+      end
+      
+      class ReturnsFirst < Returns
+        def after(result, procedure)
+          super.first
+        end        
+      end
+    end
+    
+    class MasterData
+      include Annotation
+      def load
+        procedure = annotation_context
+        procedure.call
+      end
+    end
+    
+    class DefineConstants
+      include Annotation
+      attr_reader :key_column_name
+      def initialize(key_column_name)
+        @key_column_name = key_column_name.to_sym
+      end
+      
+      def define_on(clazz)
+        procedure = annotation_context
+        procedure.call.each do |record|
+          constant_key = record[key_column_name]
+          unless clazz.const_defined?(constant_key)
+            clazz.const_set(constant_key, record)
+          end
+        end
+      end
+    end
+  end
+end
+end
diff --git a/lib/hdboo/sqlparser.rb b/lib/hdboo/sqlparser.rb
new file mode 100644 (file)
index 0000000..d8e005c
--- /dev/null
@@ -0,0 +1,1745 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by racc 1.4.5
+# from racc grammer file "sqlparser.y".
+#
+
+require 'racc/parser'
+
+
+require 'strscan'
+
+
+module Hdboo
+
+  module SQL
+
+    class Parser < Racc::Parser
+
+module_eval <<'..end sqlparser.y modeval..idadf64e57b3', 'sqlparser.y', 371
+def constraint(type, *args)
+  all_elements = args.pop
+  nd(:CONSTRAINT, SQL::Table::Constraint.new(type, args), all_elements)
+end
+
+def index(name, columns, all_elements)
+  nd(:INDEX, SQL::Table::Index.new(name, columns), all_elements)
+end
+
+def procedure(name, parameters, annotations, all_elements)
+  nd(:CREATE_PROCEDURE,
+     SQL::Procedure.new(
+       name.value,
+       parameters.to_a,
+       annotations.to_a
+     ),
+     all_elements)
+end
+
+def parameter(name, type, annotations, all_elements)
+  nd(:PARAMETER,
+     SQL::Procedure::Parameter.new(
+       name.value,
+       type.value,
+       annotations.to_a
+     ),
+     all_elements)
+end 
+
+def table(name, elements, annotations, all_elements)
+  nd(:CREATE_TABLE,
+     SQL::Table.new(name.value, elements.to_a, annotations.to_a),
+     all_elements)
+end
+
+def column(name, type, column_options, annotations, all_elements)
+  nd(:COLUMN_DEFINITION,
+     SQL::Table::Column.new(
+       name.value, type.value, column_options.to_a, annotations.to_a
+     ),
+     all_elements)
+end
+
+def annotation(type, args, all_elements)
+  type = type.value
+  args = args.to_a
+  nd(:ANNOTATION, SQL::Annotation.new(type, args), all_elements)
+end
+
+def datatype(type, length, val)
+  nd(:DATA_TYPE, SQL::DataType.new(type, length), val)
+end
+
+def nd(type, value, all_elements=value)
+  node = Node.new(type, value, @source)
+  update_pos(node, all_elements)
+  if value.respond_to?(:source=)
+    value.source = node.source
+  end
+  node
+end
+
+def update_pos(node, all_elements)
+  all_elements = all_elements.compact
+  node.start = all_elements.collect{|val| val.start}.compact[0]
+  node.last  = all_elements.collect{|val| val.last }.compact[-1]
+  node
+end
+
+class Node
+  attr_reader   :type, :value
+  attr_accessor :start, :last
+  
+  def initialize(type, value, source)
+    @type   = type.to_sym
+    @value  = value
+    @source = source
+  end
+  
+  def push(elem)
+    raise Exception if leaf?
+    @value << elem
+    self
+  end
+  
+  def unshift(elem)
+    raise Exception if leaf?
+    @value.unshift(elem)
+    self
+  end
+  
+  def [](index)
+    if leaf?
+      return index = 0 ? @value : nil
+    end
+    if index.is_a?(Integer)
+      return @value[index]
+    end
+    @value.detect{|val| val.type == index.to_sym}
+  end
+
+  def leaf?
+    !@value.is_a?(Array)
+  end
+  
+  def name
+    self[:IDENTIFIER].value
+  end
+  
+  def children
+    leaf? ? [] : @value
+  end
+  
+  def find(node_type)
+    node_type = node_type.to_sym
+    return self if type == node_type
+    if leaf?
+      return nil
+    end
+    result = nil
+    children.each do |child_node|
+      found = child_node.find(node_type)
+      if found
+        result = found
+        break
+      end
+    end
+    result
+  end
+  
+  def source
+    start_row = @start[0]
+    start_col = @start[1]
+    last_row  = @last[0]
+    last_col  = @last[1]
+
+    result = @source[start_row..last_row]
+    if result.length == 1
+      result[0] = result[0][start_col..last_col]
+    else
+      result[0]  = result[0][start_col..-1]
+      result[-1] = result[-1][0..last_col]
+    end
+    result.join("\n")
+  end
+  
+  def to_a
+    if leaf?
+      return [@value]
+    end
+    @value.collect {|child| child.value}
+  end
+
+  def to_s
+    "#{type}: at line #{start[0]+1}, column #{start[1]+1}\nsource> #{source}"
+  end
+  
+  def inspect
+    to_s
+  end
+end
+
+class ASTWalker
+  def walk(ast)
+    walk_node(ast)
+    self
+  end
+  
+  def initialize
+    @listeners = Hash.new {|hash, key| hash[key] = []}
+  end
+  
+  def visit(note_type, &block)
+    node_type = note_type.to_sym
+    @listeners[node_type] << block
+    self
+  end
+  
+  private
+  def walk_node(node)
+    if @listeners.has_key?(node.type)
+      @listeners[node.type].each do |listener|
+        listener.call(node)
+      end
+    end
+    node.children.each do |child_node|
+p node if child_node.nil?
+      walk_node(child_node)
+    end
+  end
+end
+
+# token
+PUNCTUATION = /([;\.,()=@*<>\[\]]|\$\$|\/\/|--)/
+ONE_LINE_COMMENT = /--\s[^@]*$/
+KEYWORD_LIST = [
+  #general term
+  'NOT',
+  'NULL',
+  'DEFAULT',
+  'AS',
+
+  # sql command
+  'CREATE',
+  'TABLE',
+  'VIEW',
+  'PROCEDURE',
+  'INSERT',
+  'INTO',
+  'VALUES',
+  'DELIMITER',
+  
+  # table option
+  'ENGINE',
+  'MYISAM',
+  'INNODB',
+  'HEAP',
+  
+  # column option
+  'AUTO_INCREMENT',
+  
+  # data type
+  'INT',
+  'CHAR',
+  'VARCHAR',
+  'TEXT',
+  'INTEGER',
+  'TINYINT',
+  'SMALLINT',
+  'BIT',
+  'DATETIME',
+  'TIMESTAMP',
+  
+  # procedure / function
+  'BEGIN',
+  'END',
+  'IN',
+  'OUT',
+  'INOUT',
+  
+  # constraint
+  'CONSTRAINT',
+  'PRIMARY',
+  'FOREIGN',
+  'REFERENCES',
+  'UNIQUE',
+  'INDEX',
+  'KEY',
+  'FULLTEXT',
+  'USING',
+  'SECTIONALIZE',
+  
+  #query
+  'SELECT',
+  'FROM',
+  'WHERE',
+  
+  #dml
+  'DELETE',
+  'UPDATE',
+  
+  #boolean expression
+  'TRUE',
+  'FALSE',
+  'IS',
+  
+  #triggered action
+  'ON',
+  'CASCADE'
+]
+KEYWORD = /(#{KEYWORD_LIST.collect{|k| '\b'+k+'\b'}.join('|')})/ix
+IDENTIFIER = /([a-zA-Z][_0-9a-zA-Z]*)/
+
+# NUMBER
+NUMERIC = /([1-9][0-9]*)/
+INTEGER = /([+|-]?[1-9][0-9]*|0)/
+FLOAT   = /(
+            (?: #without exponent part
+              [+|-]?(?:[1-9][0-9]*|0)(?:\.[0-9]+)?
+            |   #with exponent part
+              [+|-]?[1-9]+(?:\.[0-9]+)?e[+|-]?[1-9][0-9]*
+            )
+          )/x
+
+# STRING
+NOT_PRINTABLE = '\x00-\x1F\x7F' #including space (\x20)
+MUST_BE_ESCAPED = '\x22\x27\x5C' #quot, double-quot, back-slash
+ESCAPE_TABLE = {
+  '\n'   => "\n",
+  '\b'   => "\b",
+  '\0'   => "\x00",
+  '\t'   => "\t",
+  "\\'"  => "'",
+  '\"'   => '"',
+  "\\\\" => "\\"
+} 
+ESCAPED = ESCAPE_TABLE.keys.map{|k|k.gsub(/\\/, "\\\\\\\\")}.join('|')
+CHARACTER_STRING = /(
+  #single-quoted string
+  ' 
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ''
+    |
+      "
+    )*
+  '
+  |
+  #double-quoted string
+  "
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ""
+    |
+      '
+    )*
+  "
+)/x
+
+# PROCEDURE
+PARAMETER_NAME = /([_a-zA-Z][_0-9a-zA-Z]*)/
+
+def parse(str)
+  @source = str.split(/\n/)
+  @lines  = @source.dup
+  @total_lines = @lines.length
+  next_line
+  do_parse
+end
+
+def next_line
+  @line = @lines.empty? \
+        ? nil \
+        : StringScanner.new(@lines.shift)
+end
+
+def current_line
+  @total_lines - (@lines.length + 1)
+end
+
+def pos
+  [current_line, @line.pos]
+end
+
+def next_token
+  token = _next_token
+  return nil unless token
+  token[1].start = @start_pos
+  token[1].last  = [pos[0], pos[1]-1]
+#puts token
+  return token
+end
+
+def _next_token
+  return nil unless @line
+  @line.skip(/\s*/)
+  if @in_procedure_code
+    if token = @line.skip_until(/(?=\bEND(?!\s+(?:IF|CASE|WHILE|LOOP))\b)/i)
+      @in_procedure_code = false
+      return tkn(:PROCEDURE_CODE)
+    end
+    @line.terminate
+  end
+  if @line.eos?
+    next_line
+    return _next_token
+  end
+  @start_pos = pos
+  if token = @line.scan(ONE_LINE_COMMENT)
+    return _next_token
+  elsif token = @line.scan(PUNCTUATION)
+    return tkn(token)
+  elsif token = @line.scan(KEYWORD)
+    token = token.upcase
+    if token == 'BEGIN'
+      @in_procedure_code = true
+    end
+    return tkn(token)
+  elsif token = @line.scan(IDENTIFIER)
+    return tkn(:IDENTIFIER, token)
+  elsif token = @line.scan(PARAMETER_NAME)
+    return tkn(:PARAMETER_NAME, token)
+  elsif token = @line.scan(CHARACTER_STRING)
+    return tkn(
+      :CHARACTER_STRING,
+      token[1..-2].gsub(/#{ESCAPED}/x) {|s|ESCAPE_TABLE[s]}\
+                  .gsub(/""/, '"')\
+                  .gsub(/''/, "\'")
+    )
+  elsif token = @line.scan(NUMERIC)
+    return tkn(:NUMERIC, token.to_i)
+  elsif token = @line.scan(INTEGER)
+    return tkn(:INTEGER, token.to_i)
+  elsif token = @line.scan(FLOAT)
+    return tkn(:FLOAT, token.to_f)
+  else
+    return tkn(:UNEXPECTED_TOKEN, @line.scan(/.*/))
+  end
+end
+
+def tkn(type, value=type)
+  [type, Node.new(type, value, @source)]
+end
+
+..end sqlparser.y modeval..idadf64e57b3
+
+##### racc 1.4.5 generates ###
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 2, 76, :_reduce_1,
+ 1, 77, :_reduce_2,
+ 0, 78, :_reduce_3,
+ 1, 78, :_reduce_4,
+ 3, 78, :_reduce_5,
+ 3, 78, :_reduce_6,
+ 3, 78, :_reduce_7,
+ 1, 80, :_reduce_none,
+ 1, 80, :_reduce_none,
+ 1, 80, :_reduce_none,
+ 1, 80, :_reduce_none,
+ 1, 80, :_reduce_none,
+ 1, 86, :_reduce_none,
+ 3, 86, :_reduce_14,
+ 8, 81, :_reduce_15,
+ 6, 82, :_reduce_16,
+ 0, 88, :_reduce_17,
+ 2, 88, :_reduce_18,
+ 3, 90, :_reduce_19,
+ 1, 91, :_reduce_none,
+ 1, 91, :_reduce_none,
+ 1, 91, :_reduce_none,
+ 1, 87, :_reduce_23,
+ 3, 87, :_reduce_24,
+ 1, 92, :_reduce_none,
+ 1, 92, :_reduce_none,
+ 4, 93, :_reduce_27,
+ 0, 96, :_reduce_28,
+ 2, 96, :_reduce_29,
+ 2, 97, :_reduce_30,
+ 1, 97, :_reduce_31,
+ 2, 97, :_reduce_32,
+ 2, 97, :_reduce_33,
+ 1, 97, :_reduce_34,
+ 1, 94, :_reduce_none,
+ 1, 94, :_reduce_none,
+ 1, 94, :_reduce_none,
+ 1, 94, :_reduce_none,
+ 1, 94, :_reduce_none,
+ 6, 99, :_reduce_40,
+ 7, 100, :_reduce_41,
+ 3, 106, :_reduce_42,
+ 6, 106, :_reduce_43,
+ 0, 107, :_reduce_44,
+ 3, 107, :_reduce_45,
+ 5, 101, :_reduce_46,
+ 0, 104, :_reduce_47,
+ 2, 104, :_reduce_48,
+ 5, 102, :_reduce_49,
+ 6, 103, :_reduce_50,
+ 2, 109, :_reduce_51,
+ 0, 108, :_reduce_52,
+ 1, 108, :_reduce_53,
+ 1, 105, :_reduce_54,
+ 3, 105, :_reduce_55,
+ 10, 83, :_reduce_56,
+ 0, 110, :_reduce_57,
+ 1, 110, :_reduce_58,
+ 4, 110, :_reduce_59,
+ 4, 111, :_reduce_60,
+ 1, 113, :_reduce_none,
+ 1, 113, :_reduce_none,
+ 0, 112, :_reduce_none,
+ 1, 112, :_reduce_none,
+ 1, 112, :_reduce_none,
+ 1, 112, :_reduce_none,
+ 0, 79, :_reduce_67,
+ 3, 79, :_reduce_68,
+ 1, 114, :_reduce_69,
+ 2, 114, :_reduce_70,
+ 2, 115, :_reduce_71,
+ 5, 115, :_reduce_72,
+ 0, 116, :_reduce_73,
+ 1, 116, :_reduce_74,
+ 3, 116, :_reduce_75,
+ 1, 117, :_reduce_none,
+ 1, 117, :_reduce_none,
+ 1, 117, :_reduce_none,
+ 4, 84, :_reduce_79,
+ 1, 118, :_reduce_none,
+ 2, 119, :_reduce_81,
+ 1, 120, :_reduce_82,
+ 3, 120, :_reduce_83,
+ 3, 121, :_reduce_84,
+ 0, 122, :_reduce_85,
+ 1, 122, :_reduce_86,
+ 3, 122, :_reduce_87,
+ 1, 123, :_reduce_none,
+ 1, 123, :_reduce_none,
+ 1, 95, :_reduce_90,
+ 1, 95, :_reduce_91,
+ 1, 95, :_reduce_92,
+ 1, 95, :_reduce_93,
+ 4, 95, :_reduce_94,
+ 4, 95, :_reduce_95,
+ 4, 95, :_reduce_96,
+ 4, 95, :_reduce_97,
+ 4, 95, :_reduce_98,
+ 1, 95, :_reduce_99,
+ 1, 95, :_reduce_100,
+ 1, 95, :_reduce_101,
+ 1, 95, :_reduce_102,
+ 1, 98, :_reduce_none,
+ 1, 98, :_reduce_none,
+ 1, 124, :_reduce_none,
+ 1, 124, :_reduce_none,
+ 1, 124, :_reduce_none,
+ 1, 85, :_reduce_none,
+ 3, 89, :_reduce_109,
+ 1, 125, :_reduce_none,
+ 1, 125, :_reduce_none,
+ 1, 127, :_reduce_none,
+ 3, 127, :_reduce_none,
+ 2, 126, :_reduce_114,
+ 2, 128, :_reduce_115,
+ 2, 129, :_reduce_116,
+ 1, 130, :_reduce_none,
+ 1, 131, :_reduce_118,
+ 3, 131, :_reduce_119,
+ 1, 132, :_reduce_120,
+ 3, 132, :_reduce_121,
+ 1, 133, :_reduce_122,
+ 2, 133, :_reduce_123,
+ 1, 134, :_reduce_124,
+ 3, 134, :_reduce_125,
+ 4, 134, :_reduce_126,
+ 1, 136, :_reduce_none,
+ 1, 136, :_reduce_none,
+ 1, 135, :_reduce_none,
+ 1, 137, :_reduce_none,
+ 3, 138, :_reduce_131,
+ 1, 139, :_reduce_none,
+ 1, 139, :_reduce_none,
+ 1, 140, :_reduce_134,
+ 1, 140, :_reduce_135,
+ 2, 140, :_reduce_136,
+ 1, 140, :_reduce_137,
+ 2, 140, :_reduce_138 ]
+
+racc_reduce_n = 139
+
+racc_shift_n = 240
+
+racc_action_table = [
+   119,    57,    44,    60,    62,    64,    67,    78,    16,   151,
+   186,    17,     5,    57,    14,    60,    62,    64,    67,   233,
+   104,   217,   196,   105,    60,    62,    64,    67,    60,    62,
+    64,    67,    14,   222,    93,    94,    18,   232,   106,     5,
+     5,     5,    80,    83,    86,   199,    78,   224,   225,     9,
+   181,   107,    71,    73,    54,    55,    56,    59,    61,    63,
+    65,    68,    12,   122,    71,    73,    54,    55,    56,    59,
+    61,    63,    65,    68,   214,   215,     9,     9,     9,   224,
+   225,    80,    83,    86,   -57,   193,   190,   191,   -57,    12,
+    12,    12,    14,   160,   161,   162,   163,   196,   108,    60,
+    62,    64,    67,   196,   110,    60,    62,    64,    67,   160,
+   161,   162,   163,    46,    48,    50,    14,    46,    48,    50,
+   199,   111,    14,    20,    21,    19,   199,    71,    73,    54,
+    55,    56,    59,    61,    63,    65,    68,    71,    73,    54,
+    55,    56,    59,    61,    63,    65,    68,   115,    57,   116,
+    60,    62,    64,    67,   114,   196,   112,    60,    62,    64,
+    67,   196,   102,    60,    62,    64,    67,    57,   112,    60,
+    62,    64,    67,   118,   101,    96,    52,   127,   128,   107,
+   129,   130,   131,   132,    92,   134,   135,    91,    90,   138,
+   139,   140,   141,   142,   146,   147,   148,    52,    42,    39,
+   155,   156,   157,   158,   159,    38,   166,   166,   169,   170,
+   171,   172,   146,   174,   119,   176,   177,   179,    14,   180,
+    36,    35,   184,   185,   103,   166,   166,   166,    24,    33,
+   206,   166,   208,   209,   210,   211,   212,    29,    28,   218,
+   220,    27,    26,   226,   227,    24,    22,   230,    15,   211,
+    14,   235,   166,   237,   238,   232 ]
+
+racc_action_check = [
+    90,    42,    37,    42,    42,    42,    42,   111,     5,   123,
+   168,     5,     1,   106,   123,   106,   106,   106,   106,   230,
+    59,   202,   177,    66,   177,   177,   177,   177,   162,   162,
+   162,   162,    37,   212,    49,    49,     5,   230,    70,    19,
+    20,    21,   111,   111,   111,   177,    43,   222,   222,     1,
+   162,    72,    42,    42,    42,    42,    42,    42,    42,    42,
+    42,    42,     1,    90,   106,   106,   106,   106,   106,   106,
+   106,   106,   106,   106,   202,   202,    19,    20,    21,   212,
+   212,    43,    43,    43,   124,   174,   174,   174,    38,    19,
+    20,    21,    43,   165,   165,   165,   165,   211,    73,   211,
+   211,   211,   211,   218,    80,   218,   218,   218,   218,   136,
+   136,   136,   136,   124,   124,   124,   124,    38,    38,    38,
+   211,    82,   109,     6,     6,     6,   218,   109,   109,   109,
+   109,   109,   109,   109,   109,   109,   109,    95,    95,    95,
+    95,    95,    95,    95,    95,    95,    95,    84,    52,    84,
+    52,    52,    52,    52,    84,   199,    83,   199,   199,   199,
+   199,   216,    55,   216,   216,   216,   216,   128,    86,   128,
+   128,   128,   128,    88,    54,    51,    96,    97,    98,    99,
+   101,   102,   103,   104,    47,   107,   108,    45,    44,   113,
+   114,   115,   116,   117,   118,   119,   120,    39,    35,    33,
+   129,   130,   131,   132,   135,    29,   138,   139,   140,   141,
+   142,   143,   145,   146,   147,   148,   150,   151,   153,   160,
+    27,    24,   166,   167,    56,   169,   170,   172,    23,    22,
+   179,   184,   187,   188,   189,   194,   198,    18,    17,   205,
+   209,    16,    15,   214,   215,    14,     9,   220,     3,   229,
+     2,   232,   233,   235,   236,   238 ]
+
+racc_action_pointer = [
+   nil,    -1,   202,   248,   nil,    -6,   114,   nil,   nil,   195,
+   nil,   nil,   nil,   nil,   196,   242,   239,   236,   235,    26,
+    27,    28,   227,   179,   219,   nil,   nil,   205,   nil,   190,
+   nil,   nil,   nil,   147,   nil,   183,   nil,   -16,    72,   182,
+   nil,   nil,    -1,    44,   124,   171,   nil,   160,   nil,    32,
+   nil,   151,   146,   nil,   159,   147,   209,   nil,   nil,     5,
+   nil,   nil,   nil,   nil,   nil,   nil,     7,   nil,   nil,   nil,
+    14,   nil,    39,    83,   nil,   nil,   nil,   nil,   nil,   nil,
+   102,   nil,    97,   154,   118,   nil,   166,   nil,   157,   nil,
+    -2,   nil,   nil,   nil,   nil,    84,   161,   161,   154,   167,
+   nil,   176,   177,   178,   179,   nil,    11,   183,   182,    74,
+   nil,     5,   nil,   174,   175,   161,   162,   153,   175,   171,
+   130,   nil,   nil,   -34,    68,   nil,   nil,   nil,   165,   184,
+   185,   186,   187,   nil,   nil,   188,    84,   nil,   204,   205,
+   193,   194,   169,   196,   nil,   193,   193,   212,   213,   nil,
+   149,   209,   nil,   170,   nil,   nil,   nil,   nil,   nil,   nil,
+   193,   nil,    24,   nil,   nil,    68,   198,   207,    -6,   223,
+   224,   nil,   225,   nil,    64,   nil,   nil,    20,   nil,   186,
+   nil,   nil,   nil,   nil,   229,   nil,   nil,   216,   217,   218,
+   nil,   nil,   nil,   nil,   166,   nil,   nil,   nil,   166,   153,
+   nil,   nil,     1,   nil,   nil,   171,   nil,   nil,   nil,   208,
+   nil,    95,     8,   nil,   223,   224,   159,   nil,   101,   nil,
+   245,   nil,   -24,   nil,   nil,   nil,   nil,   nil,   nil,   180,
+     4,   nil,   217,   250,   nil,   218,   238,   nil,   222,   nil ]
+
+racc_action_default = [
+   -67,    -3,    -2,  -139,    -1,  -139,    -4,    -8,    -9,  -139,
+   -10,   -11,  -108,   -12,  -139,  -139,  -139,  -139,  -139,    -3,
+    -3,    -3,  -139,   -68,  -139,   -69,   240,  -139,   -67,  -139,
+    -7,    -5,    -6,  -139,   -70,   -71,   -67,  -139,   -63,  -139,
+   -79,   -80,   -73,   -47,  -139,  -139,   -64,   -58,   -65,  -139,
+   -66,   -82,   -85,   -81,   -92,   -93,  -139,   -13,  -104,  -139,
+  -105,   -99,  -103,  -100,  -106,  -101,  -139,  -107,  -102,   -78,
+   -74,   -90,   -77,   -91,   -76,   -35,   -36,   -37,   -67,   -38,
+  -139,   -39,   -23,   -52,  -139,   -25,   -52,   -26,  -139,   -16,
+  -139,   -67,   -67,   -61,   -62,  -139,  -139,  -139,   -86,   -89,
+   -88,  -139,  -139,  -139,  -139,   -72,   -73,  -139,  -139,  -139,
+   -48,   -47,   -53,  -139,  -139,  -139,  -139,  -139,   -17,  -112,
+  -139,  -111,  -110,  -139,   -63,   -67,   -83,   -84,   -85,  -139,
+  -139,  -139,  -139,   -75,   -14,  -139,   -28,   -24,  -139,  -139,
+  -139,  -139,  -139,  -139,   -15,   -17,  -139,  -139,  -139,  -109,
+  -139,  -139,   -59,   -60,   -87,   -95,   -96,   -97,   -98,   -94,
+  -139,   -31,  -139,   -34,   -27,   -28,   -54,  -139,  -139,  -139,
+  -139,   -51,  -139,   -18,  -139,  -113,  -115,  -139,  -114,  -139,
+   -30,   -33,   -32,   -29,  -139,   -49,   -46,  -139,  -139,  -139,
+   -21,   -22,   -19,   -20,  -118,  -120,  -132,  -122,  -124,  -139,
+  -129,  -130,  -139,  -116,  -133,  -117,   -56,   -55,   -40,  -139,
+   -50,  -139,  -139,  -123,  -135,  -137,  -139,  -134,  -139,   -41,
+  -139,  -121,  -139,  -125,  -127,  -128,  -136,  -138,  -131,  -119,
+   -44,  -126,  -139,  -139,   -42,  -139,  -139,   -45,   -44,   -43 ]
+
+racc_goto_table = [
+     2,    74,    45,   167,   168,    97,    99,    88,    66,    53,
+   121,   100,   194,   164,   144,   125,   223,   234,    25,   113,
+     4,    23,   117,    95,   143,   239,   231,    34,    37,   136,
+    40,    41,   219,   192,   187,   188,    43,   189,    30,    31,
+    32,   173,   183,   120,   149,    89,   150,   178,   203,   207,
+   205,     3,   221,   229,   213,     1,   228,   216,   nil,   nil,
+   nil,   nil,   nil,   nil,   nil,    74,   126,   175,   nil,   nil,
+   nil,   nil,   133,   nil,   nil,   137,   nil,   nil,   109,   nil,
+   nil,   154,    99,   nil,   nil,   nil,   nil,   100,   152,   nil,
+   nil,   123,   124,   nil,   nil,   nil,   nil,   nil,   236,   nil,
+   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,   182,   nil,   nil,   nil,   153 ]
+
+racc_goto_check = [
+     4,    23,    35,    30,    30,    47,    11,    12,    41,    45,
+    52,    23,    57,    21,    13,    20,    61,    32,    40,    33,
+     3,    39,    33,    38,    34,    32,    61,    40,     4,    20,
+    43,    44,    31,    16,    30,    30,     4,    30,     3,     3,
+     3,    13,    21,    50,    51,    14,    53,    54,    55,    30,
+    56,     1,    58,    57,    59,     2,    64,    65,   nil,   nil,
+   nil,   nil,   nil,   nil,   nil,    23,    45,    52,   nil,   nil,
+   nil,   nil,    41,   nil,   nil,    12,   nil,   nil,     4,   nil,
+   nil,    47,    11,   nil,   nil,   nil,   nil,    23,    35,   nil,
+   nil,     4,     4,   nil,   nil,   nil,   nil,   nil,    30,   nil,
+   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,    23,   nil,   nil,   nil,     4 ]
+
+racc_goto_pointer = [
+   nil,    51,    55,    19,     0,   nil,   nil,   nil,   nil,   nil,
+   nil,   -46,   -36,  -104,     1,   nil,  -141,   nil,   nil,   nil,
+   -80,  -123,   nil,   -41,   nil,   nil,   nil,   nil,   nil,   nil,
+  -135,  -177,  -213,   -64,   -93,   -36,   nil,   nil,   -26,     7,
+     4,   -34,   nil,    -3,    -2,   -30,   nil,   -47,   nil,   nil,
+   -47,   -76,   -80,   -74,  -103,  -129,  -127,  -165,  -159,  -145,
+   nil,  -196,   nil,   nil,  -160,  -145 ]
+
+racc_goto_default = [
+   nil,   nil,   nil,   nil,   nil,     6,     7,     8,    10,    11,
+    13,    72,   nil,   nil,   nil,   145,   nil,    82,    85,    87,
+    69,   nil,   165,   204,    75,    76,    77,    79,    81,    84,
+   nil,   nil,   nil,   nil,   nil,   nil,    47,    49,   nil,   nil,
+   nil,   nil,    70,   nil,   nil,   nil,    51,   nil,    98,    58,
+   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   195,   197,
+   198,   nil,   200,   201,   202,   nil ]
+
+racc_token_table = {
+ false => 0,
+ Object.new => 1,
+ :IDENTIFIER => 2,
+ :PARAMETER_NAME => 3,
+ :NUMERIC => 4,
+ :CHARACTER_STRING => 5,
+ :INTEGER => 6,
+ :FLOAT => 7,
+ :PROCEDURE_CODE => 8,
+ ";" => 9,
+ "$$" => 10,
+ "//" => 11,
+ "." => 12,
+ "CREATE" => 13,
+ "TABLE" => 14,
+ "(" => 15,
+ ")" => 16,
+ "VIEW" => 17,
+ "AS" => 18,
+ "ENGINE" => 19,
+ "=" => 20,
+ "MYISAM" => 21,
+ "INNODB" => 22,
+ "HEAP" => 23,
+ "," => 24,
+ "NOT" => 25,
+ "NULL" => 26,
+ "DEFAULT" => 27,
+ "AUTO_INCREMENT" => 28,
+ "PRIMARY" => 29,
+ "KEY" => 30,
+ "FOREIGN" => 31,
+ "REFERENCES" => 32,
+ "ON" => 33,
+ "DELETE" => 34,
+ "CASCADE" => 35,
+ "UNIQUE" => 36,
+ "CONSTRAINT" => 37,
+ "INDEX" => 38,
+ "FULLTEXT" => 39,
+ "USING" => 40,
+ "SECTIONALIZE" => 41,
+ "PROCEDURE" => 42,
+ "BEGIN" => 43,
+ "END" => 44,
+ "IN" => 45,
+ "OUT" => 46,
+ "INOUT" => 47,
+ "--" => 48,
+ "@" => 49,
+ "INSERT" => 50,
+ "INTO" => 51,
+ "VALUES" => 52,
+ "INT" => 53,
+ "INTEGER" => 54,
+ "TINYINT" => 55,
+ "SMALLINT" => 56,
+ "CHAR" => 57,
+ "VARCHAR" => 58,
+ "TEXT" => 59,
+ "BIT" => 60,
+ "DATETIME" => 61,
+ "TIMESTAMP" => 62,
+ "DELIMITER" => 63,
+ "SELECT" => 64,
+ "*" => 65,
+ "FROM" => 66,
+ "WHERE" => 67,
+ "OR" => 68,
+ "AND" => 69,
+ "IS" => 70,
+ "TRUE" => 71,
+ "FALSE" => 72,
+ "<" => 73,
+ ">" => 74 }
+
+racc_use_result_var = true
+
+racc_nt_base = 75
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+'$end',
+'error',
+'IDENTIFIER',
+'PARAMETER_NAME',
+'NUMERIC',
+'CHARACTER_STRING',
+'INTEGER',
+'FLOAT',
+'PROCEDURE_CODE',
+'";"',
+'"$$"',
+'"//"',
+'"."',
+'"CREATE"',
+'"TABLE"',
+'"("',
+'")"',
+'"VIEW"',
+'"AS"',
+'"ENGINE"',
+'"="',
+'"MYISAM"',
+'"INNODB"',
+'"HEAP"',
+'","',
+'"NOT"',
+'"NULL"',
+'"DEFAULT"',
+'"AUTO_INCREMENT"',
+'"PRIMARY"',
+'"KEY"',
+'"FOREIGN"',
+'"REFERENCES"',
+'"ON"',
+'"DELETE"',
+'"CASCADE"',
+'"UNIQUE"',
+'"CONSTRAINT"',
+'"INDEX"',
+'"FULLTEXT"',
+'"USING"',
+'"SECTIONALIZE"',
+'"PROCEDURE"',
+'"BEGIN"',
+'"END"',
+'"IN"',
+'"OUT"',
+'"INOUT"',
+'"--"',
+'"@"',
+'"INSERT"',
+'"INTO"',
+'"VALUES"',
+'"INT"',
+'"INTEGER"',
+'"TINYINT"',
+'"SMALLINT"',
+'"CHAR"',
+'"VARCHAR"',
+'"TEXT"',
+'"BIT"',
+'"DATETIME"',
+'"TIMESTAMP"',
+'"DELIMITER"',
+'"SELECT"',
+'"*"',
+'"FROM"',
+'"WHERE"',
+'"OR"',
+'"AND"',
+'"IS"',
+'"TRUE"',
+'"FALSE"',
+'"<"',
+'">"',
+'$start',
+'sql',
+'module_annotation',
+'sql_command_list',
+'annotation_specification',
+'sql_command',
+'table_definition',
+'view_definition',
+'procedure_definition',
+'insert_statement',
+'alter_delimiter',
+'qualified_name',
+'table_element_list',
+'table_option_list',
+'query_specification',
+'table_option',
+'engine_type',
+'table_element',
+'column_definition',
+'constraint_definition',
+'data_type',
+'column_option_list',
+'column_option',
+'literal',
+'primary_key',
+'foreign_key',
+'unique',
+'index',
+'fulltext_index',
+'constraint_name_definition',
+'constrained_column_list',
+'references_specification',
+'triggered_action',
+'index_name_definition',
+'using',
+'parameter_list',
+'parameter',
+'parameter_direction',
+'parameter_name',
+'annotation_list',
+'annotation',
+'annotation_argument_list',
+'annotation_argument',
+'insert_columns_and_source',
+'from_constructor',
+'column_value_expression_list',
+'column_value_expression',
+'value_expression_list',
+'value_expression',
+'number',
+'select_list',
+'table_expression',
+'select_sublist',
+'from_clause',
+'where_clause',
+'search_condition',
+'boolean_value_expression',
+'boolean_term',
+'boolean_facter',
+'boolean_test',
+'boolean_primary',
+'true_value',
+'predicate',
+'comparison_predicate',
+'row_value_expression',
+'comp_op']
+
+Racc_debug_parser = false
+
+##### racc system variables end #####
+
+ # reduce 0 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 14
+  def _reduce_1( val, _values, result )
+ result = nd(:SQL, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 18
+  def _reduce_2( val, _values, result )
+ result = nd(:MODULE_ANNOTATION, val[0].children, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 22
+  def _reduce_3( val, _values, result )
+ result = nd(:SQL_COMMAND_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 24
+  def _reduce_4( val, _values, result )
+ result = nd(:SQL_COMMAND_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 27
+  def _reduce_5( val, _values, result )
+ update_pos(val[2], val)
+          result = val[2].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 30
+  def _reduce_6( val, _values, result )
+ update_pos(val[2], val)
+          result = val[2].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 33
+  def _reduce_7( val, _values, result )
+ update_pos(val[2], val)
+          result = val[2].unshift(val[0])
+   result
+  end
+.,.,
+
+ # reduce 8 omitted
+
+ # reduce 9 omitted
+
+ # reduce 10 omitted
+
+ # reduce 11 omitted
+
+ # reduce 12 omitted
+
+ # reduce 13 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 45
+  def _reduce_14( val, _values, result )
+ result = nd(:IDENTIFIER, val[0].value + val[1].value + val[2].value, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 51
+  def _reduce_15( val, _values, result )
+ result = table(val[2], val[5], val[4], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 56
+  def _reduce_16( val, _values, result )
+ result = nd(:CREATE_VIEW, [val[2], val[5], val[3]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 60
+  def _reduce_17( val, _values, result )
+ result = nd(:TABLE_OPTION_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 63
+  def _reduce_18( val, _values, result )
+ update_pos(val[1], val)
+          result = val[1].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 67
+  def _reduce_19( val, _values, result )
+ result = nd(:ENGINE, val[2], val)
+   result
+  end
+.,.,
+
+ # reduce 20 omitted
+
+ # reduce 21 omitted
+
+ # reduce 22 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 76
+  def _reduce_23( val, _values, result )
+ result = nd(:TABLE_ELEMENT_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 79
+  def _reduce_24( val, _values, result )
+ update_pos(val[2], val)
+          result = val[2].unshift(val[0])
+   result
+  end
+.,.,
+
+ # reduce 25 omitted
+
+ # reduce 26 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 87
+  def _reduce_27( val, _values, result )
+ result = column(val[0], val[2], val[3], val[1], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 91
+  def _reduce_28( val, _values, result )
+ result = nd(:COLUMN_OPTION_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 94
+  def _reduce_29( val, _values, result )
+ update_pos(val[0], val)
+          result = val[1].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 97
+  def _reduce_30( val, _values, result )
+ result = nd(:NOT_NULL, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 98
+  def _reduce_31( val, _values, result )
+ result = nd(:NULL, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 99
+  def _reduce_32( val, _values, result )
+ result = nd(:DEFAULT, val[1], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 100
+  def _reduce_33( val, _values, result )
+ result = nd(:DEFAULT, nil, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 101
+  def _reduce_34( val, _values, result )
+ result = nd(:AUTO_INCREMENT, val)
+   result
+  end
+.,.,
+
+ # reduce 35 omitted
+
+ # reduce 36 omitted
+
+ # reduce 37 omitted
+
+ # reduce 38 omitted
+
+ # reduce 39 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 112
+  def _reduce_40( val, _values, result )
+ result = constraint(:PrimaryKey, val[0].value, val[4].to_a, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 120
+  def _reduce_41( val, _values, result )
+ referenced_table, referenced_columns = *(val[6].value)
+          result = constraint(:ReferencesConstraint,
+                     val[0].value, val[4].to_a,
+                     referenced_table, referenced_columns, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 124
+  def _reduce_42( val, _values, result )
+ result = nd(:REFERENCES_SPECIFICATION, [val[1].value, []], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 126
+  def _reduce_43( val, _values, result )
+ result = nd(:REFERENCES_SPECIFICATION, [val[1].value, val[4].value], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 129
+  def _reduce_44( val, _values, result )
+ result = nd(:TRIGGERED_ACTION, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 130
+  def _reduce_45( val, _values, result )
+ result = nd(:TRIGGERED_ACTION, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 134
+  def _reduce_46( val, _values, result )
+ result = constraint(:Unique, val[0].value, val[3].to_a, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 137
+  def _reduce_47( val, _values, result )
+ result = nd(:CONSTRAINT_NAME, '-', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 138
+  def _reduce_48( val, _values, result )
+ result = nd(:CONSTRAINT_NAME, val[1].value, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 142
+  def _reduce_49( val, _values, result )
+ result = index(val[1].value, val[3].value, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 146
+  def _reduce_50( val, _values, result )
+ result = index(val[1].value, val[4].value, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 150
+  def _reduce_51( val, _values, result )
+ result = nd(:USING, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 153
+  def _reduce_52( val, _values, result )
+ result = nd(:CONSTRAINT_NAME, '<NO_NAME>', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 154
+  def _reduce_53( val, _values, result )
+ result = nd(:CONSTRAINT_NAME, val[0].value, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 158
+  def _reduce_54( val, _values, result )
+ result = nd(:CONSTRAINED_COLUMN_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 161
+  def _reduce_55( val, _values, result )
+ update_pos(val[2], val)
+          result = val[2].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 167
+  def _reduce_56( val, _values, result )
+ result = procedure(val[2], val[4], val[6], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 171
+  def _reduce_57( val, _values, result )
+ result = nd(:PARAMETER_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 173
+  def _reduce_58( val, _values, result )
+ result = nd(:PARAMETER_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 177
+  def _reduce_59( val, _values, result )
+ val[0].value.annotations = val[2].to_a
+          update_pos(val[3], val)
+          result = val[3].unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 181
+  def _reduce_60( val, _values, result )
+ result = parameter(val[1], val[2], val[3], val)
+   result
+  end
+.,.,
+
+ # reduce 61 omitted
+
+ # reduce 62 omitted
+
+ # reduce 63 omitted
+
+ # reduce 64 omitted
+
+ # reduce 65 omitted
+
+ # reduce 66 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 195
+  def _reduce_67( val, _values, result )
+ result = nd(:ANNTATION_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 199
+  def _reduce_68( val, _values, result )
+ update_pos(val[0], val)
+          val[0].value.concat(val[2].value)
+          result = val[0]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 203
+  def _reduce_69( val, _values, result )
+ result = nd(:ANNTATION_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 206
+  def _reduce_70( val, _values, result )
+ update_pos(val[0], val)
+          result = val[0].push(val[1])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 210
+  def _reduce_71( val, _values, result )
+ result = annotation(val[1], [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 212
+  def _reduce_72( val, _values, result )
+ result = annotation(val[1], val[3], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 216
+  def _reduce_73( val, _values, result )
+ result = nd(:ANNOTATION_ARGUMENT_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 218
+  def _reduce_74( val, _values, result )
+ result = nd(:ANNOTATION_ARGUMENT_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 220
+  def _reduce_75( val, _values, result )
+ result = update_pos(val[2], val).unshift(val[0])
+   result
+  end
+.,.,
+
+ # reduce 76 omitted
+
+ # reduce 77 omitted
+
+ # reduce 78 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 229
+  def _reduce_79( val, _values, result )
+ result = nd(:INSERT, val)
+   result
+  end
+.,.,
+
+ # reduce 80 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 236
+  def _reduce_81( val, _values, result )
+ result = update_pos(val[1], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 240
+  def _reduce_82( val, _values, result )
+ result = nd(:COLUMN_VALUE_EXPRESSION_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 242
+  def _reduce_83( val, _values, result )
+ result = update_pos(val[2], val).unshift(val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 246
+  def _reduce_84( val, _values, result )
+ result = update_pos(val[1], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 250
+  def _reduce_85( val, _values, result )
+ result = nd(:VALUE_EXPRESSION_LIST, [], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 252
+  def _reduce_86( val, _values, result )
+ result = nd(:VALUE_EXPRESSION_LIST, [val[0]], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 254
+  def _reduce_87( val, _values, result )
+ result = update_pos(val[2], val).unshift(val[0])
+   result
+  end
+.,.,
+
+ # reduce 88 omitted
+
+ # reduce 89 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 261
+  def _reduce_90( val, _values, result )
+ result = datatype(:INTEGER,   nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 262
+  def _reduce_91( val, _values, result )
+ result = datatype(:INTEGER,   nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 263
+  def _reduce_92( val, _values, result )
+ result = datatype(:TINYINT,   nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 264
+  def _reduce_93( val, _values, result )
+ result = datatype(:SMALLINT,  nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 265
+  def _reduce_94( val, _values, result )
+ result = datatype(:INTEGER,   val[2], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 266
+  def _reduce_95( val, _values, result )
+ result = datatype(:TINYINT,   val[2], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 267
+  def _reduce_96( val, _values, result )
+ result = datatype(:SMALLINT,  val[2], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 268
+  def _reduce_97( val, _values, result )
+ result = datatype(:CHAR,      val[2], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 269
+  def _reduce_98( val, _values, result )
+ result = datatype(:VARCHAR,   val[2], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 270
+  def _reduce_99( val, _values, result )
+ result = datatype(:TEXT,      nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 271
+  def _reduce_100( val, _values, result )
+ result = datatype(:BIT,       nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 272
+  def _reduce_101( val, _values, result )
+ result = datatype(:DATETIME,  nil,    val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 273
+  def _reduce_102( val, _values, result )
+ result = datatype(:TIMESTAMP, nil,    val)
+   result
+  end
+.,.,
+
+ # reduce 103 omitted
+
+ # reduce 104 omitted
+
+ # reduce 105 omitted
+
+ # reduce 106 omitted
+
+ # reduce 107 omitted
+
+ # reduce 108 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 289
+  def _reduce_109( val, _values, result )
+ result = nd(:QUERY_SPECIFICATION, val)
+   result
+  end
+.,.,
+
+ # reduce 110 omitted
+
+ # reduce 111 omitted
+
+ # reduce 112 omitted
+
+ # reduce 113 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 301
+  def _reduce_114( val, _values, result )
+ result = nd(:TABLE_EXPRESSION, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 305
+  def _reduce_115( val, _values, result )
+ result = nd(:FROM_CLAUSE, val[1], val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 309
+  def _reduce_116( val, _values, result )
+ result = nd(:WHERE_CLAUSE, val)
+   result
+  end
+.,.,
+
+ # reduce 117 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 316
+  def _reduce_118( val, _values, result )
+ result = nd(:BOOLEAN_VALUE_EXPRESSION, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 318
+  def _reduce_119( val, _values, result )
+ result = nd(:BOOLEAN_VALUE_EXPRESSION, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 322
+  def _reduce_120( val, _values, result )
+ result = nd(:BOOLEAN_TERM, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 324
+  def _reduce_121( val, _values, result )
+ result = nd(:BOOLEAN_TERM, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 328
+  def _reduce_122( val, _values, result )
+ result = nd(:BOOLEAN_FACTER, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 330
+  def _reduce_123( val, _values, result )
+ result = nd(:BOOLEAN_FACTER, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 334
+  def _reduce_124( val, _values, result )
+ result = nd(:BOOLEAN_TEST, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 336
+  def _reduce_125( val, _values, result )
+ result = nd(:BOOLEAN_TEST, val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 338
+  def _reduce_126( val, _values, result )
+ result = nd(:BOOLEAN_TEST, val)
+   result
+  end
+.,.,
+
+ # reduce 127 omitted
+
+ # reduce 128 omitted
+
+ # reduce 129 omitted
+
+ # reduce 130 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 352
+  def _reduce_131( val, _values, result )
+ result = nd(:PREDICATE, val)
+   result
+  end
+.,.,
+
+ # reduce 132 omitted
+
+ # reduce 133 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 359
+  def _reduce_134( val, _values, result )
+ result = nd(:COMP_OP, '>=', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 360
+  def _reduce_135( val, _values, result )
+ result = nd(:COMP_OP, '>=', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 361
+  def _reduce_136( val, _values, result )
+ result = nd(:COMP_OP, '>=', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 362
+  def _reduce_137( val, _values, result )
+ result = nd(:COMP_OP, '>=', val)
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 363
+  def _reduce_138( val, _values, result )
+ result = nd(:COMP_OP, '>=', val)
+   result
+  end
+.,.,
+
+ def _reduce_none( val, _values, result )
+  result
+ end
+
+    end   # class Parser
+
+  end   # module SQL
+
+end   # module Hdboo
diff --git a/lib/hdboo/sqlparser.tab.rb b/lib/hdboo/sqlparser.tab.rb
new file mode 100644 (file)
index 0000000..09889b3
--- /dev/null
@@ -0,0 +1,727 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by racc 1.4.5
+# from racc grammer file "sqlparser.y".
+#
+
+require 'racc/parser'
+
+
+require 'strscan'
+
+
+module Rdboo
+
+  class SQLParser < Racc::Parser
+
+module_eval <<'..end sqlparser.y modeval..id847e574633', 'sqlparser.y', 171
+# token
+PUNCTUATION = /([;,()]|\$\$)/
+KEYWORD_LIST = [
+  'CREATE', 'TABLE', 'NOT', 'NULL', 'AUTO_INCREMENT', 'DEFAULT',
+  'INT', 'VARCHAR', 'PROCEDURE', 'IN', 'OUT', 'INOUT', 'KEY', 
+  'PRIMARY', 'UNIQUE', 'INDEX', 'BEGIN', 'END', 'INTEGER', 'TINYINT',
+  'SMALLINT', 'DELIMITER'
+]
+KEYWORD = /(#{KEYWORD_LIST.collect{|k| '\b'+k+'\b'}.join('|')})/ix
+IDENTIFIER = /([a-zA-Z][_0-9a-zA-Z]*)/
+
+# NUMBER
+NUMERIC = /([1-9][0-9]*)/
+INTEGER = /([+|-]?[1-9][0-9]*)/
+FLOAT   = /(
+            (?: #without exponent part
+              [+|-]?(?:[1-9][0-9]*|0)(?:\.[0-9]+)?
+            |   #with exponent part
+              [+|-]?[1-9]+(?:\.[0-9]+)?e[+|-]?[1-9][0-9]*
+            )
+          )/x
+
+# STRING
+NOT_PRINTABLE = '\x00-\x1F\x7F' #including space (\x20)
+MUST_BE_ESCAPED = '\x22\x27\x5C' #quot, double-quot, back-slash
+ESCAPE_TABLE = {
+  '\n'   => "\n",
+  '\b'   => "\b",
+  '\0'   => "\x00",
+  '\t'   => "\t",
+  "\\'"  => "'",
+  '\"'   => '"'
+} 
+ESCAPED = ESCAPE_TABLE.keys.join('|')
+CHARACTER_STRING = /(
+  #single-quoted string
+  ' 
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ''
+    |
+      "
+    )*
+  '
+  |
+  #double-quoted string
+  "
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ""
+    |
+      '
+    )*
+  "
+)/x
+
+# PROCEDURE
+PROCEDURE_CODE = /(BEGIN.*END)/
+PARAMETER_NAME = /([_a-zA-Z][_0-9a-zA-Z]*)/
+
+def parse(str)
+  @lines = str.split(/\n+/)
+  next_line
+  do_parse
+end
+
+def next_line
+  @line = @lines.empty? \
+        ? nil \
+        : StringScanner.new(@lines.shift)
+end
+
+def next_token
+  token = _next_token
+  p token
+  token
+end
+
+def _next_token
+  return nil unless @line
+  @line.skip(/\s*/)
+  if @in_procedure_code
+    if @line.skip_until(/\bEND\b/i)
+      @in_procedure_code = false
+      return ['END', 'END']
+    end
+    @line.terminate
+  end
+  if @line.eos?
+    next_line
+    return _next_token
+  end
+  if token = @line.scan(PUNCTUATION)
+    return [token, token]
+  elsif token = @line.scan(KEYWORD)
+    token = token.upcase
+    if token == 'BEGIN' then @in_procedure_code = true end
+    return [token, token]
+  elsif token = @line.scan(IDENTIFIER)
+    return [:IDENTIFIER, token]
+  elsif token = @line.scan(PARAMETER_NAME)
+    return [:PARAMETER_NAME, token]
+  elsif token = @line.scan(NUMERIC)
+    return [:NUMERIC, token.to_i]
+  elsif token = @line.scan(CHARACTER_STRING)
+    return [
+      :CHARACTER_STRING,
+      token[1...-1].gsub(/#{ESCAPED}/) {|s|ESCAPE_TABLE[token]}\
+                   .gsub(/""/, '"')\
+                   .gsub(/''/, "\'")
+    ]
+  elsif token = @line.scan(INTEGER)
+    return [:INTEGER, token.to_i]
+  elsif token = @line.scan(FLOAT)
+    return [:FLOAT, token.to_f]
+  else
+    return [:UNEXPECTED_TOKEN, @line.scan(/.*/)]
+  end
+end
+
+
+..end sqlparser.y modeval..id847e574633
+
+##### racc 1.4.5 generates ###
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 1, 36, :_reduce_none,
+ 0, 37, :_reduce_2,
+ 1, 37, :_reduce_3,
+ 3, 37, :_reduce_4,
+ 3, 37, :_reduce_5,
+ 1, 38, :_reduce_none,
+ 1, 38, :_reduce_none,
+ 1, 38, :_reduce_none,
+ 6, 39, :_reduce_9,
+ 1, 42, :_reduce_10,
+ 3, 42, :_reduce_11,
+ 1, 43, :_reduce_none,
+ 1, 43, :_reduce_none,
+ 3, 44, :_reduce_14,
+ 0, 47, :_reduce_15,
+ 2, 47, :_reduce_16,
+ 2, 48, :_reduce_17,
+ 1, 48, :_reduce_18,
+ 2, 48, :_reduce_19,
+ 2, 48, :_reduce_20,
+ 1, 48, :_reduce_21,
+ 4, 45, :_reduce_22,
+ 1, 50, :_reduce_none,
+ 1, 50, :_reduce_none,
+ 1, 50, :_reduce_none,
+ 2, 52, :_reduce_26,
+ 1, 53, :_reduce_27,
+ 2, 53, :_reduce_28,
+ 2, 53, :_reduce_29,
+ 1, 54, :_reduce_30,
+ 1, 54, :_reduce_31,
+ 1, 51, :_reduce_32,
+ 3, 51, :_reduce_33,
+ 7, 40, :_reduce_34,
+ 0, 55, :_reduce_35,
+ 1, 55, :_reduce_36,
+ 3, 55, :_reduce_37,
+ 3, 57, :_reduce_38,
+ 3, 57, :_reduce_39,
+ 0, 58, :_reduce_none,
+ 1, 58, :_reduce_none,
+ 1, 58, :_reduce_none,
+ 1, 58, :_reduce_none,
+ 2, 56, :_reduce_none,
+ 1, 46, :_reduce_45,
+ 1, 46, :_reduce_46,
+ 1, 46, :_reduce_47,
+ 1, 46, :_reduce_48,
+ 4, 46, :_reduce_49,
+ 1, 49, :_reduce_none,
+ 1, 49, :_reduce_none,
+ 1, 59, :_reduce_none,
+ 1, 59, :_reduce_none,
+ 1, 41, :_reduce_none ]
+
+racc_reduce_n = 55
+
+racc_shift_n = 85
+
+racc_action_table = [
+     8,     8,     8,    24,   -35,    50,    12,    49,    54,    55,
+    24,    58,    60,    61,    62,    35,    37,    39,    13,    52,
+    26,    28,    31,    21,     1,     1,     1,    26,    28,    31,
+    21,   -35,    10,    11,    75,    78,    79,    42,    43,    44,
+    45,    40,    35,    37,    39,    76,    42,    43,    44,    45,
+    40,    42,    43,    44,    45,    40,    58,    60,    61,    62,
+    51,    48,    53,    47,    56,    46,    63,    20,    67,    19,
+    18,    17,    71,    72,    14,     9,    80,    81,    82,    83,
+    63 ]
+
+racc_action_check = [
+     0,    11,    10,    51,    20,    31,     8,    31,    38,    38,
+    19,    59,    59,    59,    59,    20,    20,    20,     8,    34,
+    51,    51,    51,    51,     0,    11,    10,    19,    19,    19,
+    19,    53,     4,     4,    61,    61,    61,    24,    24,    24,
+    24,    24,    53,    53,    53,    61,    55,    55,    55,    55,
+    55,    54,    54,    54,    54,    54,    41,    41,    41,    41,
+    33,    30,    36,    26,    40,    25,    46,    18,    52,    17,
+    13,    12,    56,    58,     9,     2,    63,    64,    67,    71,
+    80 ]
+
+racc_action_pointer = [
+   -10,   nil,    75,   nil,    24,   nil,   nil,   nil,    -5,    74,
+    -8,    -9,    69,    68,   nil,   nil,   nil,    57,    55,     8,
+    -9,   nil,   nil,   nil,     8,    53,    43,   nil,   nil,   nil,
+    48,   -15,   nil,    46,     6,   nil,    48,   nil,     6,   nil,
+    52,    41,   nil,   nil,   nil,   nil,    64,   nil,   nil,   nil,
+   nil,     1,    41,    18,    22,    17,    68,   nil,    57,    -4,
+   nil,    29,   nil,    62,    64,   nil,   nil,    50,   nil,   nil,
+   nil,    66,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+    78,   nil,   nil,   nil,   nil ]
+
+racc_action_default = [
+    -2,   -54,   -55,    -1,    -3,    -6,    -7,    -8,   -55,   -55,
+    -2,    -2,   -55,   -55,    85,    -4,    -5,   -55,   -55,   -55,
+   -40,   -30,   -12,   -13,   -55,   -55,   -55,   -23,   -31,   -24,
+   -55,   -27,   -25,   -10,   -55,   -41,   -36,   -42,   -55,   -43,
+   -55,   -15,   -45,   -46,   -47,   -48,   -55,   -26,    -9,   -29,
+   -28,   -55,   -55,   -40,   -55,   -55,   -55,   -14,   -55,   -15,
+   -18,   -55,   -21,   -32,   -55,   -11,   -34,   -55,   -37,   -39,
+   -38,   -55,   -17,   -16,   -51,   -50,   -20,   -19,   -52,   -53,
+   -55,   -22,   -44,   -49,   -33 ]
+
+racc_goto_table = [
+    64,    30,    34,    57,     3,    77,    41,     2,    66,    74,
+   nil,   nil,   nil,   nil,    15,    16,   nil,   nil,   nil,   nil,
+   nil,    73,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,   nil,   nil,    65,    84,    68,    69,    70 ]
+
+racc_goto_check = [
+    16,     7,    20,    12,     2,    14,    11,     1,    21,    24,
+   nil,   nil,   nil,   nil,     2,     2,   nil,   nil,   nil,   nil,
+   nil,    12,   nil,   nil,   nil,   nil,   nil,   nil,   nil,   nil,
+   nil,   nil,   nil,     7,    16,    20,    11,    11 ]
+
+racc_goto_pointer = [
+   nil,     7,     4,   nil,   nil,   nil,   nil,   -18,   nil,   nil,
+   nil,   -18,   -38,   nil,   -56,   nil,   -46,   nil,   nil,   nil,
+   -18,   -44,   nil,   nil,   -52 ]
+
+racc_goto_default = [
+   nil,   nil,   nil,     4,     5,     6,     7,   nil,    33,    22,
+    23,   nil,   nil,    59,   nil,    25,   nil,    27,    29,    32,
+   nil,   nil,    36,    38,   nil ]
+
+racc_token_table = {
+ false => 0,
+ Object.new => 1,
+ :IDENTIFIER => 2,
+ :PARAMETER_NAME => 3,
+ :NUMERIC => 4,
+ :CHARACTER_STRING => 5,
+ :INTEGER => 6,
+ :FLOAT => 7,
+ ";" => 8,
+ "$$" => 9,
+ "CREATE" => 10,
+ "TABLE" => 11,
+ "(" => 12,
+ ")" => 13,
+ "," => 14,
+ "NOT" => 15,
+ "NULL" => 16,
+ "DEFAULT" => 17,
+ "AUTO_INCREMENT" => 18,
+ "PRIMARY" => 19,
+ "KEY" => 20,
+ "UNIQUE" => 21,
+ "INDEX" => 22,
+ "PROCEDURE" => 23,
+ "IN" => 24,
+ "OUT" => 25,
+ "INOUT" => 26,
+ "BEGIN" => 27,
+ "END" => 28,
+ "INT" => 29,
+ "INTEGER" => 30,
+ "TINYINT" => 31,
+ "SMALLINT" => 32,
+ "VARCHAR" => 33,
+ "DELIMITER" => 34 }
+
+racc_use_result_var = true
+
+racc_nt_base = 35
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+'$end',
+'error',
+'IDENTIFIER',
+'PARAMETER_NAME',
+'NUMERIC',
+'CHARACTER_STRING',
+'INTEGER',
+'FLOAT',
+'";"',
+'"$$"',
+'"CREATE"',
+'"TABLE"',
+'"("',
+'")"',
+'","',
+'"NOT"',
+'"NULL"',
+'"DEFAULT"',
+'"AUTO_INCREMENT"',
+'"PRIMARY"',
+'"KEY"',
+'"UNIQUE"',
+'"INDEX"',
+'"PROCEDURE"',
+'"IN"',
+'"OUT"',
+'"INOUT"',
+'"BEGIN"',
+'"END"',
+'"INT"',
+'"INTEGER"',
+'"TINYINT"',
+'"SMALLINT"',
+'"VARCHAR"',
+'"DELIMITER"',
+'$start',
+'sql',
+'sql_command_list',
+'sql_command',
+'base_table_definition',
+'procedure_definition',
+'alter_delimiter',
+'base_table_element_list',
+'base_table_element',
+'column_definition',
+'base_table_constraint_definition',
+'data_type',
+'column_option_list',
+'column_option',
+'constant',
+'constraint_type',
+'constrained_column_list',
+'primary_key',
+'unique',
+'key',
+'parameter_list',
+'procedure_code',
+'parameter',
+'parameter_direction',
+'number']
+
+Racc_debug_parser = false
+
+##### racc system variables end #####
+
+ # reduce 0 omitted
+
+ # reduce 1 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 15
+  def _reduce_2( val, _values, result )
+ result = [:SQL_COMMAND_LIST]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 17
+  def _reduce_3( val, _values, result )
+ result = [:SQL_COMMAND_LIST, val[0]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 19
+  def _reduce_4( val, _values, result )
+ result = val[2].insert(1, val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 21
+  def _reduce_5( val, _values, result )
+ result = val[2].insert(1, val[0])
+   result
+  end
+.,.,
+
+ # reduce 6 omitted
+
+ # reduce 7 omitted
+
+ # reduce 8 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 30
+  def _reduce_9( val, _values, result )
+ result = [:CREATE_TABLE, val[2], val[4]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 34
+  def _reduce_10( val, _values, result )
+ result = [:BASE_TABLE_ELEMENT_LIST, val[0]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 36
+  def _reduce_11( val, _values, result )
+ result = val[2].insert(1, val[0])
+   result
+  end
+.,.,
+
+ # reduce 12 omitted
+
+ # reduce 13 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 44
+  def _reduce_14( val, _values, result )
+ result = [:COLUMN_DEFINITION, val[0], val[1], val[2]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 47
+  def _reduce_15( val, _values, result )
+ result = [:COLUMN_OPTION_LIST]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 49
+  def _reduce_16( val, _values, result )
+ result = val[1].insert(1, val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 52
+  def _reduce_17( val, _values, result )
+ result = [:COLUMN_OPTION, :NOT_NULL]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 53
+  def _reduce_18( val, _values, result )
+ result = [:COLUMN_OPTION, :NULL]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 54
+  def _reduce_19( val, _values, result )
+ result = [:COLUMN_OPTION, :DEFAULT, val[1]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 55
+  def _reduce_20( val, _values, result )
+ result = [:COLUMN_OPTION, :DEFAULT, :NULL]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 56
+  def _reduce_21( val, _values, result )
+ result = [:COLUMN_OPTION, :AUTO_INCREMENT]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 60
+  def _reduce_22( val, _values, result )
+ result = [:BASE_TABLE_COSTRAINT_DEFINITION, val[0], val[2]]
+   result
+  end
+.,.,
+
+ # reduce 23 omitted
+
+ # reduce 24 omitted
+
+ # reduce 25 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 68
+  def _reduce_26( val, _values, result )
+ result = :PRIMARY_KEY
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 71
+  def _reduce_27( val, _values, result )
+ result = :UNIQUE
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 72
+  def _reduce_28( val, _values, result )
+ result = :UNIQUE
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 73
+  def _reduce_29( val, _values, result )
+ result = :UNIQUE
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 76
+  def _reduce_30( val, _values, result )
+ result = :KEY
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 77
+  def _reduce_31( val, _values, result )
+ result = :KEY
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 81
+  def _reduce_32( val, _values, result )
+ result = [:CONSTRAINED_COLUMN_LIST, val[0]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 83
+  def _reduce_33( val, _values, result )
+ result = val[2].insert(1, val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 87
+  def _reduce_34( val, _values, result )
+ result = [:CREATE_PROCEDURE, val[2], val[4]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 90
+  def _reduce_35( val, _values, result )
+ result = [:PARAMETER_LIST]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 92
+  def _reduce_36( val, _values, result )
+ result = [:PARAMETER_LIST, val[0]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 94
+  def _reduce_37( val, _values, result )
+ result = val[2].insert(1, val[0])
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 98
+  def _reduce_38( val, _values, result )
+ result = [:PARAMETER, val[1], val[2]]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 100
+  def _reduce_39( val, _values, result )
+ result = [:PARAMETER, val[1], val[2]]
+   result
+  end
+.,.,
+
+ # reduce 40 omitted
+
+ # reduce 41 omitted
+
+ # reduce 42 omitted
+
+ # reduce 43 omitted
+
+ # reduce 44 omitted
+
+module_eval <<'.,.,', 'sqlparser.y', 111
+  def _reduce_45( val, _values, result )
+ result = [:DATA_TYPE, :INTEGER]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 112
+  def _reduce_46( val, _values, result )
+ result = [:DATA_TYPE, :INTEGER]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 113
+  def _reduce_47( val, _values, result )
+ result = [:DATA_TYPE, :TINYINT]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 114
+  def _reduce_48( val, _values, result )
+ result = [:DATA_TYPE, :SMALLINT]
+   result
+  end
+.,.,
+
+module_eval <<'.,.,', 'sqlparser.y', 115
+  def _reduce_49( val, _values, result )
+ result = [:DATA_TYPE, :VARCHAR, val[2]]
+   result
+  end
+.,.,
+
+ # reduce 50 omitted
+
+ # reduce 51 omitted
+
+ # reduce 52 omitted
+
+ # reduce 53 omitted
+
+ # reduce 54 omitted
+
+ def _reduce_none( val, _values, result )
+  result
+ end
+
+  end   # class SQLParser
+
+end   # module Rdboo
+
+class Rdboo::SQLParser::ASTWalker
+  def walk(ast)
+    walk_node(ast)
+    self
+  end
+  
+  def initialize
+    @listeners = Hash.new {|hash, key| hash[key] = []}
+  end
+  
+  def visit(note_type, &block)
+    node_type = note_type.to_sym
+    @listeners[node_type] << block
+    self
+  end
+  
+  private
+  def walk_node(node)
+    node_type = node.is_a?(Array) \
+              ? node.shift \
+              : node
+    
+    if @listeners.has_key?(node_type)
+      @listeners[node_type].each do |listener|
+        listener.call(node)
+      end
+    end
+    
+    if node.is_a?(Array)
+      node.each do |child_node|
+        walk_node(child_node)
+      end
+    end
+  end
+end
+
diff --git a/lib/hdboo/sqlparser.y b/lib/hdboo/sqlparser.y
new file mode 100644 (file)
index 0000000..9895a35
--- /dev/null
@@ -0,0 +1,780 @@
+class Hdboo::SQL::Parser
+
+token
+  IDENTIFIER
+  PARAMETER_NAME
+  NUMERIC
+  CHARACTER_STRING
+  INTEGER
+  FLOAT
+  PROCEDURE_CODE
+
+rule
+  sql
+    : module_annotation sql_command_list
+        { result = nd(:SQL, val)}
+
+  module_annotation
+    : annotation_specification
+        { result = nd(:MODULE_ANNOTATION, val[0].children, val) }
+
+  sql_command_list
+    :
+        { result = nd(:SQL_COMMAND_LIST, [], val) }
+    | sql_command
+        { result = nd(:SQL_COMMAND_LIST, [val[0]], val) }
+    | sql_command ';' sql_command_list
+        { update_pos(val[2], val)
+          result = val[2].unshift(val[0]) }
+    | sql_command '$$' sql_command_list
+        { update_pos(val[2], val)
+          result = val[2].unshift(val[0]) }
+    | sql_command '//' sql_command_list
+        { update_pos(val[2], val)
+          result = val[2].unshift(val[0]) }
+      
+  sql_command
+    : table_definition
+    | view_definition
+    | procedure_definition
+    | insert_statement
+    | alter_delimiter
+  
+  qualified_name
+    : IDENTIFIER                     
+    | qualified_name '.' IDENTIFIER 
+        { result = nd(:IDENTIFIER, val[0].value + val[1].value + val[2].value, val)}
+  
+  table_definition
+    : 'CREATE' 'TABLE' IDENTIFIER '('
+      annotation_specification table_element_list
+      ')' table_option_list
+        { result = table(val[2], val[5], val[4], val) }
+  
+  view_definition
+    : 'CREATE' 'VIEW' IDENTIFIER annotation_specification
+      'AS' query_specification
+        { result = nd(:CREATE_VIEW, [val[2], val[5], val[3]], val) }
+  
+  table_option_list
+    :
+        { result = nd(:TABLE_OPTION_LIST, [], val) }
+    | table_option table_option_list
+        { update_pos(val[1], val)
+          result = val[1].unshift(val[0]) }
+  
+  table_option
+    : 'ENGINE' '=' engine_type
+        { result = nd(:ENGINE, val[2], val)}
+    
+  engine_type
+    : 'MYISAM'
+    | 'INNODB'
+    | 'HEAP'
+
+  table_element_list
+    : table_element
+        { result = nd(:TABLE_ELEMENT_LIST, [val[0]], val) }
+    | table_element ',' table_element_list
+        { update_pos(val[2], val)
+          result = val[2].unshift(val[0]) }
+          
+  table_element
+    : column_definition
+    | constraint_definition
+    
+  column_definition
+    : IDENTIFIER annotation_specification data_type column_option_list
+        { result = column(val[0], val[2], val[3], val[1], val) }
+    
+  column_option_list
+    :
+        { result = nd(:COLUMN_OPTION_LIST, [], val) }
+    | column_option column_option_list
+        { update_pos(val[0], val)
+          result = val[1].unshift(val[0]) }
+    
+  column_option
+    : 'NOT' 'NULL'       { result = nd(:NOT_NULL, val) }
+    | 'NULL'             { result = nd(:NULL, val) }
+    | 'DEFAULT' literal  { result = nd(:DEFAULT, val[1], val) }
+    | 'DEFAULT' 'NULL'   { result = nd(:DEFAULT, nil, val) }
+    | 'AUTO_INCREMENT'   { result = nd(:AUTO_INCREMENT, val) }
+  
+  constraint_definition
+    : primary_key
+    | foreign_key
+    | unique
+    | index
+    | fulltext_index
+    
+  primary_key
+    : constraint_name_definition 'PRIMARY' 'KEY' '(' constrained_column_list ')'
+        { result = constraint(:PrimaryKey, val[0].value, val[4].to_a, val) }
+  
+  foreign_key 
+    : constraint_name_definition 'FOREIGN' 'KEY' '(' constrained_column_list ')'
+      references_specification
+        { referenced_table, referenced_columns = *(val[6].value)
+          result = constraint(:ReferencesConstraint,
+                     val[0].value, val[4].to_a,
+                     referenced_table, referenced_columns, val) }
+                     
+  references_specification
+    : 'REFERENCES' IDENTIFIER triggered_action
+        { result = nd(:REFERENCES_SPECIFICATION, [val[1].value, []], val) }
+    | 'REFERENCES' IDENTIFIER '(' constrained_column_list ')' triggered_action
+        { result = nd(:REFERENCES_SPECIFICATION, [val[1].value, val[4].value], val) }
+    
+  triggered_action
+    :                         { result = nd(:TRIGGERED_ACTION, val) }
+    | 'ON' 'DELETE' 'CASCADE' { result = nd(:TRIGGERED_ACTION, val) }
+  
+  unique
+    : constraint_name_definition 'UNIQUE' '(' constrained_column_list ')'
+        { result = constraint(:Unique, val[0].value, val[3].to_a, val) }
+  
+  constraint_name_definition 
+    :                          { result = nd(:CONSTRAINT_NAME, '-', val) }
+    | 'CONSTRAINT' IDENTIFIER  { result = nd(:CONSTRAINT_NAME, val[1].value, val) }
+  
+  index
+    : 'INDEX' index_name_definition '(' constrained_column_list ')'
+        { result = index(val[1].value, val[3].value, val) }
+  
+  fulltext_index
+    : 'FULLTEXT' index_name_definition using '(' constrained_column_list ')'
+        { result = index(val[1].value, val[4].value, val) }
+  
+  using
+    : 'USING' 'SECTIONALIZE'
+        { result = nd(:USING, val)}
+
+  index_name_definition
+    :            { result = nd(:CONSTRAINT_NAME, '<NO_NAME>', val) }
+    | IDENTIFIER { result = nd(:CONSTRAINT_NAME, val[0].value, val) }
+  
+  constrained_column_list
+    : IDENTIFIER
+        { result = nd(:CONSTRAINED_COLUMN_LIST, [val[0]], val) }
+    | IDENTIFIER ',' constrained_column_list
+        { update_pos(val[2], val)
+          result = val[2].unshift(val[0]) }
+
+  procedure_definition
+    : 'CREATE' 'PROCEDURE' IDENTIFIER '(' parameter_list ')'
+      annotation_specification
+      'BEGIN' PROCEDURE_CODE 'END'
+        { result = procedure(val[2], val[4], val[6], val) }
+  
+  parameter_list
+    : 
+        { result = nd(:PARAMETER_LIST, [], val)}
+    | parameter
+        { result = nd(:PARAMETER_LIST, [val[0]], val) }
+    | parameter ',' annotation_specification parameter_list
+        { val[0].value.annotations = val[2].to_a
+          update_pos(val[3], val)
+          result = val[3].unshift(val[0]) }
+          
+  parameter
+    : parameter_direction parameter_name data_type annotation_specification
+        { result = parameter(val[1], val[2], val[3], val) }
+        
+  parameter_name
+    : IDENTIFIER
+    | PARAMETER_NAME
+  
+  parameter_direction
+    :
+    | 'IN'
+    | 'OUT'
+    | 'INOUT'
+  
+  annotation_specification
+    : 
+        { result = nd(:ANNTATION_LIST, [], val) }
+    | annotation_specification '--' annotation_list
+        { update_pos(val[0], val)
+          val[0].value.concat(val[2].value)
+          result = val[0] }
+
+  annotation_list
+    : annotation
+        { result = nd(:ANNTATION_LIST, [val[0]], val) }
+    | annotation_list annotation
+        { update_pos(val[0], val)
+          result = val[0].push(val[1]) }
+      
+  annotation
+    : '@' IDENTIFIER
+        { result = annotation(val[1], [], val) }
+    | '@' IDENTIFIER '(' annotation_argument_list ')'
+        { result = annotation(val[1], val[3], val) }
+
+  annotation_argument_list
+    :
+        { result = nd(:ANNOTATION_ARGUMENT_LIST, [], val) }
+    | annotation_argument
+        { result = nd(:ANNOTATION_ARGUMENT_LIST, [val[0]], val) }
+    | annotation_argument ',' annotation_argument_list
+        { result = update_pos(val[2], val).unshift(val[0]) }
+    
+  annotation_argument
+    : literal
+    | qualified_name
+    | data_type 
+    
+  insert_statement
+    : 'INSERT' 'INTO' IDENTIFIER insert_columns_and_source
+        { result = nd(:INSERT, val) }
+    
+  insert_columns_and_source
+    : from_constructor
+  
+  from_constructor
+    : 'VALUES' column_value_expression_list
+      { result = update_pos(val[1], val) }
+    
+  column_value_expression_list
+    : column_value_expression
+        { result = nd(:COLUMN_VALUE_EXPRESSION_LIST, [], val) }
+    | column_value_expression ',' column_value_expression_list
+        { result = update_pos(val[2], val).unshift(val[0]) }
+    
+  column_value_expression
+    : '(' value_expression_list ')'
+        { result = update_pos(val[1], val) }
+  
+  value_expression_list
+    :
+        { result = nd(:VALUE_EXPRESSION_LIST, [], val) }
+    | value_expression
+        { result = nd(:VALUE_EXPRESSION_LIST, [val[0]], val) }
+    | value_expression ',' value_expression_list
+        { result = update_pos(val[2], val).unshift(val[0]) }
+    
+  value_expression
+    : literal
+    | qualified_name 
+
+  data_type
+    : 'INT'                      { result = datatype(:INTEGER,   nil,    val) }
+    | 'INTEGER'                  { result = datatype(:INTEGER,   nil,    val) }
+    | 'TINYINT'                  { result = datatype(:TINYINT,   nil,    val) }
+    | 'SMALLINT'                 { result = datatype(:SMALLINT,  nil,    val) }
+    | 'INTEGER'  '(' NUMERIC ')' { result = datatype(:INTEGER,   val[2], val) }
+    | 'TINYINT'  '(' NUMERIC ')' { result = datatype(:TINYINT,   val[2], val) }
+    | 'SMALLINT' '(' NUMERIC ')' { result = datatype(:SMALLINT,  val[2], val) }    
+    | 'CHAR'     '(' NUMERIC ')' { result = datatype(:CHAR,      val[2], val) }
+    | 'VARCHAR'  '(' NUMERIC ')' { result = datatype(:VARCHAR,   val[2], val) }
+    | 'TEXT'                     { result = datatype(:TEXT,      nil,    val) }
+    | 'BIT'                      { result = datatype(:BIT,       nil,    val) }
+    | 'DATETIME'                 { result = datatype(:DATETIME,  nil,    val) }
+    | 'TIMESTAMP'                { result = datatype(:TIMESTAMP, nil,    val) }
+
+  literal
+    : CHARACTER_STRING
+    | number
+    
+  number
+    : NUMERIC
+    | INTEGER
+    | FLOAT
+    
+  alter_delimiter
+    : 'DELIMITER'
+    
+  query_specification
+    : 'SELECT' select_list table_expression
+        { result = nd(:QUERY_SPECIFICATION, val) }
+  
+  select_list
+    : '*'
+    | select_sublist
+  
+  select_sublist
+    : IDENTIFIER
+    | IDENTIFIER ',' select_sublist
+    
+  table_expression
+    : from_clause where_clause
+       { result = nd(:TABLE_EXPRESSION, val) }
+    
+  from_clause
+    : 'FROM' IDENTIFIER
+        { result = nd(:FROM_CLAUSE, val[1], val)}
+    
+  where_clause
+    : 'WHERE' search_condition
+        { result = nd(:WHERE_CLAUSE, val) }
+   
+  search_condition
+    : boolean_value_expression
+    
+  boolean_value_expression
+    : boolean_term
+        { result = nd(:BOOLEAN_VALUE_EXPRESSION, val) }
+    | boolean_value_expression 'OR' boolean_term
+        { result = nd(:BOOLEAN_VALUE_EXPRESSION, val) }
+    
+  boolean_term
+    : boolean_facter
+        { result = nd(:BOOLEAN_TERM, val) }
+    | boolean_term 'AND' boolean_facter
+        { result = nd(:BOOLEAN_TERM, val) }
+  
+  boolean_facter
+    : boolean_test
+        { result = nd(:BOOLEAN_FACTER, val) }
+    | 'NOT' boolean_test
+        { result = nd(:BOOLEAN_FACTER, val) }
+  
+  boolean_test
+    : boolean_primary
+        { result = nd(:BOOLEAN_TEST, val) }
+    | boolean_primary 'IS' true_value
+        { result = nd(:BOOLEAN_TEST, val) }
+    | boolean_primary 'IS' 'NOT' true_value
+        { result = nd(:BOOLEAN_TEST, val) }
+    
+  true_value
+    : 'TRUE'
+    | 'FALSE'
+  
+  boolean_primary
+    : predicate
+    
+  predicate
+    : comparison_predicate
+    
+  comparison_predicate
+    : row_value_expression comp_op row_value_expression
+        { result = nd(:PREDICATE, val) }
+        
+  row_value_expression
+    : IDENTIFIER
+    | literal
+    
+  comp_op
+    : '='        { result = nd(:COMP_OP, '>=', val) }
+    | '<'        { result = nd(:COMP_OP, '>=', val) }
+    | '<' '='    { result = nd(:COMP_OP, '>=', val) }
+    | '>'        { result = nd(:COMP_OP, '>=', val) }
+    | '>' '='    { result = nd(:COMP_OP, '>=', val) }
+end
+
+----header
+require 'strscan'
+
+----inner
+def constraint(type, *args)
+  all_elements = args.pop
+  nd(:CONSTRAINT, SQL::Table::Constraint.new(type, args), all_elements)
+end
+
+def index(name, columns, all_elements)
+  nd(:INDEX, SQL::Table::Index.new(name, columns), all_elements)
+end
+
+def procedure(name, parameters, annotations, all_elements)
+  nd(:CREATE_PROCEDURE,
+     SQL::Procedure.new(
+       name.value,
+       parameters.to_a,
+       annotations.to_a
+     ),
+     all_elements)
+end
+
+def parameter(name, type, annotations, all_elements)
+  nd(:PARAMETER,
+     SQL::Procedure::Parameter.new(
+       name.value,
+       type.value,
+       annotations.to_a
+     ),
+     all_elements)
+end 
+
+def table(name, elements, annotations, all_elements)
+  nd(:CREATE_TABLE,
+     SQL::Table.new(name.value, elements.to_a, annotations.to_a),
+     all_elements)
+end
+
+def column(name, type, column_options, annotations, all_elements)
+  nd(:COLUMN_DEFINITION,
+     SQL::Table::Column.new(
+       name.value, type.value, column_options.to_a, annotations.to_a
+     ),
+     all_elements)
+end
+
+def annotation(type, args, all_elements)
+  type = type.value
+  args = args.to_a
+  nd(:ANNOTATION, SQL::Annotation.new(type, args), all_elements)
+end
+
+def datatype(type, length, val)
+  nd(:DATA_TYPE, SQL::DataType.new(type, length), val)
+end
+
+def nd(type, value, all_elements=value)
+  node = Node.new(type, value, @source)
+  update_pos(node, all_elements)
+  if value.respond_to?(:source=)
+    value.source = node.source
+  end
+  node
+end
+
+def update_pos(node, all_elements)
+  all_elements = all_elements.compact
+  node.start = all_elements.collect{|val| val.start}.compact[0]
+  node.last  = all_elements.collect{|val| val.last }.compact[-1]
+  node
+end
+
+class Node
+  attr_reader   :type, :value
+  attr_accessor :start, :last
+  
+  def initialize(type, value, source)
+    @type   = type.to_sym
+    @value  = value
+    @source = source
+  end
+  
+  def push(elem)
+    raise Exception if leaf?
+    @value << elem
+    self
+  end
+  
+  def unshift(elem)
+    raise Exception if leaf?
+    @value.unshift(elem)
+    self
+  end
+  
+  def [](index)
+    if leaf?
+      return index = 0 ? @value : nil
+    end
+    if index.is_a?(Integer)
+      return @value[index]
+    end
+    @value.detect{|val| val.type == index.to_sym}
+  end
+
+  def leaf?
+    !@value.is_a?(Array)
+  end
+  
+  def name
+    self[:IDENTIFIER].value
+  end
+  
+  def children
+    leaf? ? [] : @value
+  end
+  
+  def find(node_type)
+    node_type = node_type.to_sym
+    return self if type == node_type
+    if leaf?
+      return nil
+    end
+    result = nil
+    children.each do |child_node|
+      found = child_node.find(node_type)
+      if found
+        result = found
+        break
+      end
+    end
+    result
+  end
+  
+  def source
+    start_row = @start[0]
+    start_col = @start[1]
+    last_row  = @last[0]
+    last_col  = @last[1]
+
+    result = @source[start_row..last_row]
+    if result.length == 1
+      result[0] = result[0][start_col..last_col]
+    else
+      result[0]  = result[0][start_col..-1]
+      result[-1] = result[-1][0..last_col]
+    end
+    result.join("\n")
+  end
+  
+  def to_a
+    if leaf?
+      return [@value]
+    end
+    @value.collect {|child| child.value}
+  end
+
+  def to_s
+    "#{type}: at line #{start[0]+1}, column #{start[1]+1}\nsource> #{source}"
+  end
+  
+  def inspect
+    to_s
+  end
+end
+
+class ASTWalker
+  def walk(ast)
+    walk_node(ast)
+    self
+  end
+  
+  def initialize
+    @listeners = Hash.new {|hash, key| hash[key] = []}
+  end
+  
+  def visit(note_type, &block)
+    node_type = note_type.to_sym
+    @listeners[node_type] << block
+    self
+  end
+  
+  private
+  def walk_node(node)
+    if @listeners.has_key?(node.type)
+      @listeners[node.type].each do |listener|
+        listener.call(node)
+      end
+    end
+    node.children.each do |child_node|
+p node if child_node.nil?
+      walk_node(child_node)
+    end
+  end
+end
+
+# token
+PUNCTUATION = /([;\.,()=@*<>\[\]]|\$\$|\/\/|--)/
+ONE_LINE_COMMENT = /--\s[^@]*$/
+KEYWORD_LIST = [
+  #general term
+  'NOT',
+  'NULL',
+  'DEFAULT',
+  'AS',
+
+  # sql command
+  'CREATE',
+  'TABLE',
+  'VIEW',
+  'PROCEDURE',
+  'INSERT',
+  'INTO',
+  'VALUES',
+  'DELIMITER',
+  
+  # table option
+  'ENGINE',
+  'MYISAM',
+  'INNODB',
+  'HEAP',
+  
+  # column option
+  'AUTO_INCREMENT',
+  
+  # data type
+  'INT',
+  'CHAR',
+  'VARCHAR',
+  'TEXT',
+  'INTEGER',
+  'TINYINT',
+  'SMALLINT',
+  'BIT',
+  'DATETIME',
+  'TIMESTAMP',
+  
+  # procedure / function
+  'BEGIN',
+  'END',
+  'IN',
+  'OUT',
+  'INOUT',
+  
+  # constraint
+  'CONSTRAINT',
+  'PRIMARY',
+  'FOREIGN',
+  'REFERENCES',
+  'UNIQUE',
+  'INDEX',
+  'KEY',
+  'FULLTEXT',
+  'USING',
+  'SECTIONALIZE',
+  
+  #query
+  'SELECT',
+  'FROM',
+  'WHERE',
+  
+  #dml
+  'DELETE',
+  'UPDATE',
+  
+  #boolean expression
+  'TRUE',
+  'FALSE',
+  'IS',
+  
+  #triggered action
+  'ON',
+  'CASCADE'
+]
+KEYWORD = /(#{KEYWORD_LIST.collect{|k| '\b'+k+'\b'}.join('|')})/ix
+IDENTIFIER = /([a-zA-Z][_0-9a-zA-Z]*)/
+
+# NUMBER
+NUMERIC = /([1-9][0-9]*)/
+INTEGER = /([+|-]?[1-9][0-9]*|0)/
+FLOAT   = /(
+            (?: #without exponent part
+              [+|-]?(?:[1-9][0-9]*|0)(?:\.[0-9]+)?
+            |   #with exponent part
+              [+|-]?[1-9]+(?:\.[0-9]+)?e[+|-]?[1-9][0-9]*
+            )
+          )/x
+
+# STRING
+NOT_PRINTABLE = '\x00-\x1F\x7F' #including space (\x20)
+MUST_BE_ESCAPED = '\x22\x27\x5C' #quot, double-quot, back-slash
+ESCAPE_TABLE = {
+  '\n'   => "\n",
+  '\b'   => "\b",
+  '\0'   => "\x00",
+  '\t'   => "\t",
+  "\\'"  => "'",
+  '\"'   => '"',
+  "\\\\" => "\\"
+} 
+ESCAPED = ESCAPE_TABLE.keys.map{|k|k.gsub(/\\/, "\\\\\\\\")}.join('|')
+CHARACTER_STRING = /(
+  #single-quoted string
+  ' 
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ''
+    |
+      "
+    )*
+  '
+  |
+  #double-quoted string
+  "
+    (?:
+      [^#{MUST_BE_ESCAPED}#{NOT_PRINTABLE}]
+    |
+      #{ESCAPED}
+    |
+      ""
+    |
+      '
+    )*
+  "
+)/x
+
+# PROCEDURE
+PARAMETER_NAME = /([_a-zA-Z][_0-9a-zA-Z]*)/
+
+def parse(str)
+  @source = str.split(/\n/)
+  @lines  = @source.dup
+  @total_lines = @lines.length
+  next_line
+  do_parse
+end
+
+def next_line
+  @line = @lines.empty? \
+        ? nil \
+        : StringScanner.new(@lines.shift)
+end
+
+def current_line
+  @total_lines - (@lines.length + 1)
+end
+
+def pos
+  [current_line, @line.pos]
+end
+
+def next_token
+  token = _next_token
+  return nil unless token
+  token[1].start = @start_pos
+  token[1].last  = [pos[0], pos[1]-1]
+#puts token
+  return token
+end
+
+def _next_token
+  return nil unless @line
+  @line.skip(/\s*/)
+  if @in_procedure_code
+    if token = @line.skip_until(/(?=\bEND(?!\s+(?:IF|CASE|WHILE|LOOP))\b)/i)
+      @in_procedure_code = false
+      return tkn(:PROCEDURE_CODE)
+    end
+    @line.terminate
+  end
+  if @line.eos?
+    next_line
+    return _next_token
+  end
+  @start_pos = pos
+  if token = @line.scan(ONE_LINE_COMMENT)
+    return _next_token
+  elsif token = @line.scan(PUNCTUATION)
+    return tkn(token)
+  elsif token = @line.scan(KEYWORD)
+    token = token.upcase
+    if token == 'BEGIN'
+      @in_procedure_code = true
+    end
+    return tkn(token)
+  elsif token = @line.scan(IDENTIFIER)
+    return tkn(:IDENTIFIER, token)
+  elsif token = @line.scan(PARAMETER_NAME)
+    return tkn(:PARAMETER_NAME, token)
+  elsif token = @line.scan(CHARACTER_STRING)
+    return tkn(
+      :CHARACTER_STRING,
+      token[1..-2].gsub(/#{ESCAPED}/x) {|s|ESCAPE_TABLE[s]}\
+                  .gsub(/""/, '"')\
+                  .gsub(/''/, "\'")
+    )
+  elsif token = @line.scan(NUMERIC)
+    return tkn(:NUMERIC, token.to_i)
+  elsif token = @line.scan(INTEGER)
+    return tkn(:INTEGER, token.to_i)
+  elsif token = @line.scan(FLOAT)
+    return tkn(:FLOAT, token.to_f)
+  else
+    return tkn(:UNEXPECTED_TOKEN, @line.scan(/.*/))
+  end
+end
+
+def tkn(type, value=type)
+  [type, Node.new(type, value, @source)]
+end
+
diff --git a/lib/hdboo/stub/smtp.rb b/lib/hdboo/stub/smtp.rb
new file mode 100644 (file)
index 0000000..fb24e9f
--- /dev/null
@@ -0,0 +1,15 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'net/smtp'
+
+$__sent_mail_box = $__sent_mail_box || []
+class Net::SMTP
+  def self.sent_mail_box
+    $__sent_mail_box
+  end
+
+  def send_message(message, from, to)
+    Net::SMTP.sent_mail_box << message
+  end
+end
diff --git a/test/suite.rb b/test/suite.rb
new file mode 100644 (file)
index 0000000..de97278
--- /dev/null
@@ -0,0 +1,9 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'tc_base.rb'
+require 'tc_html.rb'
+require 'tc_database.rb'
+require 'tc_sql.rb'
+require 'tc_rest.rb'
+
diff --git a/test/tc_base.rb b/test/tc_base.rb
new file mode 100644 (file)
index 0000000..81eb743
--- /dev/null
@@ -0,0 +1,294 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'test/unit'
+require 'base64'
+require 'hdboo/base'
+
+include Hdboo
+
+class TestResource < Test::Unit::TestCase
+  def test_resource
+    3.times do
+      source_file = resource('hdboo/base.rb')
+      assert_not_nil source_file
+      assert_match   /module Hdboo/, source_file
+    end
+    assert ! resource_exist?('no_such/file')
+    assert_raise RuntimeError do
+      resource('no_such/file')
+    end
+  end
+  
+  def test_template
+    $:.unshift File.join(File.dirname(__FILE__), '..')
+    3.times do
+      page = template('testtemplate/sample.txt') do
+        @first_name = 'hogeo'
+        @last_name  = 'hogeta'
+        @occupation = 'programmer'
+        @data       = 'test_data'
+      end
+  
+      assert_not_nil page
+      assert_match /name\: hogeo hogeta/, page
+      assert_match /#{Base64.encode64('test_data')}/x, page
+    end
+  end
+  
+  def test_template_base_url_replace
+    Hdboo::TemplateContext.base_url = 'http://www.example.com/'
+    page = template('testtemplate/sample.html') {}
+    assert_match(/<base href='http:\/\/www.example.com\/' \/>/, page)
+  end
+end
+
+class TestHashAsObject < Test::Unit::TestCase
+  def test_hash_as_object
+    obj = HashAsObject.new(
+      'hoge'     => 'hoge_value',
+      'fuga'     => 'fuga_value',
+      'is_piyo'  => 0,
+      'has_hoge' => 1
+    )
+    assert_equal 'hoge_value', obj.hoge
+    assert_equal 'fuga_value', obj.fuga
+    assert !obj.piyo?
+    assert  obj.has_hoge?
+    assert_raise(NoMethodError) do
+      obj.bar\r    end
+    
+    obj = HashAsObject.new(
+      :hoge     => 'hoge_value',
+      :fuga     => 'fuga_value',
+      :is_piyo  => 0,
+      :has_hoge => 1
+    )
+    assert_equal 'hoge_value', obj.hoge
+    assert_equal 'fuga_value', obj.fuga
+    assert !obj.piyo?
+    assert  obj.has_hoge?
+    assert_raise(NoMethodError) do
+      obj.bar
+    end
+  end
+end
+
+class TestValidatable < Test::Unit::TestCase
+  class ValidatableObject
+    include Validatable
+    attr_accessor :name, :data
+    def initialize(name, data)
+      @name, @data = name, data
+    end
+    
+    def validators_for(key)
+      case key.to_sym
+      when :name then [
+        Validator::NotNil.new,
+        Validator::NotBlank.new,
+        Validator::KanaKanji.new
+      ]
+      when :data then [
+        Validator::NotNil.new
+      ]
+      end
+    end
+    
+    def to_hash
+      {:name => @name, :data => @data}
+    end
+  end
+
+  def test_validate
+    hogeta = ValidatableObject.new(
+      'ほげ田ほげ夫',
+     'age:45, gender:male, occupation:Neet'
+    )
+    assert !hogeta.invalid?
+    assert_nothing_raised do
+      hogeta.validate\r    end
+    
+    hogeta.name = nil
+    exception = nil
+    begin
+      hogeta.validate
+    rescue
+      exception = $!
+    end
+    
+    assert       hogeta.invalid?
+    assert !     exception.nil?
+    assert       exception.is_a?(InvalidArgument)
+    messages = exception.messages
+    assert_equal 1, messages.size
+    assert_equal 'must not be nil.', messages[:name].first
+    
+    hogeta.name = ''
+    exception = nil
+    begin
+      hogeta.validate
+    rescue
+      exception = $!
+    end
+    
+    assert       hogeta.invalid?
+    assert !     exception.nil?
+    assert       exception.is_a?(InvalidArgument)
+    messages = exception.messages
+    assert_equal 1, messages.size
+    assert_equal 'must not be blank.', messages[:name].first
+    
+    hogeta.name = 'hogeta-hogeo'
+    hogeta.data = nil
+    exception = nil
+    begin
+      hogeta.validate
+    rescue
+      exception = $!
+    end
+    
+    assert       hogeta.invalid?
+    assert !     exception.nil?
+    assert       exception.is_a?(InvalidArgument)
+    messages = exception.messages
+    assert_equal 2, messages.size
+    assert_equal 'must be japanese kana or kanji, but was hogeta-hogeo.',
+                 messages[:name].first
+    assert_equal 'must not be nil.', messages[:data].first
+  end
+  
+end
+class TestValidator < Test::Unit::TestCase
+  def test_NotNil
+    validator = Validator::NotNil.new
+    assert  validator.pass?(1)
+    assert !validator.pass?(nil)
+  end
+  
+  def test_NotBlank
+    validator = Validator::NotBlank.new
+    assert  validator.pass?(nil)
+    assert !validator.pass?('')
+    assert  validator.pass?('test')
+  end
+  
+  def test_In
+    validator = Validator::In.new(5..6)
+    assert  validator.pass?(nil)
+    assert  validator.pass?(6)
+    assert !validator.pass?(1)
+  end
+  
+  def test_MaxLength
+    validator = Validator::MaxLength.new(10)
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?((['a']*10).join)
+    assert !validator.pass?((['a']*11).join)
+    assert  validator.pass?((['あ']*10).join)
+    assert !validator.pass?((['あ']*11).join)
+    assert  validator.pass?((['あa']*5).join)
+    assert !validator.pass?((['あa']*5).join + 'a')
+  end
+  
+  def test_MinLength
+    validator = Validator::MinLength.new(10)
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?((['a']*10).join)
+    assert !validator.pass?((['a']*9).join)
+    assert  validator.pass?((['あ']*10).join)
+    assert !validator.pass?((['あ']*9).join)
+    assert  validator.pass?((['あa']*5).join)
+    assert !validator.pass?((['あa']*4).join + 'a')
+  end
+  
+  def test_Printable
+    validator = Validator::Printable.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('aa')
+    assert  validator.pass?('dradt|ed_e@_edwr')
+    assert !validator.pass?('drayte drwed')
+  end
+  
+  def test_Hiragana
+    validator = Validator::Hiragana.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('いろはにほへと')
+    assert !validator.pass?('いろはにホへと')
+    assert !validator.pass?('いろはに歩へと')
+    assert !validator.pass?('いろはにHoへと')
+  end
+  
+  def test_Katakana
+    validator = Validator::Katakana.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('イロハニホヘト')
+    assert !validator.pass?('イロハニほヘト')
+    assert !validator.pass?('イロハニ歩ヘト')
+    assert !validator.pass?('イロハニHoヘト')
+  end
+
+  def test_Kanji
+    validator = Validator::Kanji.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('意炉葉二歩屁戸')
+    assert !validator.pass?('意炉葉二ほ屁戸')
+    assert !validator.pass?('意炉葉二ホ屁戸')
+    assert !validator.pass?('意炉葉二Ho屁戸')
+  end
+  
+  def test_Kana
+    validator = Validator::Kana.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('イロハニホヘト')
+    assert  validator.pass?('イロハニほヘト')
+    assert !validator.pass?('イロハニ歩ヘト')
+    assert !validator.pass?('イロハニHoヘト')
+    assert !validator.pass?('意炉葉二歩屁戸')
+    assert !validator.pass?('意炉葉二ほ屁戸')
+  end
+  
+  def test_KanaKanji
+    validator = Validator::KanaKanji.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('イロハニホヘト')
+    assert  validator.pass?('イロハニほヘト')
+    assert  validator.pass?('イロハニ歩ヘト')
+    assert !validator.pass?('イロハニHoヘト')
+    assert  validator.pass?('意炉葉二歩屁戸')
+    assert  validator.pass?('意炉葉二ほ屁戸')
+  end
+  
+  def test_MailAddress
+    validator = Validator::MailAddress.new
+    assert  validator.pass?(nil)
+    assert  validator.pass?('')
+    assert  validator.pass?('syok_hanako@syok.jp')
+    assert  validator.pass?('syok.hanako@syok.jp')
+    assert !validator.pass?('syok_hanako.@syok.jp')
+    assert !validator.pass?('syok_hanako..@syok.jp')
+    assert  validator.pass?('"syok_hanako."@syok.jp')
+    assert !validator.pass?('syok..hanako@syok.jp')
+    assert  validator.pass?('"syok..hanako"@syok.jp')
+    assert !validator.pass?('syok hanako@syok.jp')
+    assert  validator.pass?('"syok hanako"@syok.jp')
+    assert  validator.pass?('syok_hanako@SYOK.JP')
+    assert !validator.pass?('syok_hanako@syok.JP')
+    assert !validator.pass?('syok_hanako@.syok.jp')
+    assert  validator.pass?('syok_hanako@sy-ok.jp')
+    assert !validator.pass?('syok_hanako@syok.-jp')
+    assert  validator.pass?('syok_hanako@syok.jp0')
+    assert !validator.pass?('syok_hanako@syok.0jp')
+    assert  validator.pass?('syok%hanako@syok.jp')
+    assert  validator.pass?('syok-hanako@syok.jp')
+    assert  validator.pass?('syok/hanako@syok.jp')
+  end
+end
diff --git a/test/tc_crawler.rb b/test/tc_crawler.rb
new file mode 100644 (file)
index 0000000..d4a4860
--- /dev/null
@@ -0,0 +1,146 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+$:.unshift("#{File.dirname(__FILE__)}/../lib/")
+
+require 'fileutils'
+require 'test/unit'
+
+require 'hdboo/crawler'
+
+include Hdboo
+include FileUtils
+
+class TestFileSystemCrawler < Test::Unit::TestCase
+
+  public
+  def test_crawls_on_all_files_whose_path_matches_the_grob_given_as_a_arg
+    given_documents_are_in_TEST_DOCS_dir
+    when_it_crawls_with_a_grob_matches_all_files_under_TEST_DOCS_dir
+    then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir
+  ensure
+    teardown_TEST_dir
+  end
+
+  TEST_DIR_PATH      = './TEST/'
+  TEST_DOCS_DIR_PATH = './TEST/DOCS/'
+
+  private
+  def given_documents_are_in_TEST_DOCS_dir
+    dir1 = TEST_DOCS_DIR_PATH + 'dir1/'
+    dir2 = TEST_DOCS_DIR_PATH + 'dir2/'
+    mkdir_p(dir1)
+    mkdir_p(dir2)
+    option = File::CREAT|File::TRUNC|File::RDWR
+    cd(dir1) do
+      File.open('file1.txt', option) do |file|
+        file << 'Hello world!'
+      end
+      File.open('file2.txt', option) do |file|
+        file << 'What a wornderful world!'
+      end
+    end
+    cd(dir2) do
+      File.open('file1.txt', option) do |file|
+        file << 'Hello world!'
+      end
+    end
+  end
+
+  private
+  def when_it_crawls_with_a_grob_matches_all_files_under_TEST_DOCS_dir
+    @processed_path_list = []
+    crawler = FileSystemCrawler.new
+    crawler.crawl(TEST_DOCS_DIR_PATH + '**/*') do |file|
+      @processed_path_list << file.path
+    end
+  end
+
+  private
+  def then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir
+    processed_pathes = @processed_path_list.join(', ')
+    assert_equal 3, @processed_path_list.size, "processed_pathes: #{processed_pathes}"
+    assert_match /#{TEST_DOCS_DIR_PATH}dir1\/file1.txt/, processed_pathes.to_s
+    assert_match /#{TEST_DOCS_DIR_PATH}dir1\/file2.txt/, processed_pathes.to_s
+    assert_match /#{TEST_DOCS_DIR_PATH}dir2\/file1.txt/, processed_pathes.to_s
+  end
+
+  private
+  def teardown_TEST_dir
+    rm_rf(TEST_DIR_PATH)
+  end
+
+  public
+  def test_processes_each_grob_when_multiple_grobs_are_given_as_vargs
+    given_documents_are_in_TEST_DOCS_dir
+    when_it_crawls_with_grobs_one_of_which_matches_dir1_and_another_matches_dir2
+    then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir1_and_dir2
+  ensure
+    teardown_TEST_dir
+  end
+
+  private
+  def when_it_crawls_with_grobs_one_of_which_matches_dir1_and_another_matches_dir2
+    @processed_path_list = []
+    crawler = FileSystemCrawler.new
+    crawler.crawl(
+        TEST_DOCS_DIR_PATH + 'dir1/**/*',
+        TEST_DOCS_DIR_PATH + 'dir2/**/*'
+    ) do |file|
+       @processed_path_list << file.path
+    end
+  end
+
+  private
+  def then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir1_and_dir2
+    then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir
+  end
+
+  public
+  def test_processes_a_file_only_once_though_its_path_matches_two_or_more_grobs
+    given_documents_are_in_TEST_DOCS_dir
+    when_it_crawls_with_grobs_one_of_which_matches_parent_dir_and_another_matches_sub_dir
+    then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir_only_once
+  ensure
+    teardown_TEST_dir
+  end
+
+  private
+  def when_it_crawls_with_grobs_one_of_which_matches_parent_dir_and_another_matches_sub_dir
+    @processed_path_list = []
+    crawler = FileSystemCrawler.new
+    crawler.crawl(
+        TEST_DOCS_DIR_PATH + '**/*',
+        TEST_DOCS_DIR_PATH + 'dir1/**/*'
+    ) do |file|
+       @processed_path_list << file.path
+    end
+  end
+
+  private
+  def then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir_only_once
+    then_ensure_it_processes_the_all_documents_under_TEST_DOCS_dir
+  end
+end
+
+
+class TestWebCrawler #< Test::Unit::TestCase
+  def test_initialize
+    crawler = setup_crawler
+    assert_equal 3, crawler.path_stack.size
+  end
+
+  def test_crawl
+    crawler = setup_crawler
+    url_list = []
+    crawler.crawl {|page| url_list << page.base_uri}
+    assert_equal 3, url_list.size
+  end
+
+  def setup_crawler
+    WebCrawler.new(
+      'http://www.example.com',
+      'http://www.example.org',
+      'http://www.example.net'
+    )
+  end
+end
diff --git a/test/tc_database.rb b/test/tc_database.rb
new file mode 100644 (file)
index 0000000..5d5c807
--- /dev/null
@@ -0,0 +1,14 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'test/unit'
+require 'hdboo/database'
+
+include Hdboo
+
+class TestDatabase < Test::Unit::TestCase
+
+  def test_create_view_and_subtype_mapping
+  end
+end
diff --git a/test/tc_html.rb b/test/tc_html.rb
new file mode 100644 (file)
index 0000000..e08e2a9
--- /dev/null
@@ -0,0 +1,165 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'test/unit'
+require 'hdboo/html'
+
+class TestToHTML < Test::Unit::TestCase
+  
+  def test_String_to_html
+    assert 'hoge'.respond_to?(:to_html)
+    assert_equal 'hoge', 'hoge'.to_html
+    assert_equal "hoge\nfuga", "hoge\nfuga".to_html
+  end
+  
+  def test_String_to_html_with_indentation
+    assert_equal '  hoge', 'hoge'.to_html(nil, 1)
+    assert_equal "  hoge\n  fuga", "hoge\nfuga".to_html(nil, 1)
+  end
+  
+  def test_Nil_to_html
+    assert nil.respond_to?(:to_html)
+    assert_equal '', nil.to_html
+  end
+  
+  def test_Hash_to_html
+    hash = {:hoge => 'hoge_value', :fuga => 'fuga_value'}
+    assert hash.respond_to?(:to_html)
+    
+    html = hash.to_html
+    assert_match /^<dl>/, html
+    assert(html.include?([
+      '  <dt>',
+      '    hoge',
+      '  </dt>',
+      '  <dd>',
+      '    hoge_value',
+      '  </dd>',
+    ].join("\n")))
+    assert(html.include?([
+      '  <dt>',
+      '    fuga',
+      '  </dt>',
+      '  <dd>',
+      '    fuga_value',
+      '  </dd>'
+    ].join("\n")))
+    assert_match /<\/dl>$/, html
+  end
+  
+  def test_Array_to_html
+    array = ['hoge', 'fuga', 'piyo']
+    assert array.respond_to?(:to_html)
+    assert_equal([
+      '<ol>',
+      '  <li>',
+      '    hoge',
+      '  </li>',
+      '  <li>',
+      '    fuga',
+      '  </li>',
+      '  <li>',
+      '    piyo',
+      '  </li>',
+      '</ol>'
+    ].join("\n"), array.to_html)
+  end
+  
+  class TestClass
+    attr_reader :fuga, :piyo
+    def initialize(fuga, piyo)
+      @fuga = fuga
+      @piyo = piyo
+    end
+  end
+  
+  def test_Object_to_html
+    obj = TestClass.new('fuga_value', 'piyo_value')
+    assert obj.respond_to?(:to_html)
+    assert_equal obj.to_html, obj.to_s
+    
+    def obj.to_hash
+      {:fuga => @fuga, :piyo => @piyo}
+    end
+    html = obj.to_html
+    assert_match /^<dl>/, html
+    assert(html.include?([
+    '  <dt>',
+    '    piyo',
+    '  </dt>',
+    '  <dd>',
+    '    piyo_value',
+    '  </dd>',
+    ].join("\n")))
+    assert(html.include?([
+    '  <dt>',
+    '    fuga',
+    '  </dt>',
+    '  <dd>',
+    '    fuga_value',
+    '  </dd>',
+    ].join("\n")))
+    assert_match /<\/dl>/, html
+  end
+  
+  def test_compound_object_to_html
+    hash = {
+      :str => 'str_value',
+      :nested_hash => {:hoge => 'hoge_value', :fuga => 'fuga_value'},
+      :nested_array => ['item1', 'item2']
+    }
+    html = hash.to_html
+    assert_match /^<dl>/, html
+    assert(html.include?([
+    '  <dt>',
+    '    nested_array',
+    '  </dt>',
+    '  <dd>',
+    '    <ol>',
+    '      <li>',
+    '        item1',
+    '      </li>',
+    '      <li>',
+    '        item2',
+    '      </li>',
+    '    </ol>',
+    '  </dd>',
+    ].join("\n")))
+    assert(html.include?([
+    '  <dt>',
+    '    str',
+    '  </dt>',
+    '  <dd>',
+    '    str_value',
+    '  </dd>',
+    ].join("\n")))
+    assert(html.include?([
+    '  <dt>',
+    '    nested_hash',
+    '  </dt>',
+    '  <dd>',
+    '    <dl>',
+    ].join("\n")))
+    assert(html.include?([
+    '      <dt>',
+    '        hoge',
+    '      </dt>',
+    '      <dd>',
+    '        hoge_value',
+    '      </dd>',
+    ].join("\n")))
+    assert(html.include?([
+    '      <dt>',
+    '        fuga',
+    '      </dt>',
+    '      <dd>',
+    '        fuga_value',
+    '      </dd>',
+    ].join("\n")))
+    assert(html.include?([
+    '    </dl>',
+    '  </dd>',
+    ].join("\n")))
+    assert_match /<\/dl>/, html
+  end
+end
diff --git a/test/tc_mime.rb b/test/tc_mime.rb
new file mode 100644 (file)
index 0000000..6810ddc
--- /dev/null
@@ -0,0 +1,46 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding:UTF-8 -*-
+$:.unshift("#{File.dirname(__FILE__)}/../lib/")
+
+require 'test/unit'
+require 'fileutils'
+require 'hdboo/mime'
+
+include FileUtils
+
+class RbFileTest < Test::Unit::TestCase
+  public
+  def test_identifies_its_mime
+    given_a_plain_text_file
+    then_its_mime_type_is_text_plain
+  ensure
+    clean_up
+  end
+
+  TEST_DIR_PATH = './test_temp/'
+
+  private
+  def given_a_plain_text_file
+    mkdir_p TEST_DIR_PATH
+    cd(TEST_DIR_PATH) do
+      open('ascii_plain.txt', File::CREAT|File::RDWR) do |file|
+        file << 'hello world!'
+      end
+    end
+  end
+
+  private
+  def then_its_mime_type_is_text_plain
+    file_path = "#{TEST_DIR_PATH}ascii_plain.txt"
+    assert_equal 'text/plain charset=us-ascii', \
+                 File.new(file_path).mime_type
+    assert_equal 'text/plain charset=us-ascii', \
+                 File.mime_type(file_path)
+  end
+
+  private
+  def clean_up
+    rm_rf TEST_DIR_PATH
+  end
+end
+
diff --git a/test/tc_rest.rb b/test/tc_rest.rb
new file mode 100644 (file)
index 0000000..3a405c0
--- /dev/null
@@ -0,0 +1,337 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'digest/md5'
+require 'test/unit'
+require 'rubygems'
+require 'json'
+require 'hdboo/rest'
+
+include Hdboo
+class TestREST < Test::Unit::TestCase
+  class TestServer < REST::Server
+    @@users = {
+      'hogeta_hogeo' => {
+        :username => 'hogeta_hogeo',
+        :mail     => 'hogeta.hogeo@example.com',
+        :password => 'hogehoge'
+      }
+    }
+    
+    def password_digest(username)
+      user = @@users.find{|k, any| any[:username] == username}.last
+      return nil unless user
+      Digest::MD5::hexdigest(
+        "#{user[:username]}:#{base_realm}:#{user[:password]}"
+      )
+    end
+    
+    def initialize
+      base_url   'localhost'
+      base_realm 'test_realm'
+      
+      respond_to 'POST /user/' do |req|
+        username = req.params[:username]
+        password = req.params[:password]
+        mail     = req.params[:mail]
+        @@users[username] = {
+          :username => username,
+          :mail     => mail
+        }
+        Created @@users[username]
+      end
+      
+      respond_to 'GET /user/LIST/' do |req|
+        OK @@users.values
+      end
+      
+      respond_to 'GET /user/{username}/' do |req|
+        username = req.params.username
+        unless @@users.has_key?(username)
+          raise NotFound, {:username, 'does not be registered in this system.'}
+        end
+        OK @@users[username]
+      end
+      
+      respond_to 'DELETE /user/{username}/' do |req|
+        username = req.params.username
+        raise Unauthorized unless req.authenticated_as?(username)
+        @@users.delete(req.params[:username])
+        OK username
+      end
+    end
+  end
+  
+  def setup
+    @testServer = TestServer.new
+  end
+  
+  def test_post_method
+    response = @testServer.test_run({
+      'REQUEST_METHOD' => 'POST',
+      'SCRIPT_NAME'    => '/user/',
+      'HTTP_ACCEPT'    => 'application/json'
+    }, {
+      :username => 'fugata_fugao',
+      :mail     => 'fugata.fugao@example.com',
+      :password => 'fugafuga'
+    })
+    expect = {
+      :username => 'fugata_fugao',
+      :mail     => 'fugata.fugao@example.com'
+    }.to_json
+    
+    assert_equal [
+      'Status: 201',
+      'Content-Type: application/json',
+      "Content-Length: #{expect.length}",
+      '',
+      expect
+    ].join(CGI::EOL), response
+  end
+
+  def test_get_method_json
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/hogeta_hogeo/',
+      'HTTP_ACCEPT'    => 'application/json, text/plain'
+    )
+    expect = {
+      :username => 'hogeta_hogeo',
+      :mail     => 'hogeta.hogeo@example.com',
+      :password => 'hogehoge'
+    }.to_json
+    
+    assert_equal [
+      'Status: 200',
+      'Content-Type: application/json',
+      "Content-Length: #{expect.length}",
+      '',
+      expect
+    ].join(CGI::EOL), response
+    
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/nobody/'
+    )
+    assert_match /^Status\: 404/, response
+    
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/LIST/'
+    )
+    assert_match /^Status\: 200/, response
+  end
+  
+  def test_get_method_html
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/hogeta_hogeo/'
+    )
+    assert(response.include?([
+    'Status: 200',
+    'Content-Type: text/html',
+    'Content-Length: '
+    ].join(CGI::EOL)))
+    assert(response.include?([
+    '<head>',
+    '  <title>200: Success</title>',
+    '</head>',
+    '<body>',
+    '  <dl>',
+    ].join("\n")))
+    assert(response.include?([
+    '    <dt>',
+    '      username',
+    '    </dt>',
+    '    <dd>',
+    '      hogeta_hogeo',
+    '    </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '    <dt>',
+    '      mail',
+    '    </dt>',
+    '    <dd>',
+    '      hogeta.hogeo@example.com',
+    '    </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '    <dt>',
+    '      password',
+    '    </dt>',
+    '    <dd>',
+    '      hogehoge',
+    '    </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '  </dl>',
+    '</body>'
+    ].join("\n")))
+    
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/nobody/'
+    )
+    assert_match /^Status\: 404/, response
+    assert(response.include?([
+    'Status: 404',
+    'Content-Type: text/html',
+    'Content-Length: '
+    ].join(CGI::EOL)))
+    assert(response.include?([
+    '<head>',
+    '  <title>404: NotFound</title>',
+    '</head>',
+    '<body>',
+    '  <dl>',
+    '    <dt>',
+    '      username',
+    '    </dt>',
+    '    <dd>',
+    '      does not be registered in this system.',
+    '    </dd>',
+    '  </dl>',
+    '</body>',
+    ].join("\n")))
+
+    response = @testServer.test_run(
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/LIST/'
+    )
+    assert(response.include?([
+    'Status: 200',
+    'Content-Type: text/html',
+    'Content-Length: '
+    ].join(CGI::EOL)))
+    assert(response.include?([
+    '<head>',
+    '  <title>200: Success</title>',
+    '</head>',
+    '<body>',
+    '  <ol>',
+    '    <li>',
+    '      <dl>'
+    ].join("\n")))
+    assert(response.include?([
+    '        <dt>',
+    '          username',
+    '        </dt>',
+    '        <dd>',
+    '          hogeta_hogeo',
+    '        </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '        <dt>',
+    '          mail',
+    '        </dt>',
+    '        <dd>',
+    '          hogeta.hogeo@example.com',
+    '        </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '        <dt>',
+    '          password',
+    '        </dt>',
+    '        <dd>',
+    '          hogehoge',
+    '        </dd>',
+    ].join("\n")))
+    assert(response.include?([
+    '      </dl>',
+    '    </li>',
+    '  </ol>',
+    '</body>'
+    ].join("\n")))
+  end
+
+  def test_authorization_error
+    response = @testServer.test_run({
+      'REQUEST_METHOD' => 'DELETE',
+      'SCRIPT_NAME'    => '/user/hogeta_hogeo/',
+    })
+    assert_match /^Status\: 401/, response
+    assert_match /WWW-Authenticate\: Digest/, response
+    assert_match /realm="test_realm"/, response
+    assert /nonce="(.{30,})"/ =~ response
+    nonce = $1
+    assert_match /algorithm=MD5/, response
+    assert_match /qop=auth/, response
+    assert_match /<title>401/, response
+    
+    method   = 'DELETE'
+    username = 'hogeta_hogeo'
+    password = 'hogehoge'
+    realm    = 'test_realm'
+    uri      = '/user/hogeta_hogeo/'
+    qop      = 'auth'
+    nc       = '00000001'
+    cnonce   =Digest::MD5::hexdigest(nonce+uri)
+    opaque   =Digest::MD5::hexdigest(Time.now.to_f.to_s)
+    
+    password_digest = Digest::MD5::hexdigest([
+      username, realm, password
+    ].join(':'))
+    
+    ha2 = Digest::MD5.hexdigest(method + ':' + uri)
+
+    response = Digest::MD5::hexdigest([
+      password_digest,
+      nonce,
+      nc,
+      cnonce,
+      qop,
+      ha2
+    ].join(':'))
+
+    response = @testServer.test_run({
+      'REQUEST_METHOD' => 'DELETE',
+      'SCRIPT_NAME'    => '/user/hogeta_hogeo/',
+      'HTTP_AUTHORIZATION_USERNAME' => username,
+      'HTTP_AUTHORIZATION_REALM'    => realm,
+      'HTTP_AUTHORIZATION_NONCE'    => nonce,
+      'HTTP_AUTHORIZATION_URI'      => uri,
+      'HTTP_AUTHORIZATION_QOP'      => qop,
+      'HTTP_AUTHORIZATION_NC'       => nc,
+      'HTTP_AUTHORIZATION_CNONCE'   => cnonce,
+      'HTTP_AUTHORIZATION_OPAQUE'   => opaque,
+      'HTTP_AUTHORIZATION_RESPONSE' => response
+    })
+    assert_match /^Status\: 200/, response
+  end
+  
+  
+  def test_not_found_error
+    response = @testServer.test_run({
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/unknown_user/',
+      'HTTP_ACCEPT'    => 'application/json'
+    })
+    assert_match /Status: 404/, response
+    assert_match /"username":"does not be registered in this system\."/,
+                 response
+    
+    response = @testServer.test_run({
+      'REQUEST_METHOD' => 'GET',
+      'SCRIPT_NAME'    => '/user/unknown_user/',
+      'HTTP_ACCEPT'    => 'text/html'
+    })
+    assert_match /Status: 404/, response
+    assert_match /<title>404/, response
+    assert(response.include?([
+    '<head>',
+    '  <title>404: NotFound</title>',
+    '</head>',
+    '<body>',
+    '  <dl>',
+    '    <dt>',
+    '      username',
+    '    </dt>',
+    '    <dd>',
+    '      does not be registered in this system.',
+    '    </dd>',
+    '  </dl>',
+    '</body>'
+    ].join("\n")))
+  end
+end
diff --git a/test/tc_search.rb b/test/tc_search.rb
new file mode 100644 (file)
index 0000000..6c25850
--- /dev/null
@@ -0,0 +1,251 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+$:.unshift "#{File.dirname(__FILE__)}/../lib/"
+
+require 'test/unit'
+require 'fileutils'
+
+require 'hdboo/search'
+
+include Hdboo
+include FileUtils
+
+class DocumentTest < Test::Unit::TestCase
+  public
+  def test_is_created_from_a_file
+    given_a_text_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_the_object_is_not_nil_and_instance_of_document
+  ensure
+    clean_up
+  end
+
+  private
+  def given_a_text_file_is_in_the_test_dir
+    rm_rf 'test'
+    mkdir 'test'
+    cd('test') do
+      open('file.txt', File::CREAT|File::RDWR) do |file|
+        file << 'Hello World!!\n'
+      end
+      @file = File.new('file.txt')
+    end
+  end
+
+  private
+  def clean_up
+    @file.close
+    @document = nil
+    rm_rf('./test')
+  end
+
+  private
+  def when_a_document_object_is_created_from_the_file
+    @document = Search::Document.new(@file)
+  end
+
+  private
+  def ensure_the_object_is_not_nil_and_instance_of_document
+    assert ! @document.nil?
+    assert   @document.is_a?(Search::Document)
+  end
+
+  public
+  def test_has_text_and_term_list_of_it
+    given_a_text_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_the_text_equals_to_the_files_content
+    ensure_the_term_list_is_correct
+  ensure
+    clean_up
+  end
+
+  private
+  def ensure_the_text_equals_to_the_files_content
+    assert !     @document.text.nil?
+    assert_equal 'Hello World!!\n', @document.text
+  end
+
+  private
+  def ensure_the_term_list_is_correct
+    assert       @document.term_list.is_a?(Array)
+    assert_equal ['hellow', 'world'].sort.join(', '), \
+                 @document.term_list.sort.join(', ')
+  end
+
+  public
+  def test_has_mime_type
+    given_a_text_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_its_mime_type_field_is_text_plain
+  ensure
+    clean_up
+  end
+
+  private
+  def ensure_its_mime_type_field_is_text_plain
+    assert_match /text\/plain/, @document.mime_type
+  end
+
+  public
+  def _test_has_fields_which_represent_documents_property
+    given_a_text_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_its_doc_path_field_is_its_absolute_path
+    ensure_its_doc_name_field_is_its_file_name
+    ensure_its_doc_id_field_is_nil
+
+  end
+end
+
+class HtmlScannerTest #< Test::Unit::TestCase
+  public
+  def test_scanns_html_and_stores_text_of_body_and_title__tag_in_default_zone
+    given_a_html_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_its_default_zone_holds_inner_text_of_body_and_title_tag
+  end
+
+  public
+  def test_stores_innertext_of_particular_tags_in_separate_zones
+    given_a_html_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_innertext_of_title_tag_is_stored_in_title_zone
+    ensure_innertext_of_all_heading_tags_is_stored_in_table_of_contents_zone
+  end
+
+  public
+  def test_scanns_particular_tags_in_header_and_stores_its_values_to_fields
+    given_a_html_file_is_in_the_test_dir
+    when_a_document_object_is_created_from_the_file
+    ensure_its_language_field_equals_to_the_value_assigned_in_metatag
+  end
+end
+
+class IndexTest # < Test::Unit::TestCase
+  public
+  def test_can_be_defined_with_sql_create_table_statement
+  end
+
+  public
+  def test_can_be_queried_with_sql_select_statement
+  end
+end
+
+
+
+class FileSystemIndexerTest #< Test::Unit::TestCase
+  public
+  def test_indexes_all_files_whose_path_matches_with_a_grob_given_as_arg
+    given_documents_are_in_TEST_DOCS_dir
+    when_it_crawls_with_a_grob_matches_all_files_under_TEST_DOCS_dir
+    then_ensure_the_all_documents_under_TEST_DOCS_dir_are_indexed_properly_in_the_index
+  ensure
+    teardown_TEST_dir
+  end
+
+  TEST_DIR_PATH      = './TEST/'
+  TEST_DOCS_DIR_PATH = './TEST/DOCS/'
+  INDEX_PATH         = './TEST/INDEX'
+
+  private
+  def given_documents_are_in_TEST_DOCS_dir
+    dir1 = TEST_DOCS_DIR_PATH + 'dir1/'
+    dir2 = TEST_DOCS_DIR_PATH + 'dir2/'
+    mkdir_p(dir1)
+    mkdir_p(dir2)
+    cd(dir1) do
+      File.open('file1.txt', File::CREAT|File::TRUNC|File::RDWR) do |file|
+        file << 'Hello world!'
+      end
+      File.open('file2.txt', File::CREAT|File::TRUNC|File::RDWR) do |file|
+        file << 'What a wornderful world!'
+      end
+    end
+    cd(dir2) do
+      File.open('file1.txt', File::CREAT|File::TRUNC|File::RDWR) do |file|
+        file << 'Hello world!'
+      end
+    end
+  end
+
+  private
+  def when_it_crawls_with_a_grob_matches_all_files_under_TEST_DOCS_dir
+    indexer = FileSystemIndexer.new(
+      :index_path => INDEX_PATH
+    )
+    grob = TEST_DOCS_DIR_PATH + '**/*'
+    indexer.crawl(grob)
+  end
+
+  private
+  def then_ensure_the_all_documents_under_TEST_DOCS_dir_are_indexed_properly_in_the_index
+    index = Index.connect(INDEX_PATH)
+    results = index.query('world')
+    assert 3, results.length
+    results.each do |result|
+      assert_match /world/, result.data
+    end
+
+    results = index.query('hello world')
+    assert 2, results.length
+    results.each do |result|
+      assert_match /hello/, result.data
+      assert_match /world/,  result.data
+    end
+
+    results = index.query('wonderful')
+    assert 1, results.length
+    results.each do |result|
+      assert_match /wonderful/, result.data
+    end
+  end
+
+  private
+  def teardown_TEST_dir
+    rm_rf(TEST_DIR_PATH)
+  end
+end
+
+
+class TestXapianStorageEngine #< Test::Unit::TestCase
+
+
+  def test_provides_ability_for_sql_to_query_on_xapian_index
+
+  end
+
+  def set_up_table_uses_xapian_storage_engine
+    sql_script = <<-END_SQL
+      CREATE TABLE Document (
+      ) ENGINE = Xapian
+      //
+
+      CREATE PROCEDURE Document_fulltext_search(
+        TEXT _query
+      )
+      BEGIN
+        SELECT DBS.name, Document.kwin
+        FROM   Document, DBS
+        WHERE  Document.query = query
+         AND   Document.dbs_code = DBS.code
+        ;
+      END
+      //
+
+      CREATE TABLE DBS (
+        code
+          CHAR(24) NOT NULL,
+          CONSTRAINT PK_DBS PRIMARY KEY (code),
+
+        name
+          VARCHAR(512) NOT NULL
+      ) ENGINE = InnoDB
+      //
+    END_SQL
+  end
+
+  def test_query_via_xapian_table
+
+  end
+end
diff --git a/test/tc_sql.rb b/test/tc_sql.rb
new file mode 100644 (file)
index 0000000..c81c0db
--- /dev/null
@@ -0,0 +1,540 @@
+#!/usr/bin/ruby -Ku
+# -*- encoding: UTF-8 -*-
+
+require 'test/unit'
+require 'hdboo/sql'
+
+include Hdboo
+
+module TestSQL
+  USER_SQL = <<-END_SQL
+  -- user.sql
+  -- @Module(Model)
+  -- @Include(Pivot)
+
+  DELIMITER //
+  
+  CREATE TABLE UserType (
+    user_type_code
+      CHAR(24) NOT NULL,
+      CONSTRAINT  PK_UserType
+        PRIMARY KEY (user_type_code),
+        
+    name
+      VARCHAR(256) NOT NULL
+  ) ENGINE = INNODB
+  //
+  
+  CREATE PROCEDURE UserType_load_master_data()
+  -- @MasterData
+  BEGIN
+    INSERT INTO UserType
+    VALUES
+      ('User',           '通常ユーザー'),
+      ('PrivilegedUser', '特権ユーザー');
+  END
+  //
+  
+  CREATE PROCEDURE UserType_list_all_types()
+  -- @DefineConstants(user_type_code)
+  BEGIN
+    SELECT *
+    FROM   UserType;
+  END
+  //
+  
+  CREATE TABLE User (
+  -- @ClassifiedBy(user_type_code)
+  
+    user_id
+      INTEGER AUTO_INCREMENT,
+      CONSTRAINT
+        PK_User PRIMARY KEY(user_id),
+    
+    user_type_code
+      CHAR(24) NOT NULL DEFAULT 'User',
+      CONSTRAINT FK_User_user_type_code
+        FOREIGN KEY(user_type_code) REFERENCES UserType(user_type_code),
+      
+    is_active
+      -- @Bool
+      INTEGER NOT NULL DEFAULT 0, 
+
+    name
+      -- @KanaKanji @NotBlank
+      CHAR(32) NOT NULL,
+
+    nickname
+      -- @Word @NotBlank
+      CHAR(32) NOT NULL,
+      INDEX IND_User_nickname (nickname),
+
+    mail_address
+      -- @MailAddress @NotBlank
+      CHAR(128) NOT NULL,
+      CONSTRAINT UQ_User_mail_address
+        UNIQUE(mail_address),
+      
+    comment
+      TEXT NOT NULL
+      
+  ) ENGINE=INNODB
+  //
+           
+  CREATE PROCEDURE User_count_active_users()
+  -- @ReturnsFirst(INTEGER)
+  BEGIN
+    SELECT COUNT(*)
+    FROM   User
+    WHERE  is_active;
+  END
+  //
+
+  CREATE PROCEDURE User_list_active_users()
+  -- @Returns(User)
+  BEGIN
+    SELECT *
+    FROM   User
+    WHERE  is_active;
+  END
+  // 
+
+  CREATE PROCEDURE User_find_by_id(
+    _user_id    INTEGER  -- @ColumnValue
+  )
+  -- @ReturnsFirst(User)
+  BEGIN
+    SELECT *
+    FROM   User 
+    WHERE  user_id = _user_id;
+  END
+  //
+
+  CREATE PROCEDURE User_activate(
+    _mail_address CHAR(128) -- @ColumnValue
+  )
+  -- @ReturnsFirst(User)
+  -- @OnNoData(mail_address, 'メールアドレスが誤っているか、まだ登録されていません。')
+  BEGIN
+    UPDATE User
+    SET    is_active = 1
+    WHERE  mail_address = _mail_address
+    AND    NOT is_active;
+
+    SELECT *
+    FROM   User
+    WHERE  mail_address = _mail_address
+    AND    is_active;
+  END
+  //
+
+  CREATE PROCEDURE User_register(
+    _name         TEXT, -- @ColumnValue
+    _nickname     TEXT, -- @ColumnValue
+    _mail_address TEXT, -- @ColumnValue
+    _comment      TEXT  -- @ColumnValue @Optional
+  )
+  -- @ReturnsFirst(User)
+  -- @OnConflict(UQ_User_mail_address, mail_address, 'このメールアドレスは既に登録されています。')
+  BEGIN
+    SET @comment := COALESCE(_comment, '');
+
+    INSERT INTO User
+    VALUES (
+      DEFAULT,
+      DEFAULT,
+      DEFAULT,
+      _name,
+      _nickname,
+      _mail_address,
+      @comment
+    );
+
+    SELECT *
+    FROM   User
+    WHERE  user_id = LAST_INSERT_ID();
+  END
+  //
+  
+  CREATE PROCEDURE User_edit_comment(
+    _user_id INTEGER,  -- @ColumnValue
+    _comment TEXT      -- @ColumnValue
+  )
+  -- @ReturnsFirst(User)
+  -- @OnNoData(user_id, 'このユーザは登録されていないか、一時的に使用できなくなっています。')
+  BEGIN
+    UPDATE User
+    SET    comment = _comment
+    WHERE  user_id = _user_id
+    AND    is_active;
+    
+    SELECT *
+    FROM   User
+    WHERE  user_id = _user_id
+    AND    is_active;
+  END
+  //
+
+  CREATE TABLE PrivilegedUser (
+    user_id
+      INTEGER NOT NULL,
+      CONSTRAINT PK_PrivilegedUser
+        PRIMARY KEY(user_id),
+      CONSTRAINT FK_PrivilegedUser_user_id
+        FOREIGN KEY(user_id) REFERENCES User(user_id)
+        ON DELETE CASCADE,
+    
+    access_level
+      -- @Min(1) @Max(5)
+      INTEGER NOT NULL DEFAULT 1
+      
+  ) ENGINE = INNODB
+  //
+  
+  CREATE PROCEDURE User_make_privileged(
+    _user_id INTEGER -- @ColumnValue
+  )
+  -- @ReturnsFirst(User)
+  -- @OnNoData(user_id, 'このユーザはすでに特権ユーザとなっているか、一時的に使用できなくなっています。')
+  BEGIN
+    UPDATE User
+    SET    user_type_code = 'PrivilegedUser'
+    WHERE  user_id = _user_id
+    AND    is_active
+    AND    user_type_code = 'User';
+    
+    INSERT INTO PrivilegedUser
+    SELECT user_id, 1
+    FROM   User
+    WHERE  user_id = _user_id
+    AND    is_active
+    AND    user_type_code = 'PrivilegedUser';
+    
+    SELECT *
+    FROM   User JOIN PrivilegedUser USING (user_id)
+    WHERE  user_id = _user_id
+    AND    is_active;
+  END
+  //
+  
+  CREATE PROCEDURE PrivilegedUser_find_by_id(
+    _user_id INTEGER
+  )
+  -- @ReturnsFirst(User)
+  BEGIN
+    SELECT *
+    FROM   User JOIN PrivilegedUser USING (user_id)
+    WHERE  user_id = _user_id
+    AND    is_active
+    AND    user_type_code = 'PrivilegedUser';
+  END
+  //
+  
+  CREATE PROCEDURE PrivilegedUser_acquire_top_secret(
+    _user_id INTEGER -- @ColumnValue
+  )
+  -- @ReturnsFirst(User)
+  -- @OnNoData('*', 'このユーザには権限がありません。')
+  BEGIN
+    SELECT *, '王様の耳は、ろばタイプ' AS top_secret
+    FROM   User JOIN PrivilegedUser USING(user_id)
+    WHERE  user_id = _user_id
+    AND    is_active;
+  END
+  //
+  END_SQL
+end
+
+class TestSQLLoad < Test::Unit::TestCase
+  include Hdboo
+  
+  def test_query
+    SQL.sql_eval!('test_hdboo_temp_db', TestSQL::USER_SQL)
+    db = SQL.connect('test_hdboo_temp_db')
+    sql = <<-SQL_END
+      DELETE
+      FROM   UserType
+      WHERE  user_type_code = 'User';
+    SQL_END
+    db.transaction do
+      assert_equal 2, Model::UserType.list_all_types.size
+      SQL.query(sql)
+      assert_equal 1, Model::UserType.list_all_types.size\r    end
+  end
+  
+  def test_equality_of_record_object
+    SQL.sql_eval!('test_hdboo_temp_db', TestSQL::USER_SQL)
+    hogeo = Model::User.new(
+      :user_id => 1,
+      :email   => 'hogeta.hogeo@example.com'
+    )
+    
+    nise_hogeo = Model::User.new(
+      :user_id => 1,
+      :email   => 'nise.hogeo@example.com'
+    )
+    
+    assert_equal [1], hogeo.identifier
+    assert_equal [:user_id], hogeo.class.identifier_keys 
+    assert  hogeo == nise_hogeo
+    assert !hogeo.eql?(nise_hogeo)
+    assert  hogeo == {:user_id => 1, :email => 'hogeta.hogeo@example.com'}
+    assert  hogeo != {:user_id => 1, :email => ''}
+    assert  hogeo.eql?({:user_id => 1, :email => 'hogeta.hogeo@example.com'})
+    assert  ({:user_id => 1, :email => 'hogeta.hogeo@example.com'}) == hogeo
+    assert  ({:user_id => 1, :email => 'hogeta.hogeo@example.com'}).eql?(hogeo)
+    assert  ({:user_id => 1, :email => ''}) != hogeo
+  end
+  
+  def test_require_sql!
+    $:.unshift File.join(File.dirname(__FILE__))
+    require_sql 'testmodel/user@test_hdboo_temp_db'
+    assert ::Fuga::Model::User.is_a?(Class)
+  end
+  
+  def test_sql_eval!
+    SQL.sql_eval!('test_hdboo_temp_db', TestSQL::USER_SQL)
+    db = SQL.connect('test_hdboo_temp_db')
+    
+    #procedure call
+    assert Model::User.respond_to?(:list_active_users)
+    active_users = db.transaction {Model::User.list_active_users}
+    assert active_users.empty?
+    
+    assert Model::User.respond_to?(:find_by_id)
+    nobody = db.transaction {Model::User.find_by_id(:user_id => 9999)}
+    assert nobody.nil?
+    
+    nobody = db.transaction {Model::User.find_by_id(9999)}
+    assert nobody.nil?
+    
+    hogeo = Model::User.new(
+      :name         => 'ホゲ田ほげ夫',
+      :nickname     => 'hogeo',
+      :mail_address => 'invalid..address@example.com'
+    )
+    assert_raise(InvalidArgument) {hogeo.validate}
+    
+    hogeo.mail_address = 'hogeta.hogeo@example.com'
+    assert_nothing_raised {hogeo.validate}
+    
+    assert hogeo.respond_to?(:register)
+    assert hogeo.respond_to?(:register!)
+    
+    db.transaction { hogeo.register! }
+    assert_equal 1, hogeo.user_id
+    assert !     hogeo.active?
+    
+    assert_equal 0, db.transaction { Model::User.count_active_users }
+    db.transaction { hogeo.activate! }
+    assert       hogeo.active?
+    assert_equal 1, db.transaction { Model::User.count_active_users }
+      
+    all_users = db.transaction { Model::User.list_active_users }
+    assert_equal 1, all_users.size
+    assert       all_users[0].is_a?(Model::User)
+    
+    assert_equal '', hogeo.comment
+    db.transaction { hogeo.edit_comment!(:comment => "multi\nline\ndata") }
+    assert_equal "multi\nline\ndata", hogeo.comment
+    
+      
+    #unique constraint violation
+    nise_hogeo = Model::User.new(
+      :name         => 'にせホゲ田ほげ夫',
+      :nickname     => 'nise_hogeo',
+      :mail_address => 'hogeta.hogeo@example.com' #must be unique
+    )
+    
+    assert_raise(DataConflict) do
+      db.transaction { nise_hogeo.register! }\r    end
+    
+    assert_raise(NoDataFound) do
+      db.transaction do
+        Model::User.activate(:mail_address => 'nobody@example.com')
+      end\r    end
+    
+    #initialize master data & load as class constants 
+    assert Model::UserType.is_a?(Class)
+    assert Model::UserType.const_defined?('User')
+    assert Model::UserType.const_defined?('PrivilegedUser')
+    
+    assert_equal 'User', Model::UserType::User.user_type_code
+    assert_equal '通常ユーザー', Model::UserType::User.name
+    
+    assert_equal 'PrivilegedUser',
+                 Model::UserType::PrivilegedUser.user_type_code
+    assert_equal '特権ユーザー',
+                 Model::UserType::PrivilegedUser.name
+    
+    #inheritance
+    assert Model::PrivilegedUser.is_a?(Class)
+    assert Model::PrivilegedUser < Model::User
+    
+    super_hogeo = Model::PrivilegedUser.new(
+      :user_id      => 1,
+      :name         => '超ホゲ田ほげ夫',
+      :nickname     => 'hogeo',
+      :mail_address => 'hogeta.hogeo@example.com'
+    )
+    
+    assert  super_hogeo.is_a?(Model::PrivilegedUser)
+    assert !      hogeo.respond_to?(:acquire_top_secret)
+    assert  super_hogeo.respond_to?(:acquire_top_secret)
+    
+    assert_raise(NoDataFound) do
+      db.transaction {super_hogeo.acquire_top_secret!}\r    end
+    
+    #overridden method
+    assert_not_nil db.transaction {Model::User.find_by_id(:user_id => 1)}
+    assert_not_nil db.transaction {Model::User.find_by_id(1)}
+    assert_nil db.transaction {Model::PrivilegedUser.find_by_id(:user_id => 1)}
+    assert_nil db.transaction {Model::PrivilegedUser.find_by_id(1)}
+   
+    db.transaction { super_hogeo = hogeo.make_privileged }  
+    assert  super_hogeo.is_a?(Model::PrivilegedUser)
+    
+    assert_not_nil db.transaction {Model::PrivilegedUser.find_by_id(1)}
+    assert  super_hogeo.respond_to?(:access_level)
+    assert_equal 1, super_hogeo.access_level
+    
+    db.transaction { super_hogeo.acquire_top_secret! }
+    assert_equal '王様の耳は、ろばタイプ', super_hogeo.top_secret
+  end
+  
+  def test_load_sql
+    SQL.sql_eval('test_hdboo_temp_db', TestSQL::USER_SQL)
+    
+    assert Model::User.is_a?(Class)
+    
+    hogeo = Model::User.new(
+      :name     => 'ホゲ田ほげ夫',
+      :nickname => 'hogeo',
+      :mail_address => 'hogeta.hogeo@example.com',
+      :is_active => 0
+    )
+    
+    #accessors
+    assert hogeo.respond_to?(:mail_address)
+    assert hogeo.respond_to?(:mail_address=)
+    assert_equal 'hogeta.hogeo@example.com', hogeo.mail_address
+    assert hogeo.respond_to?(:nickname)
+    assert hogeo.respond_to?(:nickname=)
+    assert_equal 'hogeo', hogeo.nickname
+    
+    #accessors (boolean)
+    assert hogeo.respond_to?(:is_active)
+    assert hogeo.respond_to?(:is_active=)
+    assert hogeo.respond_to?(:active?)
+    assert_equal 0, hogeo.is_active
+    assert_equal false, hogeo.active?
+    
+    #instance method
+    assert hogeo.respond_to?(:count_active_users)
+    assert hogeo.respond_to?(:list_active_users)
+    
+    #class method
+    assert hogeo.class.respond_to?(:count_active_users)
+    assert hogeo.class.respond_to?(:list_active_users)
+    
+    #validation
+    assert  hogeo.respond_to?(:invalid?)
+    assert  hogeo.respond_to?(:validate)
+    assert !hogeo.invalid?
+    assert_nothing_raised do
+      hogeo.validate
+    end
+    
+    assert hogeo.validate.is_a?(Model::User)
+  
+
+    raised_error = nil
+    begin
+      hogeo.is_active = 'treu'
+    rescue
+      raised_error = $!
+    end
+    assert !     raised_error.nil?
+    assert       raised_error.is_a?(InvalidArgument)
+    assert_equal 1, raised_error.messages.size
+    assert_match /treu/, raised_error.messages[:is_active].to_s
+    
+    hogeo.is_active = true
+    assert hogeo.active?
+  end
+  
+  def test_csv_util
+    SQL.sql_eval!('test_hdboo_temp_db', <<-END_DDL)
+    -- @Module(CSVTest)
+    -- @Include(Pivot)
+    
+    DELIMITER //
+    
+    CREATE TABLE Dummy (
+      id INTEGER NOT NULL AUTO_INCREMENT,
+      PRIMARY KEY (id)
+    ) ENGINE=MYISAM
+    //
+    
+    CREATE PROCEDURE Dummy_parse(csv VARCHAR(1024))
+    BEGIN
+      SELECT csv_get(csv, pos) AS val
+      FROM   pivot
+      WHERE  pos <= csv_length(csv)
+      ;
+    END
+    //
+    END_DDL
+    
+    db = SQL.connect('test_hdboo_temp_db')
+    
+    result = db.transaction do
+      CSVTest::Dummy.parse(:csv => 'hoge,fuga,piyo')
+    end
+    assert_equal 3, result.length
+    assert_equal 'hoge', result[0]['val']
+    assert_equal 'fuga', result[1]['val']
+    assert_equal 'piyo', result[2]['val']
+  end
+end
+
+class TestSQLParser < Test::Unit::TestCase  
+  def test_parse
+    ast = SQL::Parser.new.parse(TestSQL::USER_SQL)
+
+    create_table_statements     = []
+    create_procedure_statements = []
+    
+    SQL::Parser::ASTWalker.new\
+      .visit(:CREATE_TABLE) do |statement|
+        create_table_statements << statement
+      end\
+      .visit(:CREATE_PROCEDURE) do |statement|
+        create_procedure_statements << statement
+      end\
+      .walk(ast)
+    
+    assert_equal 3,  create_table_statements.length
+    assert_equal 11, create_procedure_statements.length
+    
+    user_table_source = create_table_statements[0].source.collect do |line|
+                          line.strip
+                        end.join("\n")
+  end
+end
+
+class TestTable < Test::Unit::TestCase
+  include Hdboo::SQL
+  def test_new
+    sql_module = SQL::Module.new('TestModule.Model')
+    sql_module.elements << Table.new(:User, [
+      Table::Column.new(:id, DataType.new(:INTEGER)),
+      Table::Constraint::PrimaryKey.new(:User_id_PK, [:id]),
+      Table::Column.new(:mail_address, DataType.new(:CHAR, 128)),
+      Table::Column.new(:password,     DataType.new(:CHAR, 32))
+    ])
+    sql_module.eval
+    
+    assert TestModule::Model::User.is_a?(Class)
+  end
+end