couchbase 1.3.4-x64-mingw32

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +15 -0
  3. data/.travis.yml +22 -0
  4. data/.yardopts +5 -0
  5. data/CONTRIBUTING.markdown +75 -0
  6. data/Gemfile +4 -0
  7. data/LICENSE +201 -0
  8. data/Makefile +3 -0
  9. data/README.markdown +649 -0
  10. data/RELEASE_NOTES.markdown +796 -0
  11. data/Rakefile +20 -0
  12. data/couchbase.gemspec +49 -0
  13. data/examples/chat-em/Gemfile +7 -0
  14. data/examples/chat-em/README.markdown +45 -0
  15. data/examples/chat-em/server.rb +82 -0
  16. data/examples/chat-goliath-grape/Gemfile +5 -0
  17. data/examples/chat-goliath-grape/README.markdown +50 -0
  18. data/examples/chat-goliath-grape/app.rb +67 -0
  19. data/examples/chat-goliath-grape/config/app.rb +20 -0
  20. data/examples/transcoders/Gemfile +3 -0
  21. data/examples/transcoders/README.markdown +59 -0
  22. data/examples/transcoders/cb-zcat +40 -0
  23. data/examples/transcoders/cb-zcp +45 -0
  24. data/examples/transcoders/gzip_transcoder.rb +49 -0
  25. data/examples/transcoders/options.rb +54 -0
  26. data/ext/couchbase_ext/.gitignore +4 -0
  27. data/ext/couchbase_ext/arguments.c +956 -0
  28. data/ext/couchbase_ext/arithmetic.c +307 -0
  29. data/ext/couchbase_ext/bucket.c +1370 -0
  30. data/ext/couchbase_ext/context.c +65 -0
  31. data/ext/couchbase_ext/couchbase_ext.c +1364 -0
  32. data/ext/couchbase_ext/couchbase_ext.h +644 -0
  33. data/ext/couchbase_ext/delete.c +163 -0
  34. data/ext/couchbase_ext/eventmachine_plugin.c +452 -0
  35. data/ext/couchbase_ext/extconf.rb +168 -0
  36. data/ext/couchbase_ext/get.c +316 -0
  37. data/ext/couchbase_ext/gethrtime.c +129 -0
  38. data/ext/couchbase_ext/http.c +432 -0
  39. data/ext/couchbase_ext/multithread_plugin.c +1090 -0
  40. data/ext/couchbase_ext/observe.c +171 -0
  41. data/ext/couchbase_ext/plugin_common.c +171 -0
  42. data/ext/couchbase_ext/result.c +129 -0
  43. data/ext/couchbase_ext/stats.c +163 -0
  44. data/ext/couchbase_ext/store.c +542 -0
  45. data/ext/couchbase_ext/timer.c +192 -0
  46. data/ext/couchbase_ext/touch.c +186 -0
  47. data/ext/couchbase_ext/unlock.c +176 -0
  48. data/ext/couchbase_ext/utils.c +551 -0
  49. data/ext/couchbase_ext/version.c +142 -0
  50. data/lib/action_dispatch/middleware/session/couchbase_store.rb +38 -0
  51. data/lib/active_support/cache/couchbase_store.rb +430 -0
  52. data/lib/couchbase.rb +155 -0
  53. data/lib/couchbase/bucket.rb +457 -0
  54. data/lib/couchbase/cluster.rb +119 -0
  55. data/lib/couchbase/connection_pool.rb +58 -0
  56. data/lib/couchbase/constants.rb +12 -0
  57. data/lib/couchbase/result.rb +26 -0
  58. data/lib/couchbase/transcoder.rb +120 -0
  59. data/lib/couchbase/utils.rb +62 -0
  60. data/lib/couchbase/version.rb +21 -0
  61. data/lib/couchbase/view.rb +506 -0
  62. data/lib/couchbase/view_row.rb +272 -0
  63. data/lib/ext/multi_json_fix.rb +56 -0
  64. data/lib/rack/session/couchbase.rb +108 -0
  65. data/tasks/benchmark.rake +6 -0
  66. data/tasks/compile.rake +158 -0
  67. data/tasks/test.rake +100 -0
  68. data/tasks/util.rake +21 -0
  69. data/test/profile/.gitignore +1 -0
  70. data/test/profile/Gemfile +6 -0
  71. data/test/profile/benchmark.rb +195 -0
  72. data/test/setup.rb +178 -0
  73. data/test/test_arithmetic.rb +185 -0
  74. data/test/test_async.rb +316 -0
  75. data/test/test_bucket.rb +250 -0
  76. data/test/test_cas.rb +235 -0
  77. data/test/test_couchbase.rb +77 -0
  78. data/test/test_couchbase_connection_pool.rb +77 -0
  79. data/test/test_couchbase_rails_cache_store.rb +361 -0
  80. data/test/test_delete.rb +120 -0
  81. data/test/test_errors.rb +82 -0
  82. data/test/test_eventmachine.rb +70 -0
  83. data/test/test_format.rb +164 -0
  84. data/test/test_get.rb +407 -0
  85. data/test/test_stats.rb +57 -0
  86. data/test/test_store.rb +216 -0
  87. data/test/test_timer.rb +42 -0
  88. data/test/test_touch.rb +97 -0
  89. data/test/test_unlock.rb +119 -0
  90. data/test/test_utils.rb +58 -0
  91. data/test/test_version.rb +52 -0
  92. metadata +336 -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.4"
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