jmoses-couchbase 1.3.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (91) hide show
  1. data/.gitignore +15 -0
  2. data/.travis.yml +22 -0
  3. data/.yardopts +5 -0
  4. data/CONTRIBUTING.markdown +75 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE +201 -0
  7. data/Makefile +3 -0
  8. data/README.markdown +665 -0
  9. data/RELEASE_NOTES.markdown +819 -0
  10. data/Rakefile +20 -0
  11. data/couchbase.gemspec +49 -0
  12. data/examples/chat-em/Gemfile +7 -0
  13. data/examples/chat-em/README.markdown +45 -0
  14. data/examples/chat-em/server.rb +82 -0
  15. data/examples/chat-goliath-grape/Gemfile +5 -0
  16. data/examples/chat-goliath-grape/README.markdown +50 -0
  17. data/examples/chat-goliath-grape/app.rb +67 -0
  18. data/examples/chat-goliath-grape/config/app.rb +20 -0
  19. data/examples/transcoders/Gemfile +3 -0
  20. data/examples/transcoders/README.markdown +59 -0
  21. data/examples/transcoders/cb-zcat +40 -0
  22. data/examples/transcoders/cb-zcp +45 -0
  23. data/examples/transcoders/gzip_transcoder.rb +49 -0
  24. data/examples/transcoders/options.rb +54 -0
  25. data/ext/couchbase_ext/.gitignore +4 -0
  26. data/ext/couchbase_ext/arguments.c +956 -0
  27. data/ext/couchbase_ext/arithmetic.c +316 -0
  28. data/ext/couchbase_ext/bucket.c +1373 -0
  29. data/ext/couchbase_ext/context.c +65 -0
  30. data/ext/couchbase_ext/couchbase_ext.c +1364 -0
  31. data/ext/couchbase_ext/couchbase_ext.h +644 -0
  32. data/ext/couchbase_ext/delete.c +163 -0
  33. data/ext/couchbase_ext/eventmachine_plugin.c +452 -0
  34. data/ext/couchbase_ext/extconf.rb +169 -0
  35. data/ext/couchbase_ext/get.c +316 -0
  36. data/ext/couchbase_ext/gethrtime.c +129 -0
  37. data/ext/couchbase_ext/http.c +432 -0
  38. data/ext/couchbase_ext/multithread_plugin.c +1090 -0
  39. data/ext/couchbase_ext/observe.c +171 -0
  40. data/ext/couchbase_ext/plugin_common.c +171 -0
  41. data/ext/couchbase_ext/result.c +129 -0
  42. data/ext/couchbase_ext/stats.c +163 -0
  43. data/ext/couchbase_ext/store.c +542 -0
  44. data/ext/couchbase_ext/timer.c +192 -0
  45. data/ext/couchbase_ext/touch.c +186 -0
  46. data/ext/couchbase_ext/unlock.c +176 -0
  47. data/ext/couchbase_ext/utils.c +551 -0
  48. data/ext/couchbase_ext/version.c +142 -0
  49. data/lib/action_dispatch/middleware/session/couchbase_store.rb +38 -0
  50. data/lib/active_support/cache/couchbase_store.rb +430 -0
  51. data/lib/couchbase.rb +155 -0
  52. data/lib/couchbase/bucket.rb +457 -0
  53. data/lib/couchbase/cluster.rb +119 -0
  54. data/lib/couchbase/connection_pool.rb +58 -0
  55. data/lib/couchbase/constants.rb +12 -0
  56. data/lib/couchbase/result.rb +26 -0
  57. data/lib/couchbase/transcoder.rb +120 -0
  58. data/lib/couchbase/utils.rb +62 -0
  59. data/lib/couchbase/version.rb +21 -0
  60. data/lib/couchbase/view.rb +506 -0
  61. data/lib/couchbase/view_row.rb +272 -0
  62. data/lib/ext/multi_json_fix.rb +56 -0
  63. data/lib/rack/session/couchbase.rb +108 -0
  64. data/tasks/benchmark.rake +6 -0
  65. data/tasks/compile.rake +160 -0
  66. data/tasks/test.rake +100 -0
  67. data/tasks/util.rake +21 -0
  68. data/test/profile/.gitignore +1 -0
  69. data/test/profile/Gemfile +6 -0
  70. data/test/profile/benchmark.rb +195 -0
  71. data/test/setup.rb +178 -0
  72. data/test/test_arithmetic.rb +185 -0
  73. data/test/test_async.rb +316 -0
  74. data/test/test_bucket.rb +276 -0
  75. data/test/test_cas.rb +235 -0
  76. data/test/test_couchbase.rb +77 -0
  77. data/test/test_couchbase_connection_pool.rb +77 -0
  78. data/test/test_couchbase_rails_cache_store.rb +361 -0
  79. data/test/test_delete.rb +120 -0
  80. data/test/test_errors.rb +82 -0
  81. data/test/test_eventmachine.rb +70 -0
  82. data/test/test_format.rb +164 -0
  83. data/test/test_get.rb +407 -0
  84. data/test/test_stats.rb +57 -0
  85. data/test/test_store.rb +216 -0
  86. data/test/test_timer.rb +42 -0
  87. data/test/test_touch.rb +97 -0
  88. data/test/test_unlock.rb +119 -0
  89. data/test/test_utils.rb +58 -0
  90. data/test/test_version.rb +52 -0
  91. metadata +353 -0
@@ -0,0 +1,119 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+
20
+ class Cluster
21
+
22
+ # Establish connection to the cluster for administration
23
+ #
24
+ # @param [Hash] options The connection parameter
25
+ # @option options [String] :username The username
26
+ # @option options [String] :password The password
27
+ # @option options [String] :pool ("default") The pool name
28
+ # @option options [String] :hostname ("localhost") The hostname
29
+ # @option options [String] :port (8091) The port
30
+ def initialize(options = {})
31
+ if options[:username].nil? || options[:password].nil?
32
+ raise ArgumentError, "username and password mandatory to connect to the cluster"
33
+ end
34
+ @connection = Bucket.new(options.merge(:type => :cluster))
35
+ end
36
+
37
+ # Create data bucket
38
+ #
39
+ # @param [String] name The name of the bucket
40
+ # @param [Hash] options The bucket parameters
41
+ # @option options [String] :bucket_type ("couchbase") The type of the
42
+ # bucket. Possible values are "memcached" and "couchbase".
43
+ # @option options [Fixnum] :ram_quota (100) The RAM quota in megabytes.
44
+ # @option options [Fixnum] :replica_number (1) The number of replicas of
45
+ # each document. Minimum 0, maximum 3.
46
+ # @option options [String] :auth_type ("sasl") The authentication type.
47
+ # Possible values are "sasl" and "none". Note you should specify free
48
+ # port for "none"
49
+ # @option options [Fixnum] :proxy_port The port for moxi
50
+ # @option options [true, false] :replica_index (true) Disable or
51
+ # enable indexes for bucket replicas
52
+ # @option options [true, false] :flush_enabled (false) Enables the
53
+ # 'flush all' functionality on the specified bucket.
54
+ # @option options [true, false] :parallel_db_and_view_compaction (false)
55
+ # Indicates whether database and view files on disk can be
56
+ # compacted simultaneously
57
+ #
58
+ def create_bucket(name, options = {})
59
+ defaults = {
60
+ :type => "couchbase",
61
+ :ram_quota => 100,
62
+ :replica_number => 1,
63
+ :auth_type => "sasl",
64
+ :sasl_password => "",
65
+ :proxy_port => nil,
66
+ :flush_enabled => false,
67
+ :replica_index => true,
68
+ :parallel_db_and_view_compaction => false
69
+ }
70
+ options = defaults.merge(options)
71
+ params = {"name" => name}
72
+ params["bucketType"] = options[:type]
73
+ params["ramQuotaMB"] = options[:ram_quota]
74
+ params["replicaNumber"] = options[:replica_number]
75
+ params["authType"] = options[:auth_type]
76
+ params["saslPassword"] = options[:sasl_password]
77
+ params["proxyPort"] = options[:proxy_port]
78
+ params["flushEnabled"] = !!options[:flush_enabled]
79
+ params["replicaIndex"] = !!options[:replica_index]
80
+ params["parallelDBAndViewCompaction"] = !!options[:parallel_db_and_view_compaction]
81
+ payload = Utils.encode_params(params.reject!{|k, v| v.nil?})
82
+ request = @connection.make_http_request("/pools/default/buckets",
83
+ :content_type => "application/x-www-form-urlencoded",
84
+ :type => :management,
85
+ :method => :post,
86
+ :extended => true,
87
+ :body => payload)
88
+ response = nil
89
+ request.on_body do |r|
90
+ response = r
91
+ response.instance_variable_set("@operation", :create_bucket)
92
+ yield(response) if block_given?
93
+ end
94
+ request.continue
95
+ response
96
+ end
97
+
98
+ # Delete the data bucket
99
+ #
100
+ # @param [String] name The name of the bucket
101
+ # @param [Hash] options
102
+ def delete_bucket(name, options = {})
103
+ request = @connection.make_http_request("/pools/default/buckets/#{name}",
104
+ :type => :management,
105
+ :method => :delete,
106
+ :extended => true)
107
+ response = nil
108
+ request.on_body do |r|
109
+ response = r
110
+ response.instance_variable_set("@operation", :delete_bucket)
111
+ yield(response) if block_given?
112
+ end
113
+ request.continue
114
+ response
115
+ end
116
+
117
+ end
118
+
119
+ end
@@ -0,0 +1,58 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2013 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ if RUBY_VERSION.to_f < 1.9
19
+ raise LoadError, "connection_pool gem doesn't support ruby < 1.9"
20
+ end
21
+ require 'connection_pool'
22
+
23
+ module Couchbase
24
+ class ConnectionPool
25
+
26
+ def initialize(pool_size = 5, *args)
27
+ @pool = ::ConnectionPool.new(:size => pool_size) { ::Couchbase::Bucket.new(*args) }
28
+ end
29
+
30
+ def with
31
+ yield @pool.checkout
32
+ ensure
33
+ @pool.checkin
34
+ end
35
+
36
+ def respond_to?(id, *args)
37
+ super || @pool.with { |c| c.respond_to?(id, *args) }
38
+ end
39
+
40
+ def method_missing(name, *args, &block)
41
+ define_proxy_method(name)
42
+ send(name, *args, &block)
43
+ end
44
+
45
+ protected
46
+
47
+ def define_proxy_method(name)
48
+ self.class.class_eval <<-RUBY
49
+ def #{name}(*args, &block)
50
+ @pool.with do |connection|
51
+ connection.send(#{name.inspect}, *args, &block)
52
+ end
53
+ end
54
+ RUBY
55
+ end
56
+
57
+ end
58
+ end
@@ -0,0 +1,12 @@
1
+ module Couchbase
2
+ module Constants # :nodoc:
3
+ S_ID = 'id'.freeze
4
+ S_DOC = 'doc'.freeze
5
+ S_VALUE = 'value'.freeze
6
+ S_META = 'meta'.freeze
7
+ S_FLAGS = 'flags'.freeze
8
+ S_CAS = 'cas'.freeze
9
+ S_KEY = 'key'.freeze
10
+ S_IS_LAST = Object.new.freeze
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+ class Result
20
+ def initialize(attrs = {})
21
+ attrs.each do |k, v|
22
+ instance_variable_set("@#{k}", v) if respond_to?(k)
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,120 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2013 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'multi_json'
19
+ require 'ext/multi_json_fix'
20
+
21
+ module Couchbase
22
+
23
+ module Transcoder
24
+
25
+ module Compat
26
+ def self.enable!
27
+ @disabled = false
28
+ end
29
+
30
+ def self.disable!
31
+ @disabled = true
32
+ end
33
+
34
+ def self.enabled?
35
+ !@disabled
36
+ end
37
+
38
+ def self.guess_and_load(blob, flags, options = {})
39
+ case flags & Bucket::FMT_MASK
40
+ when Bucket::FMT_DOCUMENT
41
+ MultiJson.load(blob)
42
+ when Bucket::FMT_MARSHAL
43
+ ::Marshal.load(blob)
44
+ when Bucket::FMT_PLAIN
45
+ blob
46
+ else
47
+ raise ArgumentError, "unexpected flags (0x%02x)" % flags
48
+ end
49
+ end
50
+ end
51
+
52
+ module Document
53
+ def self.dump(obj, flags, options = {})
54
+ [
55
+ MultiJson.dump(obj),
56
+ (flags & ~Bucket::FMT_MASK) | Bucket::FMT_DOCUMENT
57
+ ]
58
+ end
59
+
60
+ def self.load(blob, flags, options = {})
61
+ if (flags & Bucket::FMT_MASK) == Bucket::FMT_DOCUMENT || options[:forced]
62
+ MultiJson.load(blob)
63
+ else
64
+ if Compat.enabled?
65
+ return Compat.guess_and_load(blob, flags, options)
66
+ else
67
+ raise ArgumentError,
68
+ "unexpected flags (0x%02x instead of 0x%02x)" % [flags, Bucket::FMT_DOCUMENT]
69
+ end
70
+ end
71
+ end
72
+ end
73
+
74
+ module Marshal
75
+ def self.dump(obj, flags, options = {})
76
+ [
77
+ ::Marshal.dump(obj),
78
+ (flags & ~Bucket::FMT_MASK) | Bucket::FMT_MARSHAL
79
+ ]
80
+ end
81
+
82
+ def self.load(blob, flags, options = {})
83
+ if (flags & Bucket::FMT_MASK) == Bucket::FMT_MARSHAL || options[:forced]
84
+ ::Marshal.load(blob)
85
+ else
86
+ if Compat.enabled?
87
+ return Compat.guess_and_load(blob, flags, options)
88
+ else
89
+ raise ArgumentError,
90
+ "unexpected flags (0x%02x instead of 0x%02x)" % [flags, Bucket::FMT_MARSHAL]
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ module Plain
97
+ def self.dump(obj, flags, options = {})
98
+ [
99
+ obj,
100
+ (flags & ~Bucket::FMT_MASK) | Bucket::FMT_PLAIN
101
+ ]
102
+ end
103
+
104
+ def self.load(blob, flags, options = {})
105
+ if (flags & Bucket::FMT_MASK) == Bucket::FMT_PLAIN || options[:forced]
106
+ blob
107
+ else
108
+ if Compat.enabled?
109
+ return Compat.guess_and_load(blob, flags, options)
110
+ else
111
+ raise ArgumentError,
112
+ "unexpected flags (0x%02x instead of 0x%02x)" % [flags, Bucket::FMT_PLAIN]
113
+ end
114
+ end
115
+ end
116
+ end
117
+
118
+ end
119
+
120
+ end
@@ -0,0 +1,62 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011-2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ module Couchbase
19
+
20
+ class Utils
21
+
22
+ def self.encode_params(params)
23
+ params.map do |k, v|
24
+ next if !v && k.to_s == "group"
25
+ if %w{key keys startkey endkey start_key end_key}.include?(k.to_s)
26
+ v = MultiJson.dump(v)
27
+ end
28
+ if v.class == Array
29
+ build_query(v.map { |x| [k, x] })
30
+ else
31
+ "#{escape(k)}=#{escape(v)}"
32
+ end
33
+ end.compact.join("&")
34
+ end
35
+
36
+ def self.build_query(uri, params = nil)
37
+ uri = uri.dup
38
+ return uri if params.nil? || params.empty?
39
+ uri << "?" << encode_params(params)
40
+ end
41
+
42
+ def self.escape(s)
43
+ s.to_s.gsub(/([^ a-zA-Z0-9_.-]+)/nu) {
44
+ '%'+$1.unpack('H2'*bytesize($1)).join('%').upcase
45
+ }.tr(' ', '+')
46
+ end
47
+
48
+ # Return the bytesize of String; uses String#size under Ruby 1.8 and
49
+ # String#bytesize under 1.9.
50
+ if ''.respond_to?(:bytesize)
51
+ def self.bytesize(string)
52
+ string.bytesize
53
+ end
54
+ else
55
+ def self.bytesize(string)
56
+ string.size
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ end
@@ -0,0 +1,21 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011, 2012 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ # Couchbase ruby client
19
+ module Couchbase
20
+ VERSION = "1.3.6"
21
+ end
@@ -0,0 +1,506 @@
1
+ # Author:: Couchbase <info@couchbase.com>
2
+ # Copyright:: 2011 Couchbase, Inc.
3
+ # License:: Apache License, Version 2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+ #
17
+
18
+ require 'base64'
19
+
20
+ module Couchbase
21
+
22
+ module Error
23
+ class View < Base
24
+ attr_reader :from, :reason
25
+
26
+ def initialize(from, reason, prefix = "SERVER: ")
27
+ @from = from
28
+ @reason = reason
29
+ super("#{prefix}#{from}: #{reason}")
30
+ end
31
+ end
32
+
33
+ class HTTP < Base
34
+ attr_reader :type, :reason
35
+
36
+ def parse_body!
37
+ if @body
38
+ hash = MultiJson.load(@body)
39
+ if hash["errors"]
40
+ @type = :invalid_arguments
41
+ @reason = hash["errors"].values.join(" ")
42
+ else
43
+ @type = hash["error"]
44
+ @reason = hash["reason"]
45
+ end
46
+ end
47
+ rescue MultiJson::DecodeError
48
+ @type = @reason = nil
49
+ end
50
+
51
+ def to_s
52
+ str = super
53
+ if @type || @reason
54
+ str.sub(/ \(/, ": #{[@type, @reason].compact.join(": ")} (")
55
+ else
56
+ str
57
+ end
58
+ end
59
+ end
60
+ end
61
+
62
+ # This class implements Couchbase View execution
63
+ #
64
+ # @see http://www.couchbase.com/docs/couchbase-manual-2.0/couchbase-views.html
65
+ class View
66
+ include Enumerable
67
+ include Constants
68
+
69
+ class ArrayWithTotalRows < Array # :nodoc:
70
+ attr_accessor :total_rows
71
+ alias total_entries total_rows
72
+ end
73
+
74
+ class AsyncHelper # :nodoc:
75
+ include Constants
76
+ EMPTY = []
77
+
78
+ def initialize(wrapper_class, bucket, include_docs, quiet, block)
79
+ @wrapper_class = wrapper_class
80
+ @bucket = bucket
81
+ @block = block
82
+ @quiet = quiet
83
+ @include_docs = include_docs
84
+ @queue = []
85
+ @first = @shift = 0
86
+ @completed = false
87
+ end
88
+
89
+ # Register object in the emitter.
90
+ def push(obj)
91
+ if @include_docs
92
+ @queue << obj
93
+ @bucket.get(obj[S_ID], :extended => true, :quiet => @quiet) do |res|
94
+ obj[S_DOC] = {
95
+ S_VALUE => res.value,
96
+ S_META => {
97
+ S_ID => obj[S_ID],
98
+ S_FLAGS => res.flags,
99
+ S_CAS => res.cas
100
+ }
101
+ }
102
+ check_for_ready_documents
103
+ end
104
+ else
105
+ old_obj = @queue.shift
106
+ @queue << obj
107
+ block_call(old_obj) if old_obj
108
+ end
109
+ end
110
+
111
+ def complete!
112
+ if @include_docs
113
+ @completed = true
114
+ check_for_ready_documents
115
+ elsif !@queue.empty?
116
+ obj = @queue.shift
117
+ obj[S_IS_LAST] = true
118
+ block_call obj
119
+ end
120
+ end
121
+
122
+ private
123
+
124
+ def block_call(obj)
125
+ @block.call @wrapper_class.wrap(@bucket, obj)
126
+ end
127
+
128
+ def check_for_ready_documents
129
+ shift = @shift
130
+ queue = @queue
131
+ save_last = @completed ? 0 : 1
132
+ while @first < queue.size + shift - save_last
133
+ obj = queue[@first - shift]
134
+ break unless obj[S_DOC]
135
+ queue[@first - shift] = nil
136
+ @first += 1
137
+ if @completed && @first == queue.size + shift
138
+ obj[S_IS_LAST] = true
139
+ end
140
+ block_call obj
141
+ end
142
+ if @first - shift > queue.size / 2
143
+ queue[0, @first - shift] = EMPTY
144
+ @shift = @first
145
+ end
146
+ end
147
+
148
+ end
149
+
150
+ attr_reader :params
151
+
152
+ # Set up view endpoint and optional params
153
+ #
154
+ # @param [Couchbase::Bucket] bucket Connection object which
155
+ # stores all info about how to make requests to Couchbase views.
156
+ #
157
+ # @param [String] endpoint Full Couchbase View URI.
158
+ #
159
+ # @param [Hash] params Optional parameter which will be passed to
160
+ # {View#fetch}
161
+ #
162
+ def initialize(bucket, endpoint, params = {})
163
+ @bucket = bucket
164
+ @endpoint = endpoint
165
+ @params = {:connection_timeout => 75_000}.merge(params)
166
+ @wrapper_class = params.delete(:wrapper_class) || ViewRow
167
+ unless @wrapper_class.respond_to?(:wrap)
168
+ raise ArgumentError, "wrapper class should reposond to :wrap, check the options"
169
+ end
170
+ end
171
+
172
+ # Yields each document that was fetched by view. It doesn't instantiate
173
+ # all the results because of streaming JSON parser. Returns Enumerator
174
+ # unless block given.
175
+ #
176
+ # @param [Hash] params Params for Couchdb query. Some useful are:
177
+ # :start_key, :start_key_doc_id, :descending. See {View#fetch}.
178
+ #
179
+ # @example Use each method with block
180
+ #
181
+ # view.each do |doc|
182
+ # # do something with doc
183
+ # end
184
+ #
185
+ # @example Use Enumerator version
186
+ #
187
+ # enum = view.each # request hasn't issued yet
188
+ # enum.map{|doc| doc.title.upcase}
189
+ #
190
+ # @example Pass options during view initialization
191
+ #
192
+ # endpoint = "http://localhost:5984/default/_design/blog/_view/recent"
193
+ # view = View.new(conn, endpoint, :descending => true)
194
+ # view.each do |document|
195
+ # # do something with document
196
+ # end
197
+ #
198
+ def each(params = {})
199
+ return enum_for(:each, params) unless block_given?
200
+ fetch(params) {|doc| yield(doc)}
201
+ end
202
+
203
+ def first(params = {})
204
+ params = params.merge(:limit => 1)
205
+ fetch(params).first
206
+ end
207
+
208
+ def take(n, params = {})
209
+ params = params.merge(:limit => n)
210
+ fetch(params)
211
+ end
212
+
213
+ # Registers callback function for handling error objects in view
214
+ # results stream.
215
+ #
216
+ # @yieldparam [String] from Location of the node where error occured
217
+ # @yieldparam [String] reason The reason message describing what
218
+ # happened.
219
+ #
220
+ # @example Using +#on_error+ to log all errors in view result
221
+ #
222
+ # # JSON-encoded view result
223
+ # #
224
+ # # {
225
+ # # "total_rows": 0,
226
+ # # "rows": [ ],
227
+ # # "errors": [
228
+ # # {
229
+ # # "from": "127.0.0.1:5984",
230
+ # # "reason": "Design document `_design/testfoobar` missing in database `test_db_b`."
231
+ # # },
232
+ # # {
233
+ # # "from": "http:// localhost:5984/_view_merge/",
234
+ # # "reason": "Design document `_design/testfoobar` missing in database `test_db_c`."
235
+ # # }
236
+ # # ]
237
+ # # }
238
+ #
239
+ # view.on_error do |from, reason|
240
+ # logger.warn("#{view.inspect} received the error '#{reason}' from #{from}")
241
+ # end
242
+ # docs = view.fetch
243
+ #
244
+ # @example More concise example to just count errors
245
+ #
246
+ # errcount = 0
247
+ # view.on_error{|f,r| errcount += 1}.fetch
248
+ #
249
+ def on_error(&callback)
250
+ @on_error = callback
251
+ self # enable call chains
252
+ end
253
+
254
+ # Performs query to Couchbase view. This method will stream results if block
255
+ # given or return complete result set otherwise. In latter case it defines
256
+ # method +total_rows+ returning corresponding entry from
257
+ # Couchbase result object.
258
+ #
259
+ # @note Avoid using +$+ symbol as prefix for properties in your
260
+ # documents, because server marks with it meta fields like flags and
261
+ # expiration, therefore dollar prefix is some kind of reserved. It
262
+ # won't hurt your application. Currently the {ViewRow}
263
+ # class extracts +$flags+, +$cas+ and +$expiration+ properties from
264
+ # the document and store them in {ViewRow#meta} hash.
265
+ #
266
+ # @param [Hash] params parameters for Couchbase query.
267
+ # @option params [true, false] :include_docs (false) Include the
268
+ # full content of the documents in the return. Note that the document
269
+ # is fetched from the in memory cache where it may have been changed
270
+ # or even deleted. See also +:quiet+ parameter below to control error
271
+ # reporting during fetch.
272
+ # @option params [true, false] :quiet (true) Do not raise error if
273
+ # associated document not found in the memory. If the parameter +true+
274
+ # will use +nil+ value instead.
275
+ # @option params [true, false] :descending (false) Return the documents
276
+ # in descending by key order
277
+ # @option params [String, Fixnum, Hash, Array] :key Return only
278
+ # documents that match the specified key. Will be JSON encoded.
279
+ # @option params [Array] :keys The same as +:key+, but will work for
280
+ # set of keys. Will be JSON encoded.
281
+ # @option params [String, Fixnum, Hash, Array] :startkey Return
282
+ # records starting with the specified key. +:start_key+ option should
283
+ # also work here. Will be JSON encoded.
284
+ # @option params [String] :startkey_docid Document id to start with
285
+ # (to allow pagination for duplicate startkeys). +:start_key_doc_id+
286
+ # also should work.
287
+ # @option params [String, Fixnum, Hash, Array] :endkey Stop returning
288
+ # records when the specified key is reached. +:end_key+ option should
289
+ # also work here. Will be JSON encoded.
290
+ # @option params [String] :endkey_docid Last document id to include
291
+ # in the output (to allow pagination for duplicate startkeys).
292
+ # +:end_key_doc_id+ also should work.
293
+ # @option params [true, false] :inclusive_end (true) Specifies whether
294
+ # the specified end key should be included in the result
295
+ # @option params [Fixnum] :limit Limit the number of documents in the
296
+ # output.
297
+ # @option params [Fixnum] :skip Skip this number of records before
298
+ # starting to return the results.
299
+ # @option params [String, Symbol] :on_error (:continue) Sets the
300
+ # response in the event of an error. Supported values:
301
+ # :continue:: Continue to generate view information in the event of an
302
+ # error, including the error information in the view
303
+ # response stream.
304
+ # :stop:: Stop immediately when an error condition occurs. No
305
+ # further view information will be returned.
306
+ # @option params [Fixnum] :connection_timeout (75000) Timeout before the
307
+ # view request is dropped (milliseconds)
308
+ # @option params [true, false] :reduce (true) Use the reduction function
309
+ # @option params [true, false] :group (false) Group the results using
310
+ # the reduce function to a group or single row.
311
+ # @option params [Fixnum] :group_level Specify the group level to be
312
+ # used.
313
+ # @option params [String, Symbol, false] :stale (:update_after) Allow
314
+ # the results from a stale view to be used. Supported values:
315
+ # false:: Force a view update before returning data
316
+ # :ok:: Allow stale views
317
+ # :update_after:: Allow stale view, update view after it has been
318
+ # accessed
319
+ # @option params [Hash] :body Accepts the same parameters, except
320
+ # +:body+ of course, but sends them in POST body instead of query
321
+ # string. It could be useful for really large and complex parameters.
322
+ #
323
+ # @yieldparam [Couchbase::ViewRow] document
324
+ #
325
+ # @return [Array] with documents. There will be +total_entries+
326
+ # method defined on this array if it's possible.
327
+ #
328
+ # @raise [Couchbase::Error::View] when +on_error+ callback is nil and
329
+ # error object found in the result stream.
330
+ #
331
+ # @example Query +recent_posts+ view with key filter
332
+ # doc.recent_posts(:body => {:keys => ["key1", "key2"]})
333
+ #
334
+ # @example Fetch second page of result set (splitted in 10 items per page)
335
+ # page = 2
336
+ # per_page = 10
337
+ # doc.recent_posts(:skip => (page - 1) * per_page, :limit => per_page)
338
+ #
339
+ # @example Simple join using Map/Reduce
340
+ # # Given the bucket with Posts(:id, :type, :title, :body) and
341
+ # # Comments(:id, :type, :post_id, :author, :body). The map function
342
+ # # below (in javascript) will build the View index called
343
+ # # "recent_posts_with_comments" which will behave like left inner join.
344
+ # #
345
+ # # function(doc) {
346
+ # # switch (doc.type) {
347
+ # # case "Post":
348
+ # # emit([doc.id, 0], null);
349
+ # # break;
350
+ # # case "Comment":
351
+ # # emit([doc.post_id, 1], null);
352
+ # # break;
353
+ # # }
354
+ # # }
355
+ # #
356
+ # post_id = 42
357
+ # doc.recent_posts_with_comments(:start_key => [post_id, 0],
358
+ # :end_key => [post_id, 1],
359
+ # :include_docs => true)
360
+ def fetch(params = {}, &block)
361
+ params = @params.merge(params)
362
+ include_docs = params.delete(:include_docs)
363
+ quiet = params.delete(:quiet){ true }
364
+
365
+ options = {:chunked => true, :extended => true, :type => :view}
366
+ if body = params.delete(:body)
367
+ body = MultiJson.dump(body) unless body.is_a?(String)
368
+ options.update(:body => body, :method => params.delete(:method) || :post)
369
+ end
370
+ path = Utils.build_query(@endpoint, params)
371
+ request = @bucket.make_http_request(path, options)
372
+
373
+ if @bucket.async?
374
+ if block
375
+ fetch_async(request, include_docs, quiet, block)
376
+ end
377
+ else
378
+ fetch_sync(request, include_docs, quiet, block)
379
+ end
380
+ end
381
+
382
+ # Method for fetching asynchronously all rows and passing array to callback
383
+ #
384
+ # Parameters are same as for {View#fetch} method, but callback is called for whole set for
385
+ # rows instead of one by each.
386
+ #
387
+ # @example
388
+ # con.run do
389
+ # doc.recent_posts.fetch_all do |posts|
390
+ # do_something_with_all_posts(posts)
391
+ # end
392
+ # end
393
+ def fetch_all(params = {}, &block)
394
+ return fetch(params) unless @bucket.async?
395
+ raise ArgumentError, "Block needed for fetch_all in async mode" unless block
396
+
397
+ all = []
398
+ fetch(params) do |row|
399
+ all << row
400
+ if row.last?
401
+ @bucket.create_timer(0) { block.call(all) }
402
+ end
403
+ end
404
+ end
405
+
406
+
407
+ # Returns a string containing a human-readable representation of the {View}
408
+ #
409
+ # @return [String]
410
+ def inspect
411
+ %(#<#{self.class.name}:#{self.object_id} @endpoint=#{@endpoint.inspect} @params=#{@params.inspect}>)
412
+ end
413
+
414
+ private
415
+
416
+ def send_error(*args)
417
+ if @on_error
418
+ @on_error.call(*args.take(2))
419
+ else
420
+ raise Error::View.new(*args)
421
+ end
422
+ end
423
+
424
+ def fetch_async(request, include_docs, quiet, block)
425
+ filter = ["/rows/", "/errors/"]
426
+ parser = YAJI::Parser.new(:filter => filter, :with_path => true)
427
+ helper = AsyncHelper.new(@wrapper_class, @bucket, include_docs, quiet, block)
428
+
429
+ request.on_body do |chunk|
430
+ if chunk.success?
431
+ parser << chunk.value if chunk.value
432
+ helper.complete! if chunk.completed?
433
+ else
434
+ send_error("http_error", chunk.error)
435
+ end
436
+ end
437
+
438
+ parser.on_object do |path, obj|
439
+ case path
440
+ when "/errors/"
441
+ from, reason = obj["from"], obj["reason"]
442
+ send_error(from, reason)
443
+ else
444
+ helper.push(obj)
445
+ end
446
+ end
447
+
448
+ request.perform
449
+ nil
450
+ end
451
+
452
+ def fetch_sync(request, include_docs, quiet, block)
453
+ res = []
454
+ filter = ["/rows/", "/errors/"]
455
+ unless block
456
+ filter << "/total_rows"
457
+ docs = ArrayWithTotalRows.new
458
+ end
459
+ parser = YAJI::Parser.new(:filter => filter, :with_path => true)
460
+ last_chunk = nil
461
+
462
+ request.on_body do |chunk|
463
+ last_chunk = chunk
464
+ res << chunk.value if chunk.success?
465
+ end
466
+
467
+ parser.on_object do |path, obj|
468
+ case path
469
+ when "/total_rows"
470
+ # if total_rows key present, save it and take next object
471
+ docs.total_rows = obj
472
+ when "/errors/"
473
+ from, reason = obj["from"], obj["reason"]
474
+ send_error(from, reason)
475
+ else
476
+ if include_docs
477
+ val, flags, cas = @bucket.get(obj[S_ID], :extended => true, :quiet => quiet)
478
+ obj[S_DOC] = {
479
+ S_VALUE => val,
480
+ S_META => {
481
+ S_ID => obj[S_ID],
482
+ S_FLAGS => flags,
483
+ S_CAS => cas
484
+ }
485
+ }
486
+ end
487
+ doc = @wrapper_class.wrap(@bucket, obj)
488
+ block ? block.call(doc) : docs << doc
489
+ end
490
+ end
491
+
492
+ request.continue
493
+
494
+ if last_chunk.success?
495
+ while value = res.shift
496
+ parser << value
497
+ end
498
+ else
499
+ send_error("http_error", last_chunk.error, nil)
500
+ end
501
+
502
+ # return nil for call with block
503
+ docs
504
+ end
505
+ end
506
+ end