couchbase 1.3.4-x64-mingw32
Sign up to get free protection for your applications and to get access to all the features.
- 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
|