gitmodel 0.0.2 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -0
- data/Gemfile.lock +13 -15
- data/README.md +68 -33
- data/gitmodel.gemspec +2 -2
- data/lib/gitmodel/errors.rb +10 -2
- data/lib/gitmodel/index.rb +79 -0
- data/lib/gitmodel/persistable.rb +108 -28
- data/lib/gitmodel.rb +8 -1
- data/spec/gitmodel/index_spec.rb +108 -0
- data/spec/gitmodel/persistable_spec.rb +168 -17
- data/spec/gitmodel_spec.rb +14 -5
- data/spec/spec_helper.rb +9 -1
- metadata +13 -12
data/.gitignore
CHANGED
data/Gemfile.lock
CHANGED
@@ -6,20 +6,19 @@ PATH
|
|
6
6
|
activesupport (>= 3.0.1)
|
7
7
|
grit (>= 2.3.0)
|
8
8
|
lockfile (>= 1.4.3)
|
9
|
+
yajl-ruby (>= 0.8.2)
|
9
10
|
|
10
11
|
GEM
|
11
12
|
remote: http://rubygems.org/
|
12
13
|
specs:
|
13
14
|
ZenTest (4.5.0)
|
14
|
-
activemodel (3.0.
|
15
|
-
activesupport (= 3.0.
|
15
|
+
activemodel (3.0.9)
|
16
|
+
activesupport (= 3.0.9)
|
16
17
|
builder (~> 2.1.2)
|
17
|
-
i18n (~> 0.
|
18
|
-
activesupport (3.0.
|
18
|
+
i18n (~> 0.5.0)
|
19
|
+
activesupport (3.0.9)
|
19
20
|
autotest (4.4.6)
|
20
21
|
ZenTest (>= 4.4.1)
|
21
|
-
autotest-fsevent (0.2.4)
|
22
|
-
sys-uname
|
23
22
|
builder (2.1.2)
|
24
23
|
diff-lcs (1.1.2)
|
25
24
|
grit (2.4.1)
|
@@ -28,15 +27,15 @@ GEM
|
|
28
27
|
i18n (0.5.0)
|
29
28
|
lockfile (1.4.3)
|
30
29
|
mime-types (1.16)
|
31
|
-
rspec (2.
|
32
|
-
rspec-core (~> 2.
|
33
|
-
rspec-expectations (~> 2.
|
34
|
-
rspec-mocks (~> 2.
|
35
|
-
rspec-core (2.
|
36
|
-
rspec-expectations (2.
|
30
|
+
rspec (2.6.0)
|
31
|
+
rspec-core (~> 2.6.0)
|
32
|
+
rspec-expectations (~> 2.6.0)
|
33
|
+
rspec-mocks (~> 2.6.0)
|
34
|
+
rspec-core (2.6.4)
|
35
|
+
rspec-expectations (2.6.0)
|
37
36
|
diff-lcs (~> 1.1.2)
|
38
|
-
rspec-mocks (2.
|
39
|
-
|
37
|
+
rspec-mocks (2.6.0)
|
38
|
+
yajl-ruby (0.8.2)
|
40
39
|
|
41
40
|
PLATFORMS
|
42
41
|
ruby
|
@@ -44,6 +43,5 @@ PLATFORMS
|
|
44
43
|
DEPENDENCIES
|
45
44
|
ZenTest (>= 4.4.0)
|
46
45
|
autotest (>= 4.4.1)
|
47
|
-
autotest-fsevent (>= 0.2.3)
|
48
46
|
gitmodel!
|
49
47
|
rspec (>= 2.0.1)
|
data/README.md
CHANGED
@@ -17,13 +17,6 @@ machines, manipulated with standard Git client tools, can be branched and
|
|
17
17
|
merged, and of course keeps the history of all changes.
|
18
18
|
|
19
19
|
|
20
|
-
Status
|
21
|
-
------
|
22
|
-
|
23
|
-
_It is nowhere near production ready but I'm working on it. Please feel free to
|
24
|
-
contribute tests and/or code to help!_
|
25
|
-
|
26
|
-
|
27
20
|
Why it's awesome
|
28
21
|
----------------
|
29
22
|
|
@@ -38,6 +31,7 @@ Why it's awesome
|
|
38
31
|
* Experiment on production data using branches, for example to test a
|
39
32
|
migration
|
40
33
|
* Distributed (synced using standard Git push/pull)
|
34
|
+
* All ActiveModel
|
41
35
|
* Transactions
|
42
36
|
* Metadata for all database changes (Git commit messages, date & time, etc.)
|
43
37
|
* In order to be easily human-editable, the database is simply files and
|
@@ -48,6 +42,22 @@ Why it's awesome
|
|
48
42
|
* Clean and easy-to-use API
|
49
43
|
|
50
44
|
|
45
|
+
Status
|
46
|
+
------
|
47
|
+
|
48
|
+
_It is not yet production ready but I'm working on it. Please feel free to
|
49
|
+
contribute tests and/or code to help!_
|
50
|
+
|
51
|
+
See the "To do" section below for details, but the main thing that needs
|
52
|
+
finishing is support for querying. Right now you can find an instance by it's
|
53
|
+
id, but there is incomplete support (90% complete) for querying, e.g.:
|
54
|
+
|
55
|
+
Post.find(:category => 'ruby', :date => lambda{|d| d > 1.month.ago} :order_by => :date, :order => :asc, :limit => 5)
|
56
|
+
|
57
|
+
This includes support for indexing all attributes so that queries don't need to
|
58
|
+
load every object.
|
59
|
+
|
60
|
+
|
51
61
|
Installation
|
52
62
|
------------
|
53
63
|
|
@@ -77,6 +87,8 @@ Usage
|
|
77
87
|
p1.image = some_binary_data
|
78
88
|
p1.save!
|
79
89
|
|
90
|
+
p = Post.find('lessons-learned')
|
91
|
+
|
80
92
|
p2 = Post.new(:id => 'hotdog-eating-contest', :title => 'I won!')
|
81
93
|
p2.body = 'This weekend I won a hotdog eating contest!'
|
82
94
|
p2.image = some_binary_data
|
@@ -96,6 +108,7 @@ Usage
|
|
96
108
|
c1 = Comment.create!(:id => '2010-01-03-328', :text => '...')
|
97
109
|
c2 = Comment.create!(:id => '2010-05-29-742', :text => '...')
|
98
110
|
|
111
|
+
|
99
112
|
An example of a project that uses GitModel is [Balisong](https://github.com/pauldowman/balisong), a blogging app for coders (but it doesn't save objects to the data store. It's read-only so far, assuming that posts will be edited with a text editor).
|
100
113
|
|
101
114
|
|
@@ -116,42 +129,64 @@ For example, the database for the example above would have a directory
|
|
116
129
|
structure that looks like this:
|
117
130
|
|
118
131
|
* db-root
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
132
|
+
* comments
|
133
|
+
* 2010-01-03-328
|
134
|
+
* _attributes.json_
|
135
|
+
* 2010-05-29-742
|
136
|
+
* _attributes.json_
|
137
|
+
* posts
|
138
|
+
* hotdog-eating-contest
|
139
|
+
* _attributes.json_
|
140
|
+
* _hotdogs.jpg_
|
141
|
+
* _image_
|
142
|
+
* _the-aftermath.jpg_
|
143
|
+
* lessons-learned
|
144
|
+
* _attributes.json_
|
145
|
+
* _image_
|
146
|
+
* running-with-scissors
|
147
|
+
* _attributes.json_
|
148
|
+
|
149
|
+
|
150
|
+
Contributing
|
137
151
|
------------
|
138
152
|
|
139
|
-
|
153
|
+
Do you have an improvement to make? Please submit a pull request on GitHub or a
|
154
|
+
patch, including a test written with RSpec. To run all tests simply run
|
155
|
+
`autospec`.
|
156
|
+
|
157
|
+
The main author is [Paul Dowman](http://pauldowman.com/about) ([@pauldowman](http://twitter.com/pauldowman)).
|
140
158
|
|
159
|
+
Thanks to everyone who has contributed so far:
|
141
160
|
|
142
|
-
|
161
|
+
* [Alex Bartlow](https://github.com/alexbartlow)
|
162
|
+
|
163
|
+
|
164
|
+
To do
|
143
165
|
-----
|
144
166
|
|
167
|
+
* Finish Query support
|
168
|
+
* Update index (efficiently) when Persistable objects are saved
|
169
|
+
* Add Rake task to generate index
|
170
|
+
* Update README
|
145
171
|
* Add validations and other feature examples to sample code in README
|
146
|
-
* Querying
|
147
|
-
* Use AREL?
|
148
172
|
* Finish some pending specs
|
149
|
-
* Associations
|
150
173
|
* API documentation
|
151
174
|
* Rails integration
|
152
|
-
|
153
|
-
|
175
|
+
* Generators
|
176
|
+
* Rake tasks
|
154
177
|
* Performance
|
155
|
-
|
178
|
+
* Haven't optimized for performance yet.
|
179
|
+
* Use [Rugged](https://github.com/libgit2/rugged) instead of Grit
|
180
|
+
* Remove the transaction lock (see transaction.rb line 19)
|
181
|
+
* Ability to iterate over result set without eager loading of all instances
|
182
|
+
* Persistable.find/find_all/etc could be based on staged files so that queries reflect uncommitted changes
|
183
|
+
* Better query support
|
184
|
+
* Associations
|
185
|
+
* Use AREL?
|
186
|
+
|
187
|
+
|
188
|
+
Bugs
|
189
|
+
------------
|
156
190
|
|
191
|
+
* Grit 2.4.1 has [an issue with non-ASCII characters](https://github.com/mojombo/grit/commit/696761d8047ffd038dc2828e6a1998e3f7c3b419)
|
157
192
|
|
data/gitmodel.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'gitmodel'
|
3
|
-
s.version = '0.0.
|
3
|
+
s.version = '0.0.3'
|
4
4
|
s.platform = Gem::Platform::RUBY
|
5
5
|
|
6
6
|
s.authors = ["Paul Dowman"]
|
@@ -21,10 +21,10 @@ Gem::Specification.new do |s|
|
|
21
21
|
s.add_dependency 'activesupport', '>= 3.0.1'
|
22
22
|
s.add_dependency 'grit', '>= 2.3.0'
|
23
23
|
s.add_dependency 'lockfile', '>= 1.4.3'
|
24
|
+
s.add_dependency 'yajl-ruby', '>= 0.8.2'
|
24
25
|
|
25
26
|
s.add_development_dependency 'ZenTest', '>= 4.4.0'
|
26
27
|
s.add_development_dependency 'autotest', '>= 4.4.1'
|
27
|
-
s.add_development_dependency 'autotest-fsevent', '>= 0.2.3' if RUBY_PLATFORM.downcase.include?("darwin") # OS X only
|
28
28
|
s.add_development_dependency 'rspec', '>= 2.0.1'
|
29
29
|
|
30
30
|
s.files = `git ls-files`.split("\n")
|
data/lib/gitmodel/errors.rb
CHANGED
@@ -8,8 +8,8 @@ module GitModel
|
|
8
8
|
class RecordNotFound < GitModelError
|
9
9
|
end
|
10
10
|
|
11
|
-
# Raised by GitModel::Persistable.save! and GitModel::Persistable.create!
|
12
|
-
# saved because record is invalid.
|
11
|
+
# Raised by GitModel::Persistable.save! and GitModel::Persistable.create!
|
12
|
+
# methods when record cannot be saved because record is invalid.
|
13
13
|
class RecordNotSaved < GitModelError
|
14
14
|
end
|
15
15
|
|
@@ -22,4 +22,12 @@ module GitModel
|
|
22
22
|
class NullId < GitModelError
|
23
23
|
end
|
24
24
|
|
25
|
+
# Raised by GitModel::Persistable.find_all when query conditions are given
|
26
|
+
# but there is no index generated
|
27
|
+
class IndexRequired < GitModelError
|
28
|
+
end
|
29
|
+
|
30
|
+
class AttributeNotIndexed < GitModelError
|
31
|
+
end
|
32
|
+
|
25
33
|
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module GitModel
|
2
|
+
class Index
|
3
|
+
def initialize(model_class)
|
4
|
+
@model_class = model_class
|
5
|
+
end
|
6
|
+
|
7
|
+
def generate!
|
8
|
+
GitModel.logger.debug "Generating indexes for #{@model_class}"
|
9
|
+
# TODO it sucks to load every instance here, optimize later
|
10
|
+
@indexes = {}
|
11
|
+
@model_class.find_all.each do |o|
|
12
|
+
o.attributes.each do |attr, value|
|
13
|
+
@indexes[attr] ||= {}
|
14
|
+
@indexes[attr][value] ||= SortedSet.new
|
15
|
+
@indexes[attr][value] << o.id
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def attr_index(attr)
|
21
|
+
self.load unless @indexes
|
22
|
+
return nil unless @indexes # this is just so that we can stub self.load in tests
|
23
|
+
|
24
|
+
ret = @indexes[attr.to_s]
|
25
|
+
raise GitModel::AttributeNotIndexed.new(attr.to_s) unless ret
|
26
|
+
return ret
|
27
|
+
end
|
28
|
+
|
29
|
+
def filename
|
30
|
+
File.join(@model_class.db_subdir, '_indexes.json')
|
31
|
+
end
|
32
|
+
|
33
|
+
def generated?
|
34
|
+
(GitModel.current_tree / filename) ? true : false
|
35
|
+
end
|
36
|
+
|
37
|
+
def save(options = {})
|
38
|
+
GitModel.logger.debug "Saving indexes for #{@model_class}..."
|
39
|
+
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
40
|
+
result = transaction.execute do |t|
|
41
|
+
# convert to array because JSON hash keys must be strings
|
42
|
+
data = []
|
43
|
+
@indexes.each do |attr,values|
|
44
|
+
values_and_ids = []
|
45
|
+
values.each do |value, ids|
|
46
|
+
values_and_ids << [value, ids.to_a]
|
47
|
+
end
|
48
|
+
data << [attr,values_and_ids]
|
49
|
+
end
|
50
|
+
data = Yajl::Encoder.encode(data, nil, :pretty => true)
|
51
|
+
t.index.add(filename, data)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def load
|
56
|
+
unless generated?
|
57
|
+
GitModel.logger.debug "No index generated for #{@model_class}, not loading."
|
58
|
+
return
|
59
|
+
end
|
60
|
+
|
61
|
+
GitModel.logger.debug "Loading indexes for #{@model_class}..."
|
62
|
+
@indexes = {}
|
63
|
+
blob = GitModel.current_tree / filename
|
64
|
+
|
65
|
+
data = Yajl::Parser.parse(blob.data)
|
66
|
+
data.each do |attr_and_values|
|
67
|
+
attr = attr_and_values[0]
|
68
|
+
values = {}
|
69
|
+
attr_and_values[1].each do |value_and_ids|
|
70
|
+
value = value_and_ids[0]
|
71
|
+
ids = SortedSet.new(value_and_ids[1])
|
72
|
+
values[value] = ids
|
73
|
+
end
|
74
|
+
@indexes[attr] = values
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
end
|
79
|
+
end
|
data/lib/gitmodel/persistable.rb
CHANGED
@@ -13,6 +13,9 @@ module GitModel
|
|
13
13
|
|
14
14
|
define_model_callbacks :initialize, :find, :touch, :only => :after
|
15
15
|
define_model_callbacks :save, :create, :update, :destroy
|
16
|
+
|
17
|
+
cattr_accessor :index, true
|
18
|
+
self.index = GitModel::Index.new(self)
|
16
19
|
end
|
17
20
|
|
18
21
|
base.extend(ClassMethods)
|
@@ -84,31 +87,32 @@ module GitModel
|
|
84
87
|
# :commit_message
|
85
88
|
# Returns false if validations failed, otherwise returns the SHA of the commit
|
86
89
|
def save(options = {})
|
87
|
-
|
90
|
+
_run_save_callbacks do
|
91
|
+
raise GitModel::NullId unless self.id
|
88
92
|
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
93
|
+
if new_record?
|
94
|
+
raise GitModel::RecordExists if self.class.exists?(self.id)
|
95
|
+
else
|
96
|
+
raise GitModel::RecordDoesntExist unless self.class.exists?(self.id)
|
97
|
+
end
|
94
98
|
|
95
|
-
|
99
|
+
GitModel.logger.debug "Saving #{self.class.name} with id: #{id}"
|
96
100
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
t.index.add(File.join(dir, 'attributes.json'), JSON.pretty_generate(attributes.to_hash))
|
101
|
+
dir = File.join(self.class.db_subdir, self.id)
|
102
|
+
|
103
|
+
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
104
|
+
result = transaction.execute do |t|
|
105
|
+
# Write the attributes to the attributes file
|
106
|
+
t.index.add(File.join(dir, 'attributes.json'), Yajl::Encoder.encode(attributes, nil, :pretty => true))
|
104
107
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
+
# Write the blob files
|
109
|
+
blobs.each do |name, data|
|
110
|
+
t.index.add(File.join(dir, name), data)
|
111
|
+
end
|
108
112
|
end
|
109
|
-
end
|
110
113
|
|
111
|
-
|
114
|
+
result
|
115
|
+
end
|
112
116
|
end
|
113
117
|
|
114
118
|
# Same as #save but raises an exception on error
|
@@ -139,11 +143,13 @@ module GitModel
|
|
139
143
|
self.id = File.basename(dir)
|
140
144
|
@new_record = false
|
141
145
|
|
146
|
+
GitModel.logger.debug "Loading #{self.class.name} with id: #{id}"
|
147
|
+
|
142
148
|
# load the attributes
|
143
149
|
object = GitModel.current_tree / File.join(dir, 'attributes.json')
|
144
150
|
raise GitModel::RecordNotFound if object.nil?
|
145
151
|
|
146
|
-
self.attributes =
|
152
|
+
self.attributes = Yajl::Parser.parse(object.data)
|
147
153
|
|
148
154
|
# load all other non-hidden files in the dir as blobs
|
149
155
|
blobs = (GitModel.current_tree / dir).blobs.reject{|b| b.name[0] == '.' || b.name == 'attributes.json'}
|
@@ -184,25 +190,92 @@ module GitModel
|
|
184
190
|
end
|
185
191
|
|
186
192
|
def exists?(id)
|
193
|
+
GitModel.logger.debug "Checking existence of #{name} with id: #{id}"
|
187
194
|
GitModel.repo.commits.any? && !(GitModel.current_tree / File.join(db_subdir, id, 'attributes.json')).nil?
|
188
195
|
end
|
189
196
|
|
190
197
|
def find_all(conditions = {})
|
198
|
+
# TODO Refactor this spaghetti
|
191
199
|
GitModel.logger.debug "Finding all #{name.pluralize} with conditions: #{conditions.inspect}"
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
196
|
-
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
+
return [] unless GitModel.current_tree
|
201
|
+
|
202
|
+
order = conditions.delete(:order) || :asc
|
203
|
+
order_by = conditions.delete(:order_by) || :id
|
204
|
+
limit = conditions.delete(:limit)
|
205
|
+
|
206
|
+
matching_ids = []
|
207
|
+
if conditions.empty? # load all objects
|
208
|
+
trees = (GitModel.current_tree / db_subdir).trees
|
209
|
+
trees.each do |t|
|
210
|
+
matching_ids << t.name if t.blobs.any?
|
200
211
|
end
|
212
|
+
else # only load objects that match conditions
|
213
|
+
matching_ids_for_condition = {}
|
214
|
+
conditions.each do |k,v|
|
215
|
+
matching_ids_for_condition[k] = []
|
216
|
+
if k == :id # id isn't indexed
|
217
|
+
if v.is_a?(Proc)
|
218
|
+
trees = (GitModel.current_tree / db_subdir).trees
|
219
|
+
trees.each do |t|
|
220
|
+
matching_ids_for_condition[k] << t.name if t.blobs.any? && v.call(t.name)
|
221
|
+
end
|
222
|
+
else
|
223
|
+
# an unlikely use case but supporting it for completeness
|
224
|
+
matching_ids_for_condition[k] << v if (GitModel.current_tree / db_subdir / v)
|
225
|
+
end
|
226
|
+
else
|
227
|
+
raise GitModel::IndexRequired unless index.generated?
|
228
|
+
attr_index = index.attr_index(k)
|
229
|
+
if v.is_a?(Proc)
|
230
|
+
attr_index.each do |value, ids|
|
231
|
+
matching_ids_for_condition[k] += ids.to_a if v.call(value)
|
232
|
+
end
|
233
|
+
else
|
234
|
+
matching_ids_for_condition[k] += attr_index[v].to_a
|
235
|
+
end
|
236
|
+
end
|
237
|
+
end
|
238
|
+
matching_ids += matching_ids_for_condition.values.inject{|memo, obj| memo & obj}
|
239
|
+
end
|
240
|
+
|
241
|
+
results = nil
|
242
|
+
if order_by != :id
|
243
|
+
GitModel.logger.warn "Ordering by an attribute other than id requires loading all matching objects before applying limit, this will be slow" if limit
|
244
|
+
results = matching_ids.map{|k| find(k)}
|
245
|
+
|
246
|
+
if order == :asc
|
247
|
+
results = results.sort{|a,b| a.send(order_by) <=> b.send(order_by)}
|
248
|
+
elsif order == :desc
|
249
|
+
results = results.sort{|b,a| a.send(order_by) <=> b.send(order_by)}
|
250
|
+
else
|
251
|
+
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
252
|
+
end
|
253
|
+
|
254
|
+
if limit
|
255
|
+
results = results[0, limit]
|
256
|
+
end
|
257
|
+
else
|
258
|
+
if limit
|
259
|
+
matching_ids = matching_ids[0, limit]
|
260
|
+
end
|
261
|
+
if order == :asc
|
262
|
+
matching_ids = matching_ids.sort{|a,b| a <=> b}
|
263
|
+
elsif order == :desc
|
264
|
+
matching_ids = matching_ids.sort{|b,a| a <=> b}
|
265
|
+
else
|
266
|
+
raise GitModel::InvalidParams("invalid order: '#{order}'")
|
267
|
+
end
|
268
|
+
results = matching_ids.map{|k| find(k)}
|
201
269
|
end
|
202
270
|
|
203
271
|
return results
|
204
272
|
end
|
205
273
|
|
274
|
+
def all_values_for_attr(attr)
|
275
|
+
attr_index = index.attr_index(attr.to_s)
|
276
|
+
values = attr_index ? attr_index.keys : []
|
277
|
+
end
|
278
|
+
|
206
279
|
def create(args)
|
207
280
|
if args.is_a?(Array)
|
208
281
|
args.map{|arg| create(arg)}
|
@@ -224,6 +297,7 @@ module GitModel
|
|
224
297
|
end
|
225
298
|
|
226
299
|
def delete(id, options = {})
|
300
|
+
GitModel.logger.debug "Deleting #{name} with id: #{id}"
|
227
301
|
path = File.join(db_subdir, id)
|
228
302
|
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
229
303
|
result = transaction.execute do |t|
|
@@ -232,12 +306,18 @@ module GitModel
|
|
232
306
|
end
|
233
307
|
|
234
308
|
def delete_all(options = {})
|
309
|
+
GitModel.logger.debug "Deleting all #{name.pluralize}"
|
235
310
|
transaction = options.delete(:transaction) || GitModel::Transaction.new(options)
|
236
311
|
result = transaction.execute do |t|
|
237
312
|
delete_tree(db_subdir, t.index, options)
|
238
313
|
end
|
239
314
|
end
|
240
315
|
|
316
|
+
def index!
|
317
|
+
index.generate!
|
318
|
+
index.save
|
319
|
+
end
|
320
|
+
|
241
321
|
|
242
322
|
private
|
243
323
|
|
data/lib/gitmodel.rb
CHANGED
@@ -4,12 +4,13 @@ require 'bundler/setup'
|
|
4
4
|
require 'active_model'
|
5
5
|
require 'active_support/all' # TODO we don't really want all here, clean this up
|
6
6
|
require 'grit'
|
7
|
-
require '
|
7
|
+
require 'yajl'
|
8
8
|
require 'lockfile'
|
9
9
|
require 'pp'
|
10
10
|
|
11
11
|
$:.unshift(File.dirname(__FILE__))
|
12
12
|
require 'gitmodel/errors'
|
13
|
+
require 'gitmodel/index'
|
13
14
|
require 'gitmodel/persistable'
|
14
15
|
require 'gitmodel/transaction'
|
15
16
|
|
@@ -73,4 +74,10 @@ module GitModel
|
|
73
74
|
c ? c.tree : nil
|
74
75
|
end
|
75
76
|
|
77
|
+
def self.index!
|
78
|
+
dirs = (GitModel.current_tree).trees
|
79
|
+
dirs.each do |dir|
|
80
|
+
dir.name.classify.constantize.index!
|
81
|
+
end
|
82
|
+
end
|
76
83
|
end
|
@@ -0,0 +1,108 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe GitModel::Index do
|
4
|
+
before(:each) do
|
5
|
+
TestEntity.create!(:id => "foo", :attributes => {:x => 1, :y => 2})
|
6
|
+
TestEntity.create!(:id => "bar", :attributes => {:x => 1, :y => 3})
|
7
|
+
TestEntity.create!(:id => "baz", :attributes => {:x => 2, :y => 2})
|
8
|
+
|
9
|
+
@i = GitModel::Index.new(TestEntity)
|
10
|
+
@i.generate!
|
11
|
+
end
|
12
|
+
|
13
|
+
it "has a hash for each attribute of the model" do
|
14
|
+
@i.attr_index(:x).should be_a(Hash)
|
15
|
+
end
|
16
|
+
|
17
|
+
it "knows the id's of all instances with a given value for an attribute" do
|
18
|
+
@i.attr_index(:x)[1].should == SortedSet.new(["foo", "bar"])
|
19
|
+
@i.attr_index(:x)[2].should == SortedSet.new(["baz"])
|
20
|
+
@i.attr_index(:y)[2].should == SortedSet.new(["foo", "baz"])
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can regenerate itself" do
|
24
|
+
@i.attr_index(:x).clear
|
25
|
+
@i.attr_index(:x).should be_empty
|
26
|
+
@i.generate!
|
27
|
+
@i.attr_index(:x).should == {1 => SortedSet.new(["foo", "bar"]), 2 => SortedSet.new(["baz"])}
|
28
|
+
end
|
29
|
+
|
30
|
+
it "knows it's filename" do
|
31
|
+
@i.filename.should == "test_entities/_indexes.json"
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can save itself to a JSON file" do
|
35
|
+
@i.save
|
36
|
+
json = <<-END.strip
|
37
|
+
[
|
38
|
+
[
|
39
|
+
"x",
|
40
|
+
[
|
41
|
+
[
|
42
|
+
1,
|
43
|
+
[
|
44
|
+
"bar",
|
45
|
+
"foo"
|
46
|
+
]
|
47
|
+
],
|
48
|
+
[
|
49
|
+
2,
|
50
|
+
[
|
51
|
+
"baz"
|
52
|
+
]
|
53
|
+
]
|
54
|
+
]
|
55
|
+
],
|
56
|
+
[
|
57
|
+
"y",
|
58
|
+
[
|
59
|
+
[
|
60
|
+
3,
|
61
|
+
[
|
62
|
+
"bar"
|
63
|
+
]
|
64
|
+
],
|
65
|
+
[
|
66
|
+
2,
|
67
|
+
[
|
68
|
+
"baz",
|
69
|
+
"foo"
|
70
|
+
]
|
71
|
+
]
|
72
|
+
]
|
73
|
+
]
|
74
|
+
]
|
75
|
+
END
|
76
|
+
repo = Grit::Repo.new(GitModel.db_root)
|
77
|
+
# We should be able to use just repo.commits.first here but
|
78
|
+
# this is a workaround for this bug:
|
79
|
+
# http://github.com/mojombo/grit/issues/issue/38
|
80
|
+
(repo.commits("master^..master").first.tree / @i.filename).data.should == json
|
81
|
+
end
|
82
|
+
|
83
|
+
it "can save and load itself from a file" do
|
84
|
+
@i.save
|
85
|
+
@i.attr_index(:x).clear
|
86
|
+
@i.load
|
87
|
+
@i.attr_index(:x).should == {1 => SortedSet.new(["foo", "bar"]), 2 => SortedSet.new(["baz"])}
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#attr_index" do
|
91
|
+
it "loads itself" do
|
92
|
+
i = GitModel::Index.new(TestEntity)
|
93
|
+
i.should_receive(:load)
|
94
|
+
i.attr_index(:foo)
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "with an index file already created" do
|
98
|
+
before(:each) { @i.save }
|
99
|
+
|
100
|
+
it "loads itself from file" do
|
101
|
+
i = GitModel::Index.new(TestEntity)
|
102
|
+
i.should_not_receive(:generate!)
|
103
|
+
i.attr_index(:x)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
end
|
@@ -1,9 +1,5 @@
|
|
1
1
|
require 'spec_helper'
|
2
2
|
|
3
|
-
class TestEntity
|
4
|
-
include GitModel::Persistable
|
5
|
-
end
|
6
|
-
|
7
3
|
class LintTest < ActiveModel::TestCase
|
8
4
|
include ActiveModel::Lint::Tests
|
9
5
|
|
@@ -51,7 +47,7 @@ describe GitModel::Persistable do
|
|
51
47
|
|
52
48
|
repo = Grit::Repo.new(GitModel.db_root)
|
53
49
|
attrs = (repo.commits.first.tree / File.join(TestEntity.db_subdir, id, 'attributes.json')).data
|
54
|
-
r =
|
50
|
+
r = Yajl::Parser.parse(attrs)
|
55
51
|
r.size.should == 2
|
56
52
|
r['one'].should == 1
|
57
53
|
r['two'].should == 2
|
@@ -83,6 +79,10 @@ describe GitModel::Persistable do
|
|
83
79
|
it 'returns the SHA of the commit if the save was successful'
|
84
80
|
|
85
81
|
it 'deletes blobs that have been removed'
|
82
|
+
|
83
|
+
it 'updates the index' do
|
84
|
+
# TODO
|
85
|
+
end
|
86
86
|
end
|
87
87
|
|
88
88
|
describe '#save!' do
|
@@ -223,6 +223,7 @@ describe GitModel::Persistable do
|
|
223
223
|
TestEntity.create!(:id => 'ape')
|
224
224
|
|
225
225
|
TestEntity.delete_all
|
226
|
+
TestEntity.index!
|
226
227
|
TestEntity.find_all.should be_empty
|
227
228
|
end
|
228
229
|
|
@@ -246,7 +247,7 @@ describe GitModel::Persistable do
|
|
246
247
|
|
247
248
|
end
|
248
249
|
|
249
|
-
describe '
|
250
|
+
describe '.find' do
|
250
251
|
|
251
252
|
#it 'can load an object from an empty subdir of db_root' do
|
252
253
|
# id = "foo"
|
@@ -316,25 +317,158 @@ describe GitModel::Persistable do
|
|
316
317
|
|
317
318
|
end
|
318
319
|
|
319
|
-
describe '
|
320
|
+
describe '.find_all' do
|
321
|
+
describe 'with no parameters' do
|
322
|
+
it 'returns an array of all objects' do
|
323
|
+
TestEntity.create!(:id => 'one')
|
324
|
+
TestEntity.create!(:id => 'two')
|
325
|
+
TestEntity.create!(:id => 'three')
|
320
326
|
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
327
|
+
r = TestEntity.find_all
|
328
|
+
r.size.should == 3
|
329
|
+
end
|
330
|
+
|
331
|
+
it 'returns an empty array if there are no objects of the current type' do
|
332
|
+
r = TestEntity.find_all
|
333
|
+
r.should == []
|
334
|
+
end
|
335
|
+
end
|
336
|
+
|
337
|
+
describe 'with conditions but no index' do
|
338
|
+
it 'raises an exception' do
|
339
|
+
TestEntity.create!(:id => 'one')
|
340
|
+
lambda {TestEntity.find_all(:a => "b")}.should raise_error(GitModel::IndexRequired)
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
describe 'with one condition' do
|
345
|
+
describe 'with a literal value' do
|
346
|
+
it 'returns an array of all objects that match' do
|
347
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
348
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
349
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
350
|
+
TestEntity.index!
|
351
|
+
|
352
|
+
r = TestEntity.find_all(:a => 1)
|
353
|
+
r.size.should == 2
|
354
|
+
r.first.id.should == 'one'
|
355
|
+
r.second.id.should == 'three'
|
356
|
+
end
|
357
|
+
end
|
358
|
+
|
359
|
+
describe 'with a lambda as the value' do
|
360
|
+
it 'returns an array of all objects that match' do
|
361
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
362
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
363
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
364
|
+
TestEntity.index!
|
365
|
+
|
366
|
+
r = TestEntity.find_all(:b => lambda{|b| b > 1}, :order => :asc)
|
367
|
+
r.size.should == 2
|
368
|
+
r.first.id.should == 'three'
|
369
|
+
r.second.id.should == 'two'
|
370
|
+
end
|
371
|
+
end
|
372
|
+
end
|
373
|
+
|
374
|
+
describe 'with multiple conditions' do
|
375
|
+
describe 'with a literal value' do
|
376
|
+
it 'returns an array of all objects that match both (i.e. AND)' do
|
377
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 2})
|
378
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
|
379
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
|
380
|
+
TestEntity.index!
|
381
|
+
|
382
|
+
r = TestEntity.find_all(:a => 1, :b => 2, :order => :asc)
|
383
|
+
r.size.should == 2
|
384
|
+
r.first.id.should == 'one'
|
385
|
+
r.second.id.should == 'two'
|
386
|
+
end
|
387
|
+
end
|
388
|
+
|
389
|
+
describe 'with a lambda as the value' do
|
390
|
+
it 'returns an array of all objects that match both (i.e. AND)' do
|
391
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 3})
|
392
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
393
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 1})
|
394
|
+
TestEntity.create!(:id => 'four', :attributes => {:a => 3, :b => 3})
|
395
|
+
TestEntity.index!
|
396
|
+
|
397
|
+
r = TestEntity.find_all(:a => lambda{|a| a > 1}, :b => lambda{|b| b > 2}, :order => :asc)
|
398
|
+
r.size.should == 1
|
399
|
+
r.first.id.should == 'four'
|
400
|
+
end
|
401
|
+
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
describe 'with the id attribute in the conditions' do
|
406
|
+
describe 'with a literal value' do
|
407
|
+
it 'returns an array that includes the object with that id' do
|
408
|
+
TestEntity.create!(:id => 'one')
|
409
|
+
TestEntity.create!(:id => 'two')
|
410
|
+
TestEntity.create!(:id => 'three')
|
411
|
+
|
412
|
+
r = TestEntity.find_all(:id => 'one')
|
413
|
+
r.size.should == 1
|
414
|
+
r.first.id.should == 'one'
|
415
|
+
end
|
416
|
+
end
|
417
|
+
|
418
|
+
describe 'with a lambda as the value' do
|
419
|
+
it 'returns an array that includes the object with that id' do
|
420
|
+
TestEntity.create!(:id => 'one')
|
421
|
+
TestEntity.create!(:id => 'two')
|
422
|
+
TestEntity.create!(:id => 'three')
|
423
|
+
|
424
|
+
r = TestEntity.find_all(:id => lambda{|id| id =~ /o/})
|
425
|
+
r.size.should == 2
|
426
|
+
r.first.id.should == 'one'
|
427
|
+
r.second.id.should == 'two'
|
428
|
+
end
|
429
|
+
|
430
|
+
end
|
431
|
+
end
|
432
|
+
|
433
|
+
it 'can return results in ascending order' do
|
434
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
435
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
436
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
437
|
+
TestEntity.index!
|
438
|
+
|
439
|
+
r = TestEntity.find_all(:a => 1, :order => :asc)
|
440
|
+
r.size.should == 2
|
441
|
+
r.first.id.should == 'one'
|
442
|
+
r.second.id.should == 'three'
|
443
|
+
end
|
444
|
+
|
445
|
+
it 'can return results in descending order' do
|
446
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
447
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 2, :b => 2})
|
448
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
449
|
+
TestEntity.index!
|
325
450
|
|
326
|
-
r = TestEntity.find_all
|
327
|
-
r.size.should ==
|
451
|
+
r = TestEntity.find_all(:a => 1, :order => :desc)
|
452
|
+
r.size.should == 2
|
453
|
+
r.first.id.should == 'three'
|
454
|
+
r.second.id.should == 'one'
|
328
455
|
end
|
329
456
|
|
330
|
-
it '
|
331
|
-
|
332
|
-
|
457
|
+
it 'can limit the number of results returned' do
|
458
|
+
TestEntity.create!(:id => 'one', :attributes => {:a => 1, :b => 1})
|
459
|
+
TestEntity.create!(:id => 'two', :attributes => {:a => 1, :b => 2})
|
460
|
+
TestEntity.create!(:id => 'three', :attributes => {:a => 1, :b => 3})
|
461
|
+
TestEntity.index!
|
462
|
+
|
463
|
+
r = TestEntity.find_all(:a => 1, :limit => 2)
|
464
|
+
r.size.should == 2
|
465
|
+
r.first.id.should == 'one'
|
466
|
+
r.second.id.should == 'three'
|
333
467
|
end
|
334
468
|
|
335
469
|
end
|
336
470
|
|
337
|
-
describe '
|
471
|
+
describe '.exists?' do
|
338
472
|
|
339
473
|
it 'returns true if the record exists' do
|
340
474
|
TestEntity.create!(:id => 'one')
|
@@ -347,6 +481,23 @@ describe GitModel::Persistable do
|
|
347
481
|
|
348
482
|
end
|
349
483
|
|
484
|
+
describe ".index!" do
|
485
|
+
it "generates and saves the index" do
|
486
|
+
TestEntity.index.should_receive(:generate!)
|
487
|
+
TestEntity.index.should_receive(:save)
|
488
|
+
TestEntity.index!
|
489
|
+
end
|
490
|
+
end
|
491
|
+
|
492
|
+
describe '.all_values_for_attr' do
|
493
|
+
it 'returns a list of all values that exist for a given attribute' do
|
494
|
+
o = TestEntity.create!(:id => 'first', :attributes => {"a" => 1, "b" => 2})
|
495
|
+
o = TestEntity.create!(:id => 'second', :attributes => {"a" => 3, "b" => 4})
|
496
|
+
TestEntity.index!
|
497
|
+
TestEntity.all_values_for_attr("a").should == [1, 3]
|
498
|
+
end
|
499
|
+
end
|
500
|
+
|
350
501
|
describe '#attributes' do
|
351
502
|
it 'accepts symbols or strings interchangeably as strings' do
|
352
503
|
o = TestEntity.new(:id => 'lol', :attributes => {"one" => 1, :two => 2})
|
data/spec/gitmodel_spec.rb
CHANGED
@@ -2,8 +2,7 @@ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
|
|
2
2
|
|
3
3
|
describe GitModel do
|
4
4
|
|
5
|
-
describe "
|
6
|
-
|
5
|
+
describe ".last_commit" do
|
7
6
|
it "returns nil if there are no commits" do
|
8
7
|
GitModel.last_commit.should == nil
|
9
8
|
end
|
@@ -17,11 +16,9 @@ describe GitModel do
|
|
17
16
|
|
18
17
|
GitModel.last_commit.to_s.should == sha
|
19
18
|
end
|
20
|
-
|
21
19
|
end
|
22
20
|
|
23
|
-
describe "
|
24
|
-
|
21
|
+
describe ".current_tree" do
|
25
22
|
it "returns nil if there are no commits" do
|
26
23
|
GitModel.current_tree.should == nil
|
27
24
|
end
|
@@ -32,7 +29,19 @@ describe GitModel do
|
|
32
29
|
GitModel.should_receive(:last_commit).with('master').and_return(last_commit)
|
33
30
|
GitModel.current_tree('master')
|
34
31
|
end
|
32
|
+
end
|
35
33
|
|
34
|
+
describe ".index" do
|
35
|
+
before(:each) do
|
36
|
+
TestEntity.create!(:id => "foo")
|
37
|
+
TestEntity2.create!(:id => "bar")
|
38
|
+
end
|
39
|
+
|
40
|
+
it "calls index! on each Persistable model class" do
|
41
|
+
TestEntity.should_receive(:index!)
|
42
|
+
TestEntity2.should_receive(:index!)
|
43
|
+
GitModel.index!
|
44
|
+
end
|
36
45
|
end
|
37
46
|
|
38
47
|
end
|
data/spec/spec_helper.rb
CHANGED
@@ -6,7 +6,15 @@ require 'gitmodel'
|
|
6
6
|
|
7
7
|
Dir[File.join(File.dirname(__FILE__), "support/**/*.rb")].each {|f| require f}
|
8
8
|
|
9
|
-
|
9
|
+
RSpec.configure do |c|
|
10
10
|
c.mock_with :rspec
|
11
11
|
end
|
12
12
|
|
13
|
+
class TestEntity
|
14
|
+
include GitModel::Persistable
|
15
|
+
end
|
16
|
+
class TestEntity2
|
17
|
+
include GitModel::Persistable
|
18
|
+
end
|
19
|
+
|
20
|
+
#GitModel.logger.level = ::Logger::DEBUG
|
metadata
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
name: gitmodel
|
3
3
|
version: !ruby/object:Gem::Version
|
4
4
|
prerelease:
|
5
|
-
version: 0.0.
|
5
|
+
version: 0.0.3
|
6
6
|
platform: ruby
|
7
7
|
authors:
|
8
8
|
- Paul Dowman
|
@@ -10,8 +10,7 @@ autorequire:
|
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
12
|
|
13
|
-
date: 2011-
|
14
|
-
default_executable:
|
13
|
+
date: 2011-07-04 00:00:00 Z
|
15
14
|
dependencies:
|
16
15
|
- !ruby/object:Gem::Dependency
|
17
16
|
name: activemodel
|
@@ -58,36 +57,36 @@ dependencies:
|
|
58
57
|
type: :runtime
|
59
58
|
version_requirements: *id004
|
60
59
|
- !ruby/object:Gem::Dependency
|
61
|
-
name:
|
60
|
+
name: yajl-ruby
|
62
61
|
prerelease: false
|
63
62
|
requirement: &id005 !ruby/object:Gem::Requirement
|
64
63
|
none: false
|
65
64
|
requirements:
|
66
65
|
- - ">="
|
67
66
|
- !ruby/object:Gem::Version
|
68
|
-
version:
|
69
|
-
type: :
|
67
|
+
version: 0.8.2
|
68
|
+
type: :runtime
|
70
69
|
version_requirements: *id005
|
71
70
|
- !ruby/object:Gem::Dependency
|
72
|
-
name:
|
71
|
+
name: ZenTest
|
73
72
|
prerelease: false
|
74
73
|
requirement: &id006 !ruby/object:Gem::Requirement
|
75
74
|
none: false
|
76
75
|
requirements:
|
77
76
|
- - ">="
|
78
77
|
- !ruby/object:Gem::Version
|
79
|
-
version: 4.4.
|
78
|
+
version: 4.4.0
|
80
79
|
type: :development
|
81
80
|
version_requirements: *id006
|
82
81
|
- !ruby/object:Gem::Dependency
|
83
|
-
name: autotest
|
82
|
+
name: autotest
|
84
83
|
prerelease: false
|
85
84
|
requirement: &id007 !ruby/object:Gem::Requirement
|
86
85
|
none: false
|
87
86
|
requirements:
|
88
87
|
- - ">="
|
89
88
|
- !ruby/object:Gem::Version
|
90
|
-
version:
|
89
|
+
version: 4.4.1
|
91
90
|
type: :development
|
92
91
|
version_requirements: *id007
|
93
92
|
- !ruby/object:Gem::Dependency
|
@@ -122,14 +121,15 @@ files:
|
|
122
121
|
- gitmodel.gemspec
|
123
122
|
- lib/gitmodel.rb
|
124
123
|
- lib/gitmodel/errors.rb
|
124
|
+
- lib/gitmodel/index.rb
|
125
125
|
- lib/gitmodel/persistable.rb
|
126
126
|
- lib/gitmodel/transaction.rb
|
127
|
+
- spec/gitmodel/index_spec.rb
|
127
128
|
- spec/gitmodel/persistable_spec.rb
|
128
129
|
- spec/gitmodel/transaction_spec.rb
|
129
130
|
- spec/gitmodel_spec.rb
|
130
131
|
- spec/spec_helper.rb
|
131
132
|
- spec/support/setup.rb
|
132
|
-
has_rdoc: true
|
133
133
|
homepage: http://github.com/pauldowman/gitmodel
|
134
134
|
licenses: []
|
135
135
|
|
@@ -153,11 +153,12 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
153
153
|
requirements: []
|
154
154
|
|
155
155
|
rubyforge_project:
|
156
|
-
rubygems_version: 1.5
|
156
|
+
rubygems_version: 1.8.5
|
157
157
|
signing_key:
|
158
158
|
specification_version: 3
|
159
159
|
summary: An ActiveModel-compliant persistence framework for Ruby that uses Git for versioning and remote syncing.
|
160
160
|
test_files:
|
161
|
+
- spec/gitmodel/index_spec.rb
|
161
162
|
- spec/gitmodel/persistable_spec.rb
|
162
163
|
- spec/gitmodel/transaction_spec.rb
|
163
164
|
- spec/gitmodel_spec.rb
|