ashikawa-core 0.4.1 → 0.5.1

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.
@@ -29,16 +29,19 @@ module Ashikawa
29
29
  # @api public
30
30
  def initialize(database, raw_document)
31
31
  @database = database
32
- @is_persistent = raw_document.has_key?('_id') and raw_document.has_key?('rev')
33
-
34
- if @is_persistent
35
- @collection_id, @id = raw_document['_id'].split('/').map { |id| id.to_i }
36
- @revision = raw_document['_rev'].to_i
37
- end
38
-
32
+ @collection_id, @id = raw_document['_id'].split('/').map { |id| id.to_i } unless raw_document['_id'].nil?
33
+ @revision = raw_document['_rev'].to_i unless raw_document['_rev'].nil?
39
34
  @content = raw_document.delete_if { |key, value| key[0] == "_" }
40
35
  end
41
36
 
37
+ # Raises an exception if the document is not persisted
38
+ #
39
+ # @raise [DocumentNotFoundException]
40
+ # @api semi-public
41
+ def check_if_persisted!
42
+ raise DocumentNotFoundException if @id.nil?
43
+ end
44
+
42
45
  # Get the value of an attribute of the document
43
46
  #
44
47
  # @param [String] attribute_name
@@ -52,7 +55,7 @@ module Ashikawa
52
55
  #
53
56
  # @api public
54
57
  def delete
55
- raise DocumentNotFoundException unless @is_persistent
58
+ check_if_persisted!
56
59
  @database.send_request "document/#{@collection_id}/#{@id}", delete: {}
57
60
  end
58
61
 
@@ -62,7 +65,7 @@ module Ashikawa
62
65
  # @param [Object] value
63
66
  # @api public
64
67
  def []=(attribute_name, value)
65
- raise DocumentNotFoundException unless @is_persistent
68
+ check_if_persisted!
66
69
  @content[attribute_name] = value
67
70
  end
68
71
 
@@ -78,7 +81,7 @@ module Ashikawa
78
81
  #
79
82
  # @api public
80
83
  def save()
81
- raise DocumentNotFoundException unless @is_persistent
84
+ check_if_persisted!
82
85
  @database.send_request "document/#{@collection_id}/#{@id}", put: @content
83
86
  end
84
87
  end
@@ -1,8 +1,11 @@
1
1
  module Ashikawa
2
2
  module Core
3
- # This Exception is thrown, when a document was requested from
3
+ # This Exception is thrown when a document was requested from
4
4
  # the server that does not exist.
5
5
  class DocumentNotFoundException < RuntimeError
6
+ def to_s
7
+ "You requested a document from the server that does not exist"
8
+ end
6
9
  end
7
10
  end
8
11
  end
@@ -0,0 +1,11 @@
1
+ module Ashikawa
2
+ module Core
3
+ # This Exception is thrown when a Query object should execute a simple query
4
+ # but no collection was provided upon creation
5
+ class NoCollectionProvidedException < RuntimeError
6
+ def to_s
7
+ "A simple query can't be executed by a Query object without a collection"
8
+ end
9
+ end
10
+ end
11
+ end
@@ -25,15 +25,15 @@ module Ashikawa
25
25
  # Create a new Index
26
26
  #
27
27
  # @param [Collection] collection The collection the index is defined on
28
- # @param [Hash] raw_data The JSON representation of the index
28
+ # @param [Hash] raw_index The JSON representation of the index
29
29
  # @return [Index]
30
30
  # @api
31
- def initialize(collection, raw_data)
31
+ def initialize(collection, raw_index)
32
32
  @collection = collection
33
- @id = raw_data["id"].split("/")[1].to_i if raw_data["id"]
34
- @on = raw_data["fields"].map { |field| field.to_sym } if raw_data.has_key? "fields"
35
- @type = raw_data["type"].to_sym if raw_data.has_key? "type"
36
- @unique = raw_data["unique"] if raw_data.has_key? "unique"
33
+ @id = raw_index["id"].split("/")[1].to_i if raw_index.has_key? "id"
34
+ @on = raw_index["fields"].map { |field| field.to_sym } if raw_index.has_key? "fields"
35
+ @type = raw_index["type"].to_sym if raw_index.has_key? "type"
36
+ @unique = raw_index["unique"] if raw_index.has_key? "unique"
37
37
  end
38
38
 
39
39
  # Remove the index from the collection
@@ -0,0 +1,251 @@
1
+ require 'ashikawa-core/cursor'
2
+ require 'ashikawa-core/document'
3
+ require 'ashikawa-core/exceptions/no_collection_provided'
4
+ require 'forwardable'
5
+
6
+ module Ashikawa
7
+ module Core
8
+ # Formulate a Query on a collection or on a database
9
+ class Query
10
+ extend Forwardable
11
+
12
+ # Delegate sending requests to the connection
13
+ delegate send_request: :@connection
14
+
15
+ # Initializes a Query
16
+ #
17
+ # @param [Collection, Database] connection
18
+ # @return [Query]
19
+ def initialize(connection)
20
+ @connection = connection
21
+ end
22
+
23
+ # Retrieves all documents for a collection
24
+ #
25
+ # @note It is advised to NOT use this method due to possible HUGE data amounts requested
26
+ # @option options [Integer] :limit limit the maximum number of queried and returned elements.
27
+ # @option options [Integer] :skip skip the first <n> documents of the query.
28
+ # @return [Cursor]
29
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
30
+ # @api public
31
+ # @example Get an array with all documents
32
+ # query = Ashikawa::Core::Query.new collection
33
+ # query.all # => #<Cursor id=33>
34
+ def all(options={})
35
+ simple_query_request "/simple/all",
36
+ options,
37
+ [:limit, :skip]
38
+ end
39
+
40
+ # Looks for documents in a collection which match the given criteria
41
+ #
42
+ # @option example [Hash] a Hash with data matching the documents you are looking for.
43
+ # @option options [Hash] a Hash with additional settings for the query.
44
+ # @option options [Integer] :limit limit the maximum number of queried and returned elements.
45
+ # @option options [Integer] :skip skip the first <n> documents of the query.
46
+ # @return [Cursor]
47
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
48
+ # @api public
49
+ # @example Find all documents in a collection that are red
50
+ # query = Ashikawa::Core::Query.new collection
51
+ # query.by_example { "color" => "red" }, :options => { :limit => 1 } # => #<Cursor id=2444>
52
+ def by_example(example={}, options={})
53
+ simple_query_request "/simple/by-example",
54
+ { example: example }.merge(options),
55
+ [:limit, :skip, :example]
56
+ end
57
+
58
+ # Looks for one document in a collection which matches the given criteria
59
+ #
60
+ # @param [Hash] example a Hash with data matching the document you are looking for.
61
+ # @return [Document]
62
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
63
+ # @api public
64
+ # @example Find one document in a collection that is red
65
+ # query = Ashikawa::Core::Query.new collection
66
+ # query.first_example { "color" => "red"} # => #<Document id=2444 color="red">
67
+ def first_example(example = {})
68
+ response = simple_query_request "/simple/first-example",
69
+ { example: example },
70
+ [:example]
71
+ response.first
72
+ end
73
+
74
+ # Looks for documents in a collection based on location
75
+ #
76
+ # @option options [Integer] :latitude Latitude location for your search.
77
+ # @option options [Integer] :longitude Longitude location for your search.
78
+ # @option options [Integer] :skip The documents to skip in the query.
79
+ # @option options [Integer] :distance If given, the attribute key used to store the distance.
80
+ # @option options [Integer] :limit The maximal amount of documents to return (default: 100).
81
+ # @option options [Integer] :geo If given, the identifier of the geo-index to use.
82
+ # @return [Cursor]
83
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
84
+ # @api public
85
+ # @example Find all documents at Infinite Loop
86
+ # query = Ashikawa::Core::Query.new collection
87
+ # query.near latitude: 37.331693, longitude: -122.030468
88
+ def near(options={})
89
+ simple_query_request "/simple/near",
90
+ options,
91
+ [:latitude, :longitude, :distance, :skip, :limit, :geo]
92
+ end
93
+
94
+ # Looks for documents in a collection within a radius
95
+ #
96
+ # @option options [Integer] :latitude Latitude location for your search.
97
+ # @option options [Integer] :longitude Longitude location for your search.
98
+ # @option options [Integer] :radius Radius around the given location you want to search in.
99
+ # @option options [Integer] :skip The documents to skip in the query.
100
+ # @option options [Integer] :distance If given, the attribute key used to store the distance.
101
+ # @option options [Integer] :limit The maximal amount of documents to return (default: 100).
102
+ # @option options [Integer] :geo If given, the identifier of the geo-index to use.
103
+ # @return [Cursor]
104
+ # @api public
105
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
106
+ # @example Find all documents within a radius of 100 to Infinite Loop
107
+ # query = Ashikawa::Core::Query.new collection
108
+ # query.within latitude: 37.331693, longitude: -122.030468, radius: 100
109
+ def within(options={})
110
+ simple_query_request "/simple/within",
111
+ options,
112
+ [:latitude, :longitude, :radius, :distance, :skip, :limit, :geo]
113
+ end
114
+
115
+ # Looks for documents in a collection with an attribute between two values
116
+ #
117
+ # @option options [Integer] :attribute The attribute path to check.
118
+ # @option options [Integer] :left The lower bound
119
+ # @option options [Integer] :right The upper bound
120
+ # @option options [Integer] :closed If true, use intervall including left and right, otherwise exclude right, but include left.
121
+ # @option options [Integer] :skip The documents to skip in the query (optional).
122
+ # @option options [Integer] :limit The maximal amount of documents to return (optional).
123
+ # @return [Cursor]
124
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
125
+ # @api public
126
+ # @example Find all documents within a radius of 100 to Infinite Loop
127
+ # query = Ashikawa::Core::Query.new collection
128
+ # query.within latitude: 37.331693, longitude: -122.030468, radius: 100
129
+ def in_range(options={})
130
+ simple_query_request "/simple/range",
131
+ options,
132
+ [:attribute, :left, :right, :closed, :limit, :skip]
133
+ end
134
+
135
+ # Send an AQL query to the database
136
+ #
137
+ # @param [String] query
138
+ # @option options [Integer] :count Should the number of results be counted?
139
+ # @option options [Integer] :batch_size Set the number of results returned at once
140
+ # @return [Cursor]
141
+ # @api public
142
+ # @example Send an AQL query to the database
143
+ # query = Ashikawa::Core::Query.new collection
144
+ # query.execute "FOR u IN users LIMIT 2" # => #<Cursor id=33>
145
+ def execute(query, options = {})
146
+ post_request "/cursor",
147
+ options.merge({ query: query }),
148
+ [:query, :count, :batch_size]
149
+ end
150
+
151
+ # Test if an AQL query is valid
152
+ #
153
+ # @param [String] query
154
+ # @return [Boolean]
155
+ # @api public
156
+ # @example Validate an AQL query
157
+ # query = Ashikawa::Core::Query.new collection
158
+ # query.valid? "FOR u IN users LIMIT 2" # => true
159
+ def valid?(query)
160
+ begin
161
+ !!post_request("/query", { query: query })
162
+ rescue RestClient::BadRequest
163
+ false
164
+ end
165
+ end
166
+
167
+ private
168
+
169
+ # The database object
170
+ #
171
+ # @return [Database]
172
+ # @api private
173
+ def database
174
+ @connection.respond_to?(:database) ? @connection.database : @connection
175
+ end
176
+
177
+ # The collection object
178
+ #
179
+ # @return [collection]
180
+ # @api private
181
+ def collection
182
+ raise NoCollectionProvidedException unless @connection.respond_to? :database
183
+ @connection
184
+ end
185
+
186
+ # Removes the keys that are not allowed from an object
187
+ #
188
+ # @param [Hash] options
189
+ # @param [Array<Symbol>] allowed_keys
190
+ # @return [Hash] The filtered Hash
191
+ # @api private
192
+ def allowed_options(options, allowed_keys)
193
+ options.keep_if { |key, _| allowed_keys.include? key }
194
+ end
195
+
196
+ # Transforms the keys into strings, camelizes them and removes pairs without a value
197
+ #
198
+ # @param [Hash] request_data
199
+ # @return [Hash] Cleaned request data
200
+ # @api private
201
+ def prepare_request_data(request_data)
202
+ Hash[request_data.map { |key, value|
203
+ [key.to_s.gsub(/_(.)/) { $1.upcase }, value]
204
+ }].reject { |_, value| value.nil? }
205
+ end
206
+
207
+ # Send a simple query to the server
208
+ #
209
+ # @param [String] path The path for the request
210
+ # @param [Hash] request_data The data send to the database
211
+ # @param [Array<Symbol>] keys The keys allowed for this request
212
+ # @return [String] Server response
213
+ # @raise [NoCollectionProvidedException] If you provided a database, no collection
214
+ # @api private
215
+ def simple_query_request(path, request_data, allowed_keys)
216
+ request_data = request_data.merge({ collection: collection.name })
217
+ put_request path,
218
+ request_data,
219
+ allowed_keys << :collection
220
+ end
221
+
222
+ # Perform a put request
223
+ #
224
+ # @param [String] path The path for the request
225
+ # @param [Hash] request_data The data send to the database
226
+ # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
227
+ # @return [Cursor]
228
+ # @api private
229
+ def put_request(path, request_data, allowed_keys = nil)
230
+ request_data = allowed_options request_data, allowed_keys unless allowed_keys.nil?
231
+ request_data = prepare_request_data request_data
232
+ server_response = send_request path, :put => request_data
233
+ Cursor.new database, server_response
234
+ end
235
+
236
+ # Perform a post request
237
+ #
238
+ # @param [String] path The path for the request
239
+ # @param [Hash] request_data The data send to the database
240
+ # @param [Array] allowed_keys Keys allowed in request_data, if nil: All keys are allowed
241
+ # @return [Cursor]
242
+ # @api private
243
+ def post_request(path, request_data, allowed_keys = nil)
244
+ request_data = allowed_options request_data, allowed_keys unless allowed_keys.nil?
245
+ request_data = prepare_request_data request_data
246
+ server_response = send_request path, :post => request_data
247
+ Cursor.new database, server_response
248
+ end
249
+ end
250
+ end
251
+ end
@@ -1,6 +1,6 @@
1
1
  module Ashikawa
2
2
  module Core
3
3
  # Current version of Ashikawa::Core
4
- VERSION = "0.4.1"
4
+ VERSION = "0.5.1"
5
5
  end
6
6
  end
@@ -1,7 +1,7 @@
1
1
  RSpec.configure do |config|
2
2
  raise "Could not find arangod. Please install it or check if it is in your path." if `which arangod` == ""
3
3
 
4
- database_directory = "/tmp/ashikawa-integration"
4
+ database_directory = "/tmp/ashikawa-acceptance"
5
5
  arango_process = false
6
6
 
7
7
  config.before(:suite) do
@@ -1,11 +1,11 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance/spec_helper'
2
2
 
3
3
  describe "Basics" do
4
4
  subject { ARANGO_HOST }
5
5
 
6
- it "should have booted up an ArangoDB instance" do
7
- expect { RestClient.get(subject) }.to_not raise_error
8
- end
6
+ # it "should have booted up an ArangoDB instance" do
7
+ # expect { RestClient.get(subject) }.to_not raise_error
8
+ # end
9
9
 
10
10
  describe "initialized database" do
11
11
  subject { Ashikawa::Core::Database.new ARANGO_HOST }
@@ -1,4 +1,4 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance/spec_helper'
2
2
 
3
3
  describe "Indices" do
4
4
  let(:database) { Ashikawa::Core::Database.new ARANGO_HOST }
@@ -0,0 +1,90 @@
1
+ require 'acceptance/spec_helper'
2
+
3
+ describe "Queries" do
4
+ let(:database) { Ashikawa::Core::Database.new ARANGO_HOST }
5
+ let(:collection) { database["my_collection"] }
6
+
7
+ describe "AQL query via the database" do
8
+ it "should return the documents" do
9
+ collection << { "name" => "Jeff Lebowski", "bowling" => true }
10
+ collection << { "name" => "Walter Sobchak", "bowling" => true }
11
+ collection << { "name" => "Donny Kerabatsos", "bowling" => true }
12
+ collection << { "name" => "Jeffrey Lebowski", "bowling" => false }
13
+
14
+ query = "FOR u IN my_collection FILTER u.bowling == true RETURN u"
15
+ results = database.query.execute query, batch_size: 2, count: true
16
+
17
+ results.length.should == 3
18
+ results = results.map { |person| person["name"] }
19
+ results.should include "Jeff Lebowski"
20
+ results.should_not include "Jeffrey Lebowski"
21
+ end
22
+
23
+ it "should be possible to validate" do
24
+ valid_query = "FOR u IN my_collection FILTER u.bowling == true RETURN u"
25
+ database.query.valid?(valid_query).should be_true
26
+
27
+ invalid_query = "FOR u IN my_collection FILTER u.bowling == true"
28
+ database.query.valid?(invalid_query).should be_false
29
+ end
30
+ end
31
+
32
+ describe "simple query via collection object" do
33
+ subject { collection }
34
+ before(:each) { subject.truncate! }
35
+
36
+ it "should return all documents of a collection" do
37
+ subject << { name: "testname", age: 27}
38
+ subject.query.all.first["name"].should == "testname"
39
+ end
40
+
41
+ it "should be possible to limit and skip results" do
42
+ subject << { name: "test1"}
43
+ subject << { name: "test2"}
44
+ subject << { name: "test3"}
45
+
46
+ subject.query.all(limit: 2).length.should == 2
47
+ subject.query.all(skip: 2).length.should == 1
48
+ end
49
+
50
+ it "should be possible to query documents by example" do
51
+ subject << { "name" => "Random Document" }
52
+ result = subject.query.by_example name: "Random Document"
53
+ result.length.should == 1
54
+ end
55
+
56
+ describe "query by geo coordinates" do
57
+ before :each do
58
+ subject.add_index :geo, on: [:latitude, :longitude]
59
+ subject << { "name" => "cologne", "latitude" => 50.948045, "longitude" => 6.961212 }
60
+ subject << { "name" => "san francisco", "latitude" => -122.395899, "longitude" => 37.793621 }
61
+ end
62
+
63
+ it "should be possible to query documents near a certain location" do
64
+ found_places = subject.query.near latitude: 50, longitude: 6
65
+ found_places.first["name"].should == "cologne"
66
+ end
67
+
68
+ it "should be possible to query documents within a certain range" do
69
+ found_places = subject.query.within latitude: 50.948040, longitude: 6.961210, radius: 2
70
+ found_places.length.should == 1
71
+ found_places.first["name"].should == "cologne"
72
+ end
73
+ end
74
+
75
+ describe "queries by integer ranges" do
76
+ before :each do
77
+ subject.add_index :skiplist, on: [:age]
78
+ subject << { "name" => "Georg", "age" => 12 }
79
+ subject << { "name" => "Anne", "age" => 21 }
80
+ subject << { "name" => "Jens", "age" => 49 }
81
+ end
82
+
83
+ it "should be possible to query documents for numbers in a certain range" do
84
+ found_people = subject.query.in_range attribute: "age", left: 20, right: 30, closed: true
85
+ found_people.length.should == 1
86
+ found_people.first["name"].should == "Anne"
87
+ end
88
+ end
89
+ end
90
+ end
File without changes
@@ -1,7 +1,7 @@
1
1
  RSpec.configure do |config|
2
2
  raise "Could not find arangod. Please install it or check if it is in your path." if `which arangod` == ""
3
3
 
4
- database_directory = "/tmp/ashikawa-integration-auth"
4
+ database_directory = "/tmp/ashikawa-acceptance-auth"
5
5
  arango_process = false
6
6
 
7
7
  config.before(:suite) do
@@ -1,4 +1,4 @@
1
- require 'integration/spec_helper'
1
+ require 'acceptance_auth/spec_helper'
2
2
 
3
3
  describe "authenticated database" do
4
4
  subject { ARANGO_HOST }
@@ -0,0 +1,5 @@
1
+ {
2
+ "error": false,
3
+ "bindVars": [],
4
+ "code": 200
5
+ }
@@ -0,0 +1,68 @@
1
+ #!/bin/bash
2
+
3
+ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
4
+ cd $DIR
5
+
6
+ #VERSION=1.0.2
7
+ VERSION=1.1.beta1
8
+
9
+ NAME=ArangoDB-$VERSION
10
+
11
+ if [ ! -d "$DIR/$NAME" ]; then
12
+ # download ArangoDB
13
+ wget http://www.arangodb.org/travisCI/$NAME.tar.gz
14
+ tar zxf $NAME.tar.gz
15
+ fi
16
+
17
+
18
+ PID=$(echo $PPID)
19
+ TMP_DIR="/tmp/arangodb.$PID"
20
+ PID_FILE="/tmp/arangodb.$PID.pid"
21
+ ARANGODB_DIR="$DIR/$NAME"
22
+ UPDATE_SCRIPT="${ARANGODB_DIR}/js/server/arango-upgrade.js"
23
+
24
+ # create database directory
25
+ mkdir ${TMP_DIR}
26
+
27
+ # check for update script
28
+ echo "looking for: $UPDATE_SCRIPT"
29
+ if [ -f "$UPDATE_SCRIPT" ] ; then
30
+ # version 1.1
31
+ ${ARANGODB_DIR}/bin/arangod \
32
+ --database.directory ${TMP_DIR} \
33
+ --configuration none \
34
+ --server.endpoint tcp://127.0.0.1:8529 \
35
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
36
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
37
+ --javascript.script "$UPDATE_SCRIPT"
38
+
39
+ ${ARANGODB_DIR}/bin/arangod \
40
+ --database.directory ${TMP_DIR} \
41
+ --configuration none \
42
+ --server.endpoint tcp://127.0.0.1:8529 \
43
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
44
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
45
+ --javascript.action-directory ${ARANGODB_DIR}/js/actions/system \
46
+ --database.maximal-journal-size 1048576 \
47
+ --server.disable-admin-interface true \
48
+ --server.disable-authentication true \
49
+ --javascript.gc-interval 1 &
50
+ else
51
+ # version 1.0
52
+ ${ARANGODB_DIR}/bin/arangod ${TMP_DIR} \
53
+ --configuration none \
54
+ --pid-file ${PID_FILE} \
55
+ --javascript.startup-directory ${ARANGODB_DIR}/js \
56
+ --javascript.modules-path ${ARANGODB_DIR}/js/server/modules:${ARANGODB_DIR}/js/common/modules \
57
+ --javascript.action-directory ${ARANGODB_DIR}/js/actions/system \
58
+ --database.maximal-journal-size 1000000 \
59
+ --javascript.gc-interval 1 &
60
+ fi
61
+
62
+ echo "Waiting until ArangoDB is ready on port 8529"
63
+ while [[ -z `curl -s 'http://127.0.0.1:8529/_api/version' ` ]] ; do
64
+ echo -n "."
65
+ sleep 2s
66
+ done
67
+
68
+ echo "ArangoDB is up"