dm-couchdb-adapter 0.10.2
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.
- data/Gemfile +1 -0
- data/History.txt +33 -0
- data/LICENSE +20 -0
- data/Manifest.txt +21 -0
- data/README.rdoc +68 -0
- data/Rakefile +65 -0
- data/TODO +0 -0
- data/VERSION +1 -0
- data/lib/couchdb_adapter/adapter.rb +165 -0
- data/lib/couchdb_adapter/attachments.rb +121 -0
- data/lib/couchdb_adapter/collection.rb +7 -0
- data/lib/couchdb_adapter/conditions.rb +190 -0
- data/lib/couchdb_adapter/couch_resource.rb +45 -0
- data/lib/couchdb_adapter/design.rb +5 -0
- data/lib/couchdb_adapter/json_object.rb +23 -0
- data/lib/couchdb_adapter/migrations.rb +25 -0
- data/lib/couchdb_adapter/model.rb +9 -0
- data/lib/couchdb_adapter/query.rb +7 -0
- data/lib/couchdb_adapter/resource.rb +19 -0
- data/lib/couchdb_adapter/version.rb +5 -0
- data/lib/couchdb_adapter/view.rb +41 -0
- data/lib/couchdb_adapter.rb +31 -0
- data/spec/integration/couchdb_adapter_spec.rb +291 -0
- data/spec/integration/couchdb_attachments_spec.rb +116 -0
- data/spec/integration/couchdb_view_spec.rb +47 -0
- data/spec/integration_spec.rb +76 -0
- data/spec/shared/adapter_shared_spec.rb +310 -0
- data/spec/spec.opts +2 -0
- data/spec/spec_helper.rb +22 -0
- data/spec/testfile.txt +1 -0
- data/spec/unit/couch_db_adapter_spec.rb +8 -0
- data/tasks/install.rb +13 -0
- data/tasks/spec.rb +25 -0
- metadata +139 -0
@@ -0,0 +1,190 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Couch
|
3
|
+
module Conditions
|
4
|
+
include DataMapper::Query::Conditions
|
5
|
+
|
6
|
+
# TODO: document
|
7
|
+
# @api semipublic
|
8
|
+
def property_to_column_name(property, qualify)
|
9
|
+
property.field
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
# Constructs couchdb if statement
|
14
|
+
#
|
15
|
+
# @return [String]
|
16
|
+
# where clause
|
17
|
+
#
|
18
|
+
# @api private
|
19
|
+
def conditions_statement(conditions, qualify = false)
|
20
|
+
case conditions
|
21
|
+
when Query::Conditions::NotOperation
|
22
|
+
negate_operation(conditions, qualify)
|
23
|
+
|
24
|
+
when Query::Conditions::AbstractOperation
|
25
|
+
# TODO: remove this once conditions can be compressed
|
26
|
+
if conditions.operands.size == 1
|
27
|
+
# factor out operations with a single operand
|
28
|
+
conditions_statement(conditions.operands.first, qualify)
|
29
|
+
else
|
30
|
+
operation_statement(conditions, qualify)
|
31
|
+
end
|
32
|
+
|
33
|
+
when Query::Conditions::AbstractComparison
|
34
|
+
comparison_statement(conditions, qualify)
|
35
|
+
|
36
|
+
when Array
|
37
|
+
statement, bind_values = conditions # handle raw conditions
|
38
|
+
[ "(#{statement})", bind_values ]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# TODO: document
|
43
|
+
# @api private
|
44
|
+
def negate_operation(operation, qualify)
|
45
|
+
@negated = !@negated
|
46
|
+
begin
|
47
|
+
conditions_statement(operation.operands.first, qualify)
|
48
|
+
ensure
|
49
|
+
@negated = !@negated
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def query_string(query)
|
54
|
+
query_string = []
|
55
|
+
if query.view_options
|
56
|
+
query_string +=
|
57
|
+
query.view_options.map do |key, value|
|
58
|
+
if [:endkey, :key, :startkey].include? key
|
59
|
+
URI.escape(%Q(#{key}=#{value.to_json}))
|
60
|
+
else
|
61
|
+
URI.escape("#{key}=#{value}")
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
query_string << "limit=#{query.limit}" if query.limit
|
66
|
+
query_string << "descending=#{query.add_reversed?}" if query.add_reversed?
|
67
|
+
query_string << "skip=#{query.offset}" if query.offset != 0
|
68
|
+
query_string.empty? ? nil : "?#{query_string.join('&')}"
|
69
|
+
end
|
70
|
+
|
71
|
+
|
72
|
+
# TODO: document
|
73
|
+
# @api private
|
74
|
+
def operation_statement(operation, qualify)
|
75
|
+
statements = []
|
76
|
+
bind_values = []
|
77
|
+
|
78
|
+
operation.each do |operand|
|
79
|
+
statement, values = conditions_statement(operand, qualify)
|
80
|
+
|
81
|
+
if operand.respond_to?(:operands) && operand.operands.size > 1
|
82
|
+
statement = "(#{statement})"
|
83
|
+
end
|
84
|
+
|
85
|
+
statements << statement
|
86
|
+
bind_values.concat(values)
|
87
|
+
end
|
88
|
+
|
89
|
+
join_with = operation.kind_of?(@negated ? Query::Conditions::OrOperation : Query::Conditions::AndOperation) ? '&&' : '||'
|
90
|
+
statement = statements.join(" #{join_with} ")
|
91
|
+
|
92
|
+
return statement, bind_values
|
93
|
+
end
|
94
|
+
|
95
|
+
# Constructs comparison clause
|
96
|
+
#
|
97
|
+
# @return [String]
|
98
|
+
# comparison clause
|
99
|
+
#
|
100
|
+
# @api private
|
101
|
+
def comparison_statement(comparison, qualify)
|
102
|
+
value = comparison.value.to_json.gsub("\"", "'")
|
103
|
+
|
104
|
+
# TODO: move exclusive Range handling into another method, and
|
105
|
+
# update conditions_statement to use it
|
106
|
+
|
107
|
+
# break exclusive Range queries up into two comparisons ANDed together
|
108
|
+
if value.kind_of?(Range) && value.exclude_end?
|
109
|
+
operation = Query::Conditions::Operation.new(:and,
|
110
|
+
Query::Conditions::Comparison.new(:gte, comparison.subject, value.first),
|
111
|
+
Query::Conditions::Comparison.new(:lt, comparison.subject, value.last)
|
112
|
+
)
|
113
|
+
|
114
|
+
statement, bind_values = conditions_statement(operation, qualify)
|
115
|
+
|
116
|
+
return "(#{statement})", bind_values
|
117
|
+
elsif comparison.relationship?
|
118
|
+
return conditions_statement(comparison.foreign_key_mapping, qualify)
|
119
|
+
end
|
120
|
+
|
121
|
+
operator = case comparison
|
122
|
+
when Query::Conditions::EqualToComparison then @negated ? '!=' : '=='
|
123
|
+
when Query::Conditions::InclusionComparison then @negated ? exclude_operator(comparison.subject, value) : include_operator(comparison.subject, value)
|
124
|
+
when Query::Conditions::RegexpComparison then @negated ? not_regexp_operator(value) : regexp_operator(value)
|
125
|
+
when Query::Conditions::LikeComparison then @negated ? unlike_operator(value) : like_operator(value)
|
126
|
+
when Query::Conditions::GreaterThanComparison then @negated ? ' <= ' : ' > '
|
127
|
+
when Query::Conditions::LessThanComparison then @negated ? ' >= ' : ' < '
|
128
|
+
when Query::Conditions::GreaterThanOrEqualToComparison then @negated ? ' < ' : ' >= '
|
129
|
+
when Query::Conditions::LessThanOrEqualToComparison then @negated ? ' > ' : ' <= '
|
130
|
+
end
|
131
|
+
|
132
|
+
# if operator return value contains ? then it means that it is function call
|
133
|
+
# and it contains placeholder (%s) for property name as well (used in Oracle adapter for regexp operator)
|
134
|
+
if operator.include?('?')
|
135
|
+
return operator % property_to_column_name(comparison.subject, qualify), [ value ]
|
136
|
+
else
|
137
|
+
return "doc.#{property_to_column_name(comparison.subject, qualify)}#{operator}#{value}".strip, [value].compact
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
# TODO: document
|
142
|
+
# @api private
|
143
|
+
def include_operator(property, operand)
|
144
|
+
case operand
|
145
|
+
when Array then 'IN'
|
146
|
+
when Range then 'BETWEEN'
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
# TODO: document
|
151
|
+
# @api private
|
152
|
+
def exclude_operator(property, operand)
|
153
|
+
"NOT #{include_operator(property, operand)}"
|
154
|
+
end
|
155
|
+
|
156
|
+
# TODO: document
|
157
|
+
# @api private
|
158
|
+
def regexp_operator(operand)
|
159
|
+
'~'
|
160
|
+
end
|
161
|
+
|
162
|
+
# TODO: document
|
163
|
+
# @api private
|
164
|
+
def not_regexp_operator(operand)
|
165
|
+
'!~'
|
166
|
+
end
|
167
|
+
|
168
|
+
# TODO: document
|
169
|
+
# @api private
|
170
|
+
def like_operator(value)
|
171
|
+
case value
|
172
|
+
when Regexp then value = value.source
|
173
|
+
when String
|
174
|
+
# We'll go ahead and transform this string for SQL compatability
|
175
|
+
value = "^#{value}" unless value[0..0] == ("%")
|
176
|
+
value = "#{value}$" unless value[-1..-1] == ("%")
|
177
|
+
value.gsub!("%", ".*")
|
178
|
+
value.gsub!("_", ".")
|
179
|
+
end
|
180
|
+
return "match(/#{value}/)"
|
181
|
+
end
|
182
|
+
|
183
|
+
# TODO: document
|
184
|
+
# @api private
|
185
|
+
def unlike_operator(value)
|
186
|
+
# how the hell...
|
187
|
+
end
|
188
|
+
end # end Collection
|
189
|
+
end # end Couch
|
190
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Couch
|
3
|
+
module Resource
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.send(:include, DataMapper::Resource)
|
7
|
+
mod.class_eval do
|
8
|
+
# include DataMapper::CouchResource::Attachments
|
9
|
+
|
10
|
+
property :id, String, :key => true, :field => '_id', :nullable => true
|
11
|
+
property :attachments, DataMapper::Types::JsonObject, :field => '_attachments'
|
12
|
+
property :rev, String, :field => '_rev'
|
13
|
+
property :couchdb_type, DataMapper::Types::Discriminator
|
14
|
+
|
15
|
+
class << self
|
16
|
+
|
17
|
+
def couchdb_types
|
18
|
+
[self.base_model] | self.descendants
|
19
|
+
end
|
20
|
+
|
21
|
+
def couchdb_types_condition
|
22
|
+
couchdb_types.collect {|type| "doc.couchdb_type == '#{type}'"}.join(' || ')
|
23
|
+
end
|
24
|
+
|
25
|
+
def view(name, &block)
|
26
|
+
@views ||= Hash.new { |h,k| h[k] = {} }
|
27
|
+
view = View.new(self, name)
|
28
|
+
@views[repository.name][name] = block_given? ? block : lambda {}
|
29
|
+
view
|
30
|
+
end
|
31
|
+
|
32
|
+
def views(repository_name = default_repository_name)
|
33
|
+
@views ||= Hash.new { |h,k| h[k] = {} }
|
34
|
+
views = @views[repository_name].dup
|
35
|
+
views.each_pair {|key, value| views[key] = value.call}
|
36
|
+
end
|
37
|
+
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
end # end Resource
|
44
|
+
end
|
45
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
# Non-lazy objects that serialize to/from JSON, for use with couchdb
|
4
|
+
module DataMapper
|
5
|
+
module Types
|
6
|
+
class JsonObject < DataMapper::Type
|
7
|
+
primitive String
|
8
|
+
size 65535
|
9
|
+
|
10
|
+
def self.load(value, property)
|
11
|
+
value.nil? ? nil : value
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.dump(value, property)
|
15
|
+
value.nil? ? nil : value
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.typecast(value, property)
|
19
|
+
value
|
20
|
+
end
|
21
|
+
end # class JsonObject
|
22
|
+
end # module Types
|
23
|
+
end # module DataMapper
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Migrations
|
3
|
+
module CouchAdapter
|
4
|
+
def create_model_storage(repository, model)
|
5
|
+
uri = "/#{self.escaped_db_name}/_design/#{model.base_model.to_s}"
|
6
|
+
view = Net::HTTP::Put.new(uri)
|
7
|
+
view['content-type'] = "application/json"
|
8
|
+
views = model.views.reject {|key, value| value.nil?}
|
9
|
+
view.body = { :views => views }.to_json
|
10
|
+
request do |http|
|
11
|
+
http.request(view)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def destroy_model_storage(repository, model)
|
16
|
+
uri = "/#{self.escaped_db_name}/_design/#{model.base_model.to_s}"
|
17
|
+
response = http_get(uri)
|
18
|
+
unless response['error']
|
19
|
+
uri += "?rev=#{response["_rev"]}"
|
20
|
+
http_delete(uri)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
module DataMapper
|
2
|
+
module Resource
|
3
|
+
# Converts a Resource to a JSON representation.
|
4
|
+
def to_couch_json(dirty = false)
|
5
|
+
property_list = self.class.properties.select { |key, value| dirty ? self.dirty_attributes.key?(key) : true }
|
6
|
+
data = {}
|
7
|
+
for property in property_list do
|
8
|
+
data[property.field] =
|
9
|
+
if property.type.respond_to?(:dump)
|
10
|
+
property.type.dump(property.get!(self), property)
|
11
|
+
else
|
12
|
+
property.get!(self)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
data.delete('_attachments') if data['_attachments'].nil? || data['_attachments'].empty?
|
16
|
+
data.to_json
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
module DataMapper
|
2
|
+
class Query
|
3
|
+
attr_accessor :view, :view_options
|
4
|
+
end
|
5
|
+
end
|
6
|
+
|
7
|
+
module DataMapper
|
8
|
+
module CouchResource
|
9
|
+
class View
|
10
|
+
attr_reader :model, :name
|
11
|
+
|
12
|
+
def initialize(model, name)
|
13
|
+
@model = model
|
14
|
+
@name = name
|
15
|
+
|
16
|
+
create_getter
|
17
|
+
end
|
18
|
+
|
19
|
+
def create_getter
|
20
|
+
@model.class_eval <<-RUBY, __FILE__, __LINE__ + 1
|
21
|
+
def self.#{@name}(*args)
|
22
|
+
options = {}
|
23
|
+
if args.size == 1 && !args.first.is_a?(Hash)
|
24
|
+
options[:key] = args.shift
|
25
|
+
else
|
26
|
+
options = args.pop
|
27
|
+
end
|
28
|
+
query = Query.new(repository, self)
|
29
|
+
query.view_options = options || {}
|
30
|
+
query.view = '#{@name}'
|
31
|
+
if options.is_a?(Hash) && options.has_key?(:repository)
|
32
|
+
repository(options.delete(:repository)).read_many(query)
|
33
|
+
else
|
34
|
+
repository.read_many(query)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
RUBY
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'pathname'
|
3
|
+
|
4
|
+
|
5
|
+
gem "couchrest", "~> 0.35"
|
6
|
+
require "couchrest"
|
7
|
+
|
8
|
+
# load after because couchrest clones some methods in Extlib,
|
9
|
+
# I'd rather just have the ones in the current Extlib
|
10
|
+
gem 'dm-core', '~>0.10.2'
|
11
|
+
require 'dm-core'
|
12
|
+
|
13
|
+
begin
|
14
|
+
gem "json"
|
15
|
+
require "json/ext"
|
16
|
+
rescue LoadError
|
17
|
+
gem "json_pure"
|
18
|
+
require "json/pure"
|
19
|
+
end
|
20
|
+
|
21
|
+
|
22
|
+
dir = Pathname(__FILE__).dirname.expand_path / 'couchdb_adapter'
|
23
|
+
|
24
|
+
require dir / 'attachments'
|
25
|
+
require dir / 'couch_resource'
|
26
|
+
require dir / 'json_object'
|
27
|
+
require dir / 'view'
|
28
|
+
require dir / 'version'
|
29
|
+
require dir / 'resource'
|
30
|
+
require dir / 'adapter'
|
31
|
+
require dir / 'migrations'
|