/ 最近 .rdf 追記 設定 本棚

脳log[Ruby: 2007-12-20~]



2007年12月20日 (木)

[Ruby] るびまゴルフ 【第 2 回】

 問題

以下のコードと同じ出力をより短いコードで得よ。(パーは 27バイト)

-2000.step(-10000,-10) do |v|
  puts v
end

 答案

n=1990;801.times{p -n+=10}

-1 (26バイト)だからバーディーかな。アルバトロス(-3)が上限とは限らないけど。数値リテラルが冗長な気がするけど他の表現が思いつかない。文字コード(?X)を使おうと思ったけどできなかった。

 やってみたこと

  • 省ける空白を省く
  • do〜end を {〜} に
  • puts を pに
  • 三カ所の -(マイナス)を一カ所に

 失敗したこと

  • ブロック変数名を $\(レコードセパレータ)にして、printを無引数で呼ぶ。(.step{|$\\|print}。$\に数字を代入できなくて失敗。なら文字列に対して uptoを呼べばいいんだけど文字列リテラルにするために引用符で +2バイト使っちゃ無駄が多いし、そもそもお題を満たせないし、泥縄)
  • (irb限定で) 変数_を使ったインチキ。

 できなかったこと (他人の答えを見て)

  • 三カ所の 0(十)を一カ所に (思いつかなかった)
  • Shift_JISの文字コードを利用 (できなかった)

2007年12月17日 (月)

[Ruby] Rubyの 定数の見える範囲 の理解はあきらめております

irb(main):115:0> require :benchmark.to_s
irb(main):119:0> Benchmark.bmbm{|j| j.report }
NameError: uninitialized constant Benchmark::Job::ArgmentError
        from C:/Program Files (x86)/ruby/lib/ruby/1.8/benchmark.rb:333:in `report'
        from (irb):119
        from C:/Program Files (x86)/ruby/lib/ruby/1.8/benchmark.rb:250:in `bmbm'
        from (irb):119
        from :0
irb(main):120:0>

 追記@2008-02-20:実は可視範囲の問題ではなくタイポだった

ArgmentError -> ArgumentError

http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-list/44601


2007年01月29日 (月)

[Ruby] 文字列リテラル連結

irb(main):001:0> RUBY_RELEASE_DATE
=> "2006-12-25"
irb(main):002:0> "aaa"\
irb(main):003:0* "bbb"
=> "aaabbb"
irb(main):004:0> "aaa"\
irb(main):005:0* %[bbb]
NameError: undefined local variable or method `bbb' for main:Object
        from (irb):5

こういうもんなの? "bbb" と %!bbb! は同じものだと思っていたが。

 追記:2007-12-12

'%04d-%02d-%02d'%[2007,12,12]
=> "2007-12-12"

String#% が呼ばれてるんだよ。


2007年01月20日 (土)

[Ruby][SQLite] sqlite3-ruby-1.2.0リリース

二年近くの長い沈黙を破って 2007-01-13にリリースされていた。

[Ruby] mkmf.rb

空白を含むパスで失敗するのでこのように↓。

 def link_command(ldflags, opt="", libpath=$LIBPATH)
   Config::expand(TRY_LINK.dup,
                  CONFIG.merge('hdrdir' => $hdrdir.quote,
+                              'srcdir' => $srcdir.quote,
                               'src' => CONFTEST_C,
                               'INCFLAGS' => $INCFLAGS,

2007年01月13日 (土)

[Ruby][Hiki] Hiki-0.8.6のインストロールインストゥール、install。

まずローカルで試してからアップロードしたのだがローカルの Apacheが Ruby-1.9.0を呼ぶので、FrontPageの表示からログインまでを可能にするために加えた変更点のリストをメモしておく。

 hiki/config.rb: Hiki::Config#initialize

-      instance_variables.each do |v|
+      instance_variables.each do |v| v = v.to_s;

vにシンボルが渡されて、次の行で v.sub!したときにエラーになっていた。

 hiki/command.rb: Hiki::Command#cmd_login

          if page && !page.empty?
-           redirect(@cgi, @conf.base_url + @plugin.hiki_url( page ), session_cookie( session.session_id ))
+           redirect(@cgi, @conf.base_url + @plugin.hiki_url( page ), [session_cookie( session.session_id )])
          else
-           redirect(@cgi, @conf.index_url, session_cookie( session.session_id ))
+           redirect(@cgi, @conf.index_url, [session_cookie( session.session_id )])

Cookieが session_id=SESSIONIDの形でなく SESSIONIDと key名なしの状態でブラウザにセットされるからログインに失敗していた。

Hiki::Command#cmd_logoutでは同じ引数を [session_cookie(session_id, -1)] としていたので同じように配列にした。

 hiki/storage.rb: Hiki::HikiDBBase#md5hex

-      Digest::MD5::new( s || '' ).hexdigest
+      Digest::MD5::hexdigest( s || '' )

リファレンスマニュアルには Digest::MD5.new([str]) とあるが引数の数が 0でないと叱られる。

 hiki/db/tmarshal.rb: TMarshal::dump_text

    when Array
-     "[\n"+obj.collect{|x| dump_text(x)+",\n"}.to_s+"]"
+     "[\n"+obj.collect{|x| dump_text(x)}.join(",\n")+"\n]"
    when Hash
-     "{\n"+obj.sort_by{|e| e[0].inspect}.collect{|k,v| "#{dump_text(k)} => #{dump_text(v)},\n"}.to_s+"}"
+     "{\n"+obj.sort_by{|e| e[0].inspect}.collect{|k,v| "#{dump_text(k)} => #{dump_text(v)}"}.join(",\n")+"\n}"

dumpに失敗していた。

原因となった Array#to_sのバージョンによる出力の違い↓。

Ruby-1.8.5p12> [1,2,3].to_s #=> "123"
Ruby-1.9.0 20061205> [1,2,3].to_s #=> "[1, 2, 3]"

リファレンスマニュアルには

to_s
   self.join($,) と同じです。

と書いてあるから to_sで(ある種の) joinを代用していても仕方ない。

と思ったがどちらにしろ Array#to_sの出力は $, に依存するので、後で(loadするときに) evalすることを考えれば今回の to_sの使用は不適切か。

[Ruby][Hiki] 差分の表示

上のエントリで FrontPageの表示とログインまでやったが、差分の表示もおかしかったので Array#to_s らしき部分を join('') に書き換えまくったら直った模様。

'string'.join は存在しなかったので、書き換えてエラーにならないということは考えたとおり Array#to_sだったか、そのコードが実行されてなくて発覚してないが実は間違いだった(実行されたらNoMethodErrorになる)かのどちらか ^_^; こういう実行してみないとわからないところは javascriptと同じでレアなコードパスのデバッグを難しくするね。

以下、リスト。

 style/default/hikidoc.rb: HikiDoc#escape_meta_char

+if(defined? ' '.ord) # Ruby-1.9
   def escape_meta_char( text )
     text.gsub( META_CHAR_RE ) do |s|
+      '&#x%x;' % s[1].ord
+    end
+  end
+else
+  def escape_meta_char( text )
+    text.gsub( META_CHAR_RE ) do |s|
       '&#x%x;' % s[1]
     end
   end
+end

いきなり Array#to_sと関係ないが String#[index] が Integerに代えて一文字の Stringを返すようになった対策。

 hiki/util.rb: Hiki::Util#word_diff

      if digest
-        return View.new( diff, src.encoding, src.eol ).to_html_digest(overriding_tags, false).to_s.gsub( %r|<br />|, '' ).gsub( %r|\n</ins>|, "</ins>\n" )
+        return View.new( diff, src.encoding, src.eol ).to_html_digest(overriding_tags, false).join(\).gsub( %r|<br />|,  ).gsub( %r|\n</ins>|, "</ins>\n" )
      else
-        return View.new( diff, src.encoding, src.eol ).to_html(overriding_tags, false).to_s.gsub( %r|<br />|, '' ).gsub( %r|\n</ins>|, "</ins>\n" )
+        return View.new( diff, src.encoding, src.eol ).to_html(overriding_tags, false).join(\).gsub( %r|<br />|,  ).gsub( %r|\n</ins>|, "</ins>\n" )
      end

 hiki/util.rb: Hiki::Util#word_diff_text

      if digest
-        return View.new( diff, src.encoding, src.eol ).to_wdiff_digest({}, false).join.gsub( %r|\n\+\}|, "+}\n" )
+        return View.new( diff, src.encoding, src.eol ).to_wdiff_digest({}, false).join(nil).gsub( %r|\n\+\}|, "+}\n" )
      else
-        return View.new( diff, src.encoding, src.eol ).to_wdiff({}, false).join.gsub( %r|\n\+\}|, "+}\n" )
+        return View.new( diff, src.encoding, src.eol ).to_wdiff({}, false).join(nil).gsub( %r|\n\+\}|, "+}\n" )
      end

join('')か join(nil)か統一しろよ、とセルフツッコミ。

 hiki/util.rb: Hiki::Util#compare_by_line_word

-          before_change = Document.new(line[1].to_s,
+          before_change = Document.new(line[1].join(''),
                                       doc1.encoding, doc1.eol)
-          after_change  = Document.new(line[2].to_s,
+          after_change  = Document.new(line[2].join(''),
                                       doc2.encoding, doc2.eol)

 hiki/docdiff/view.rb: View#apply_style

       if block_given?
-        source = yield block[1].to_s
-        target = yield block[2].to_s
+        source = yield block[1].to_a.join ''
+        target = yield block[2].to_a.join ''
       else
-        source = block[1].to_s
-        target = block[2].to_s
+        source = block[1].to_a.join ''
+        target = block[2].to_a.join ''
       end

block[i]は nilの可能性があるので to_a.join

 hiki/docdiff/view.rb: View#apply_style_digest

       if block_given?
-        source = yield entry[1].to_s
-        target = yield entry[2].to_s
+        source = yield entry[1].to_a.join ''
+        target = yield entry[2].to_a.join ''
       else
-        source = entry[1].to_s
-        target = entry[2].to_s
+        source = entry[1].to_a.join ''
+        target = entry[2].to_a.join ''
         end
       if  i == 0
         context_pre  = ""  # no pre context for the first entry
       else
-        context_pre  = @difference[i-1][1].to_s.scan(context_pre_pat).to_s
+        context_pre  = @difference[i-1][1].to_a.join('').scan(context_pre_pat).to_s
       end
       if (i + 1) == @difference.size
         context_post = ""  # no post context for the last entry
       else
-        context_post = @difference[i+1][1].to_s.scan(context_post_pat).to_s
+        context_post = @difference[i+1][1].to_a.join('').scan(context_post_pat).to_s
       end

 hiki/docdiff/view.rb: View#source_lines

   def source_lines()
     if @source_lines == nil
-      @source_lines = @difference.collect{|entry| entry[1]}.join.scan_lines(@eol)
+      @source_lines = @difference.collect{|entry| entry[1]}.join(nil).scan_lines(@eol)

 hiki/docdiff/view.rb: View#target_lines

   def target_lines()
     if @target_lines == nil
-      @target_lines = @difference.collect{|entry| entry[2]}.join.scan_lines(@eol)
+      @target_lines = @difference.collect{|entry| entry[2]}.join(nil).scan_lines(@eol)

[Hiki][Ruby]プラグイン記法

 {{hoge a b 5}}      #=> hoge('a', 'b', 5)
 {{hoge 'a' b(5)}}   #=> hoge('a', 'b', 5)
 {{hoge ,a(, b)(5)}} #=> hoge('a', 'b', 5)
 {{hoge, a, b, 5}}   #=> PluginException('not plugin method: hoge,')
 {{hoge; hage}}      #=> PluginException

文法の緩さとか、一つのメソッドしか呼べないとか、嫌すぎる。

そりゃあ Rubyで

require digest/md5

という風にライブラリ名をクォーテーションで括らずに書けたら楽だなとかは考えるし、Symbolが Stringのサブクラスになったときは

require :sqlite3

が通るのを一番に確認したけど、

defined? printf
alias printg printf

を見て、

  • printfって何?クラスは?
  • printgと printfの間にカンマがないけどどういう文法? alias専用ルール?

というのと同種の嫌悪を感じる。


2006年12月30日 (土)

[Ruby][iPod] ipodpl.rb

前々回(9月12日)前回(12月12日)

結局

> ruby ipodpl.rb I:\ artist:"小松 未歩" rating:5 rating:4 or and --album-shuffle > temp.m3u

という形になった。

最近は iPodから曲が溢れてきていて、HDDが安いこともあって iPodに入ってる曲や溢れた曲、全て PCに入っている。

iPodが唯一のミュージックライブラリだった 9月とは状況が変わっているので iPod内の曲を PCから検索できてもあまり嬉しくない。iPodより多くの曲が PCに入っているから。

一応、産物

予め iTunesDBから使えそうな情報を拾って SQLite3形式のデータベースに登録しておいて、検索は SQL任せ、という仕様です。

 2007-02-11

Songbirdのデータベースは最初から SQLiteだ。良き哉良き哉。


2006年12月12日 (火)

[Ruby][SWIG][iPod] Rubyから iPodのデータベースを読む

  1. libipod-0.1を bcc32でコンパイル
  2. ipod/ipod.hをベースに swigで Ruby用のラッパー作成
  3. 手持ちの iPod(第3世代*/ソフトウェアバージョン2.3)のデータベースは読めた

* 再生時間 8時間は短いけど、タッチボタンによる軽快な操作は捨てがたい。


2006年10月30日 (月)

[Ruby][SQLite] sqlite3-ruby-1.1.0の DLドライバを Ruby-1.9.0(20061029版)添付の dl2に*不完全*対応。

29日に Ruby1.9をダウンロードした。添付ライブラリのRipperが、m4sugar.m4が見つからない、というエラーでコンパイルできない以外は問題なくインストール完了。RUBY_PLATFORMは i386-bccwin32。

Ruby1.8.5で動いている http://vvvvvv.sakura.ne.jp/ds14050/buch/ が Ruby1.9でも動くのか試してみると、sqlite3-rubyの Nativeドライバ、DLドライバが両方とも動かない。Nativeは当然として、DLが動かないのは Ruby1.9では ruby-dl2が dlとして添付されているから。

出てくるエラーを順番に潰す過程でやったことは定数名の置き換えが殆ど。そんなことしかできません。コールバック関数を SQLiteに渡す authorizerや create_function関連は自分で使っていないので何もしていない。DL.callbackが存在しない為にエラーが出るのは間違いない。dl2では bindを使うのだろうか?

以下は変更点のリスト

* sqlite3/driver/dl/api.rb:38
   -extend ::DL::Importable
   +extend ::DL::Importer

単なる名称変更。

* sqlite3/driver/dl/api.rb:92
   -extern "ibool sqlite3_complete(const char*)"
   +extern "int sqlite3_complete(const char*)"
* sqlite3/driver/dl/api.rb:93
   -extern "ibool sqlite3_complete16(const void*)"
   +extern "int sqlite3_complete16(const void*)"
* sqlite3/driver/dl/driver.rb:96
   -API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql+"\0" )
   +API.send( utf16 ? :sqlite3_complete16 : :sqlite3_complete, sql+"\0" ) != 0

iboolという返り値(戻り値?)の型が dl2では定義されていない(定義しようがない?)ので、intを bool値として受け取るのは諦めて、返り値を利用するドライバの方で非0かどうか調べる。

* sqlite3/driver/dl/driver.rb:40
   -DL.sizeof("L")
   +DL::SIZEOF_LONG

sizeofというメソッドは dl2の DL::Importerモジュールにもあるが使い方がわからないし、定数の方が良い。

* sqlite3/driver/dl/driver.rb:*
   -DL::PtrData
   +DL::CPtr
* sqlite3/driver/dl/driver.rb:242,247,252
   -result ? result.to_s : nil
   +result.null? ? nil : result.to_s

DL::CPtrが DL::PtrDataと完全に互換な置き換えなのかわからないが当面のエラーは消えた。

DLL関数の返値がポインタの場合は常に CPtrが返ってくるらしく、CPtrの指すアドレスが NULLの場合でも Ruby的には nilではないので「result ? result.to_s : nil」が常に result.to_sになり、ぬるぽエラーになることがある。PtrDataとは振る舞いが違う?

* lib/ruby/1.9/dl/value.rb:72
   -return [arg].pack("p").unpack("l!")[0]
   +return [arg.dup].pack("p").unpack("l!")[0]
* lib/ruby/1.9/dl/value.rb:74
   -return [arg].pack("p").unpack("q")[0]
   +return [arg.dup].pack("p").unpack("q")[0]

frozenオブジェクトを変更しようとした、ってエラーがでるので間に合わせで Rubyの添付ライブラリの方を修整。どこから frozenオブジェクトが渡されたのやら。

 追記(2006-11-01):Nativeドライバ

swigが sqlite3_api.iを基に出力する sqlite3_api_wrap.cを以下のように置換したら自分が使用している範囲では動いている。

-RSTRING()->ptr
+RSTRING_PTR()
-RSTRING()->len
+RSTRING_LEN()

2006年08月22日 (火)

[Ruby] 1.8系でもはまる、String#split

irb(main):001:0> RUBY_VERSION
=> "1.8.4"
irb(main):002:0> '//a//b//'.split('/')
=> [, , "a", "", "b"]
irb(main):003:0> '//a//b//'.split('/', 99)
=> [, , "a", , "b", , ""]

末尾のスラッシュが無かったことに……。< Perl由来のようです

因みに 1.6系のはまりどころは splitの第一引数に 2文字以上の文字列を与えた場合、勝手に正規表現にコンパイルされてしまうところ。


2006年06月26日 (月)

[Ruby] REXML::Element#xpathがおかしい

 1. 適当なXMLファイルの各要素ごとに xpathを列挙して現象を確認

/ItemLookupResponse
/ItemLookupResponse/OperationRequest
/ItemLookupResponse/OperationRequest/HTTPHeaders
/ItemLookupResponse/OperationRequest/RequestId
/ItemLookupResponse/OperationRequest/Arguments
/ItemLookupResponse/OperationRequest/RequestProcessingTime
/ItemLookupResponse/OperationRequest/HTTPHeaders/Header
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument

pathが重複している。

 2. xpathの定義(rexml/element.rb)

    def xpath
      path_elements = []
      cur = self
      path_elements << __to_xpath_helper( self )
      while cur.parent
        cur = cur.parent
        path_elements << __to_xpath_helper( cur )
      end
      return path_elements.reverse.join( "/" )
    end

pathの各要素は __to_xpath_helperで取ってきている。

 3. __to_xpath_helperの定義(rexml/element.rb)

    def __to_xpath_helper node
      rv = node.expanded_name
      if node.parent
        results = node.parent.find_all {|n|
          n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name
        }
        if results.length > 1
          idx = results.index( node )
          rv << "[#{idx+1}]"
        end
      end
      rv
    end

node.expanded_nameを破壊的に変更している。("Argument" -> "Argument[1]")

 4. 修正

D:\ruby\lib\ruby\1.8\rexml>diff -u element.rb~ element.rb
--- element.rb~      2005-08-12 21:08:47.000000000 +0900
+++ element.rb       2006-06-27 00:36:58.546875000 +0900
@@ -720,7 +720,8 @@
         }
         if results.length > 1
           idx = results.index( node )
-          rv << "[#{idx+1}]"
+          rv += "[#{idx+1}]"
         end
       end
       rv

 5. もう一度列挙して結果を確認

/ItemLookupResponse
/ItemLookupResponse/OperationRequest
/ItemLookupResponse/OperationRequest/HTTPHeaders
/ItemLookupResponse/OperationRequest/RequestId
/ItemLookupResponse/OperationRequest/Arguments
/ItemLookupResponse/OperationRequest/RequestProcessingTime
/ItemLookupResponse/OperationRequest/HTTPHeaders/Header
/ItemLookupResponse/OperationRequest/Arguments/Argument[1]
/ItemLookupResponse/OperationRequest/Arguments/Argument[2]
/ItemLookupResponse/OperationRequest/Arguments/Argument[3]
/ItemLookupResponse/OperationRequest/Arguments/Argument[4]
/ItemLookupResponse/OperationRequest/Arguments/Argument[5]
/ItemLookupResponse/OperationRequest/Arguments/Argument[6]
/ItemLookupResponse/OperationRequest/Arguments/Argument[7]

 6. REXML 3.1.4

をを、直っている。というわけで上記は ruby-1.8.4に付随する REXML 3.1.3限定の話でした。

    def __to_xpath_helper node
      rv = node.expanded_name.clone
      if node.parent
        results = node.parent.find_all {|n|
          n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name
        }
        if results.length > 1
          idx = results.index( node )
          rv << "[#{idx+1}]"
        end
      end
      rv
    end

3.1.3 からの ChangeLog。(そのうち 3.1.4からの ChangeLogになりそう)

 7. レンタルサーバーの REXMLを更新することはできないので

間に合わせにこんなのを紛れ込ませる。

if(::REXML::Version < '3.1.4')
module ::REXML
  class Element
    def __to_xpath_helper node
      rv = node.expanded_name.clone
      if node.parent
        results = node.parent.find_all {|n|
          n.kind_of?(REXML::Element) and n.expanded_name == node.expanded_name
        }
        if results.length > 1
          idx = results.index( node )
          rv << "[#{idx+1}]"
        end
      end
      rv
    end
  end
end
end

2006年06月08日 (木)

[Ruby][SQLite] sqlite3-rubyのバグ。SQLite3::Database#create_aggregate()などに注意

Segmentation faultが起こったり起こらなかったり、起こったとしても(特定のパターンはあるにせよ)違う場所だったりとはっきりしないエラーに困らされた。

原因が create_aggregateで独自に定義した集約関数を使ってるからだということはわかってる。RubyForgeに関連のありそうな投稿を見つけた。

原因はリファレンスが切れて GCに回収されてしまったオブジェクトを参照しようとしてることにある、ということで良いか? 何ともヘタレな回避策は↓。

GC.disable; db.execute(sql); GC.enable

sqlite3-rubyはもうメンテされないのかね。名前付きプレースホルダの問題も解決されないし。

sql = 'SELECT * FROM Books WHERE Title = :title;'
db.execute(sql, {'title'=>'惑星をつぐ者'}) #=> no such bind parameter 'title' とかなんとか
db.execute(sql, {':title'=>'星を継ぐもの'}) #=>(゜Д゜ )ウマー

bind_parameterのキーに普通はコロンを付けたりしないよね、多分。


2006年05月30日 (火)

[Ruby] Windowsの環境変数と文字コードのわからん

UTF-8な文字列を環境変数に設定して読み出すと尻切れ。

C:\Documents and Settings\ds14050\デスクトップ>irb
irb(main):001:0> sjis = '高殿 円\' # 『銃姫』を読んでる。
=> "\215\202\223a \211~"
irb(main):002:0> ENV['hoge'] = sjis
=> "\215\202\223a \211~"
irb(main):003:0> ENV['hoge'] == sjis
=> true
irb(main):004:0> require 'nkf'
=> true
irb(main):005:0> utf8 = NKF::nkf('-w', sjis)
=> "\351\253\230\346\256\277 \345\206\206"
irb(main):006:0> ENV['hoge'] = utf8
=> "\351\253\230\346\256\277 \345\206\206"
irb(main):007:0> ENV['hoge'] == utf8
=> false
irb(main):008:0> ENV['hoge']
=> "\351\253\230\346\256\277 \345\206"

日本語の PATH_INFOが文字化けするのに閉口してて、Apacheだとか mod_rewriteが悪さをしてるのかと思ってたけど環境変数を経由してたところに問題があったのかも。

文句を言ってても解決しないので REQUEST_URIから SCRIPT_NAME相当部分を取り除いてから URLデコードして自分で PATH_INFOを手に入れる。

ところで URLエンコードされたスラッシュ(%2F)が含まれてた場合、PATH_INFOを参照するだけではその存在がわからないと思うんだけど。やっぱり PATH_INFOって使えない?(<< いやいや PATHと名の付くものにスラッシュやバックスラッシュを含めるのが間違い)


2006年04月01日 (土)

[Ruby][SQLite] create_function()が強力。俺の探してたのはこれだ。

SQLite3の提供する集約関数は avg, count, max, min, sum, totalが全てで、どれも数値を引数にとる。sumが与えられた数値の合計を返すように、与えられた文字列を全て連結して返す集約関数(MySQLの GROUP_CONCATがまさしくそれ)がないものかと探していた。それが無いなら無いで SELECT, JOIN, UNION, CASEといった標準的なものを使って、特定の列の値を(行をまたいで)連結したりできないものかと考えたけど、行と行の独立は破れなかった。

話は変わって、これ↓は昨日見つけた目から鱗ものの文書。

http://www.geocities.jp/mickindex/database/idx_database.html

対象読者として次のような項目が挙げられている。

  • なぜ"関係"モデルという抽象的な用語を使うのか分からない。"表"モデルでいいじゃない。
  • なぜ「= NULL」ではなく「IS NULL」と書かねばならないのか分からない。
  • E.F.Codd、C.J.Date、J.Celko、F.Pascal の本を読んだことがない。
  • IN述語とEXISTS述語では、IN の方が直観的に分かりやすいから好きだ。
  • IN述語、EXISTS述語、LIKE述語・・・「述語」って何だ?
  • テーブル設計のときは滅多に NOT NULL制約を付けない。しかもそれが大罪であることの自覚がない。
  • SQL で CASE式を使ったことがない。

「INは使ったことあるけど EXISTSは知らない」「何故 = NULLと書いたものが IS NULLと同じ結果を返さないのか分からない」「 CASE?そんなんあった?」「DEFAULT '' は多用するけど、NOT NULLは付けてない。付けるべき理由があるなら知りたいよ」と、冒頭からがっちりハートを鷲掴み。

このサイトの文書がどれも興味深く、有用なのはさておいて、CASEの存在を今まで知らなかったことに少なからずショックを受けた。SQLiteの本家サイトにもちゃんと記述されているというのに。(→Query Language Understood by SQLite: expression)

そこで改めて SQLiteでできることをおさらいしてみたところ発見されたのが create_function。(→C/C++ Interface For SQLite Version 3)

C/C++ APIだから Rubyから使うには dl を使わなあかんのかと思ったら、何と SQLiteといつもセットで利用しているsqlite3-rubySQLite3::Databaseオブジェクトにはその名も create_aggregateなんてメソッドが存在するのですねえ。一体今までどこに目を付けていたのかと……。

そんなわけで、無いなら作ってしまえ文字列連結集約関数〜♪。

require 'sqlite3'
database = SQLite3::Database.new('hoge.db');

# concatという不定数の引数をとる集約関数を作成。
# 第一引数(val)は連結される文字列。
# 第二引数(sep)は valと valの間に挿入されるセパレータ。
# 第三引数(sortval)は valを連結する前に並び替えたい場合にソートキーとして利用される文字列/数値。(省略されたり NULLの場合はソートしない)
# 第四引数(desc)はソートの昇順/降順を切り替える。(省略/NULL=>昇順, その他=>降順)
database.create_aggregate(name='concat', arity=-1){
	step {|func, val, sep, sortval, desc|
		func['separator'] = sep.to_s;
		func['sortdesc'] = !(desc.nil? || desc.null?);
		func['needsort'] = (func['needsort'] || !(sortval.nil? || sortval.null?));
		func['sortvaltype'] ||= (sortval.nil? || sortval.null?) ? nil : {:int=>:to_i, :float=>:to_f, :blob=>:to_blob, :text=>:to_s}[sortval.type];
		func['array'] ||= [];
		func['array'].push([ (sortval.nil? || sortval.null?) ? nil : sortval.send(func['sortvaltype']), val.to_s ]);
	}
	finalize {|func|
		arr = (func['array'] || []);
		arr = arr.sort_by{|x| x.nil? ? {:to_i=>0, :to_f=>0.0, :to_s=>''}[func['sortvaltype']] : x.first.dup } if(func['needsort']);
		arr.reverse! if(func['sortdesc']);
		func.result = arr.map{|x| x.last  }.join(func['separator']);
	}
}
  • val, sepなど集約関数の引数は SQLite3::Valueのインスタンス。SQLite3::Value#to_sは nilを返すこともあるので注意。
  • val, sepなど集約関数の引数はコールバックの度に上書きされるので、引数をそのまま func(SQLite3::Database::FunctionProxy)など、ブロックローカル変数以外の場所に保存すると ( ゜Д゜)マズー
  • コールバック用の step, finalizeを create_aggregateに渡す方法は Rubyの新文法ではなく sqlite3-rubyがコールバック関数を定義しやすいようにしてくれているだけ。
  • dupがないとソートした後で例外が発生する理由は?
  • スピードを考えたら(頻繁に使うクエリで)こんなの使っちゃダメ。

というわけで、探し物が見つかったということに満足しつつ、文字列の連結は集約関数でなく ERBスクリプトで行っている現在。

 authorizerを使えば

データベースに対するリードオンリーアクセスを保証したりもできるのねん。

CGI経由で渡された SQLも安全に実行できそうじゃね?

#!ruby -T4
eval(ENV['QUERY_STRING'])

と同程度かそれ以上に。

http://www.sqlite.org/capi3ref.html#sqlite3_set_authorizer から引用しておく。

The intent of this routine is to allow applications to safely execute user-entered SQL. An appropriate callback can deny the user-entered SQL access certain operations (ex: anything that changes the database) or to deny access to certain tables or columns within the database.