Let me code again...

Welcome to ijokarumawak's blog :)

N1QL: DMLのRETURNING句を調査しながらGo言語の基礎を学ぶ

昨日のEmbulk Output Couchbase プラグインを Scala で書く by zaneliさん、に続く、Couchbase Advent Calendar、12/15分の記事です。先日の記事、N1QL INSERTからの続きです。ほぼGo言語の勉強メモになってしまった!

まえがき

先日、N1QLでインサートしたドキュメントのメタデータを返す方法が分からず、もやもやしていました。例えば、次のようなクエリを実行するとき、タイムスタンプやUUIDを使って動的にKeyを生成した場合は、Keyが返ってきてほしいものですよね。

-- サンプル: 集計結果のJSONドキュメントを保存する
INSERT INTO `default` AS r
(PRIMARY KEY UUID(), VALUE {
    "count": cnt, "avg": avg, "min": min, "max": max})
SELECT COUNT(u) AS cnt, AVG(u.age) AS avg,
  MAX(u.age) AS max, MIN(u.age) AS min
  FROM `default` AS u WHERE u.age IS NOT MISSING
RETURNING r

実行結果は次のように返却されます:

[ {
    "r": {
      "avg": 16.6, "count": 5,
      "max": 34, "min": 5 }
  } ]

Goの勉強がてらQueryエンジンのソースコードを追ってみました。

結果から言うと、インサートしたドキュメントの返却方法はわかりませんでした。今、クエリ実装チームに問い合わせているところですw

というわけで、今日の記事はほとんど私のGo言語勉強メモです。

KVにインサートした結果のあたりからソースコードリーディング開始

前にDMLの更新モードを調査した際に、QueryサービスからDataサービスにドキュメントをインサートする箇所は分かったので、今回はそこから始めました。

execution/insert_send.go

func (this *SendInsert) flushBatch(context *Context) bool {
(中略)
	// Capture the inserted keys in case there is a RETURNING clause
	for i, k := range keys {
		av := value.NewAnnotatedValue(make(map[string]interface{}))
		av.SetAttachment("meta", map[string]interface{}{"id": k})
		av.SetField(this.plan.Alias(), dpairs[i].Value)
		if !this.sendItem(av) {
			return false
		}
	}

metaでインサートしたドキュメントのkeyをav.SetAttachmentしている。 avってなんだ?Attachmentってなんだ?

avはNewAnnotatedValueで作成されているもの。

Goでの変数代入、”=”と”:=”の違いは何?

:=は関数の中で変数を宣言するのに使える。varとTypeを書く必要ない。 パッケージレベルで書く場合はvar使わないといけない。 ってなことさえ知らないGo初心者です。。 A Tour of Go分かりやすくていいっすね。

this.sendItem(av)は?

そもそもthisって誰よ?

Goにはクラスは無いが、functionをstructに紐付けてmethodとして扱える。 先のコードではSendInsert structのflushBatchというmethodになる。 Goではthisに特別な意味合いはない。Stackoverflow

StructとAnonymous FieldでOO指向的なクラス階層

んーっと、でもinsert_send.goにはsendItemってfunctionは実装されていないんだけど、どこにあるのか。。? 最近Githubの検索機能がうまく動かずツラい。ソースをgrepすると、

execution/base.goにありました。

func (this *base) sendItem(item value.AnnotatedValue) bool {
	select {
  // stopChannelに何かあれば読み捨てて終了
	case <-this.stopChannel: // Never closed
		return false
	default:
	}

	select {
  // itemChannelにitemを送信
	case this.output.ItemChannel() <- item:
		return true
	case <-this.stopChannel: // Never closed
		return false
	}
}

これまた意味不明。SendInsertとbase structの関係は一体どこで紐付いてるのか?? おー、これがAnonymous fieldsってやつか。SendInsertにはbaseって無名フィールドが定義されていて、これがbaseを継承する感じになっているらしい。

Goroutine、channel、select

baseのsendItemが呼ばれることは分かったけどー。 まずはcaseの解読。おっと、switchじゃなくて、selectだ!

selectはcaseのいずれかがreadyになっているときにそれを実行する。Readyとはchannelにアイテムがあることだろう。channelはgoroutine間でデータをやりとりするもの。 A Tour of Go

channelはmake()しないと使えない。makeの引数は何ですか? 一つ目はchannelの型。 二つ目はbufferサイズ。ちなみに以下はdeadlockになる:

func main() {
	ch := make(chan int, 2)
	ch <- 1
	ch <- 2
	ch <- 3
	fmt.Println(<-ch)
	fmt.Println(<-ch)
}

execution/base.goでは、以下のようにitemChannelとstopChannelを定義している:

func newBase() base {
	return base{
    // pipelineCapは512
		itemChannel: make(value.AnnotatedChannel, GetPipelineCap()),
		stopChannel: make(StopChannel, 1),
	}
}

base.goのrunConsumer()を見ると、consumerにitemChannelから読み込んで渡している箇所がある。

			select {
			case item, ok = <-this.input.ItemChannel():
				if ok {
					ok = cons.processItem(item, context)
				}
			case <-this.stopChannel: // Never closed
				break loop
			}

ここまでくると、実行計画を見てconsumerを特定するのが良さげか?

N1QLの実行計画を元に動きを追ってみる

今回実行しているN1QLの実行計画を見ると、最後の方で、SendInsert -> InitialProject -> FinalProjectの流れとなっている。 普通に考えると、先ほどの文脈ではSendInsertのconsumerがInitialProjectになるはず。

execution/project_initial.goを見ると、ありました。processItem! ここに渡ってきたitemのattachmentにmetaがあるはず。

Termsってのは、実行計画にあったresult_termsだろう。 このあたりでMETA()が何やってるか見ておこう。

やはりattachmentのmetaを取得しているのだけど、なぜ返ってこないんだろう。

func (this *Meta) Evaluate(item value.Value, context Context) (value.Value, error) {
(中略)
	switch val := val.(type) {
	case value.AnnotatedValue:
    // attachmentのmetaを取得している。
		return value.NewValue(val.GetAttachment("meta")), nil

まとめ

結局RETURNING句でKeyを返す方法はわかりませんでしたが、Go言語の基本的なことが分かってきたので良しとします! Keyを返す方法が分かったら共有しまーす。