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.
- checksums.yaml +7 -0
- data/.gitignore +15 -0
- data/.travis.yml +22 -0
- data/.yardopts +5 -0
- data/CONTRIBUTING.markdown +75 -0
- data/Gemfile +4 -0
- data/LICENSE +201 -0
- data/Makefile +3 -0
- data/README.markdown +649 -0
- data/RELEASE_NOTES.markdown +796 -0
- data/Rakefile +20 -0
- data/couchbase.gemspec +49 -0
- data/examples/chat-em/Gemfile +7 -0
- data/examples/chat-em/README.markdown +45 -0
- data/examples/chat-em/server.rb +82 -0
- data/examples/chat-goliath-grape/Gemfile +5 -0
- data/examples/chat-goliath-grape/README.markdown +50 -0
- data/examples/chat-goliath-grape/app.rb +67 -0
- data/examples/chat-goliath-grape/config/app.rb +20 -0
- data/examples/transcoders/Gemfile +3 -0
- data/examples/transcoders/README.markdown +59 -0
- data/examples/transcoders/cb-zcat +40 -0
- data/examples/transcoders/cb-zcp +45 -0
- data/examples/transcoders/gzip_transcoder.rb +49 -0
- data/examples/transcoders/options.rb +54 -0
- data/ext/couchbase_ext/.gitignore +4 -0
- data/ext/couchbase_ext/arguments.c +956 -0
- data/ext/couchbase_ext/arithmetic.c +307 -0
- data/ext/couchbase_ext/bucket.c +1370 -0
- data/ext/couchbase_ext/context.c +65 -0
- data/ext/couchbase_ext/couchbase_ext.c +1364 -0
- data/ext/couchbase_ext/couchbase_ext.h +644 -0
- data/ext/couchbase_ext/delete.c +163 -0
- data/ext/couchbase_ext/eventmachine_plugin.c +452 -0
- data/ext/couchbase_ext/extconf.rb +168 -0
- data/ext/couchbase_ext/get.c +316 -0
- data/ext/couchbase_ext/gethrtime.c +129 -0
- data/ext/couchbase_ext/http.c +432 -0
- data/ext/couchbase_ext/multithread_plugin.c +1090 -0
- data/ext/couchbase_ext/observe.c +171 -0
- data/ext/couchbase_ext/plugin_common.c +171 -0
- data/ext/couchbase_ext/result.c +129 -0
- data/ext/couchbase_ext/stats.c +163 -0
- data/ext/couchbase_ext/store.c +542 -0
- data/ext/couchbase_ext/timer.c +192 -0
- data/ext/couchbase_ext/touch.c +186 -0
- data/ext/couchbase_ext/unlock.c +176 -0
- data/ext/couchbase_ext/utils.c +551 -0
- data/ext/couchbase_ext/version.c +142 -0
- data/lib/action_dispatch/middleware/session/couchbase_store.rb +38 -0
- data/lib/active_support/cache/couchbase_store.rb +430 -0
- data/lib/couchbase.rb +155 -0
- data/lib/couchbase/bucket.rb +457 -0
- data/lib/couchbase/cluster.rb +119 -0
- data/lib/couchbase/connection_pool.rb +58 -0
- data/lib/couchbase/constants.rb +12 -0
- data/lib/couchbase/result.rb +26 -0
- data/lib/couchbase/transcoder.rb +120 -0
- data/lib/couchbase/utils.rb +62 -0
- data/lib/couchbase/version.rb +21 -0
- data/lib/couchbase/view.rb +506 -0
- data/lib/couchbase/view_row.rb +272 -0
- data/lib/ext/multi_json_fix.rb +56 -0
- data/lib/rack/session/couchbase.rb +108 -0
- data/tasks/benchmark.rake +6 -0
- data/tasks/compile.rake +158 -0
- data/tasks/test.rake +100 -0
- data/tasks/util.rake +21 -0
- data/test/profile/.gitignore +1 -0
- data/test/profile/Gemfile +6 -0
- data/test/profile/benchmark.rb +195 -0
- data/test/setup.rb +178 -0
- data/test/test_arithmetic.rb +185 -0
- data/test/test_async.rb +316 -0
- data/test/test_bucket.rb +250 -0
- data/test/test_cas.rb +235 -0
- data/test/test_couchbase.rb +77 -0
- data/test/test_couchbase_connection_pool.rb +77 -0
- data/test/test_couchbase_rails_cache_store.rb +361 -0
- data/test/test_delete.rb +120 -0
- data/test/test_errors.rb +82 -0
- data/test/test_eventmachine.rb +70 -0
- data/test/test_format.rb +164 -0
- data/test/test_get.rb +407 -0
- data/test/test_stats.rb +57 -0
- data/test/test_store.rb +216 -0
- data/test/test_timer.rb +42 -0
- data/test/test_touch.rb +97 -0
- data/test/test_unlock.rb +119 -0
- data/test/test_utils.rb +58 -0
- data/test/test_version.rb +52 -0
- 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
|