greyscale_record 0.0.1 → 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +79 -9
- data/greyscale_record.gemspec +2 -2
- data/lib/greyscale_record/base.rb +4 -24
- data/lib/greyscale_record/cacheable.rb +1 -1
- data/lib/greyscale_record/data_store/engine.rb +66 -0
- data/lib/greyscale_record/data_store/index.rb +25 -0
- data/lib/greyscale_record/data_store/store.rb +72 -0
- data/lib/greyscale_record/data_store/table.rb +80 -0
- data/lib/greyscale_record/data_store.rb +8 -0
- data/lib/greyscale_record/drivers/base.rb +19 -17
- data/lib/greyscale_record/drivers/yaml.rb +6 -9
- data/lib/greyscale_record/errors.rb +3 -0
- data/lib/greyscale_record/indexable.rb +3 -22
- data/lib/greyscale_record/queriable.rb +9 -51
- data/lib/greyscale_record/relation.rb +29 -0
- data/lib/greyscale_record/version.rb +1 -1
- data/lib/greyscale_record.rb +4 -1
- metadata +22 -3
- data/lib/greyscale_record/index.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e182b351304fb45824511ff704bae0fb071aea01
|
4
|
+
data.tar.gz: 147b67b9aca64b01243585e57cd21d11303a83ca
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fcd11e5ee95ec0f0630e32b65b20c5e9b01af69b2903aeb481b583ed4687de64b350d14b28b6d6e6f9d8c0d67113115715c847a610546a1fa61bb3ce7a36cdf2
|
7
|
+
data.tar.gz: da33bc49c25147f838485b784a843bbba247243b2f20c5af1eac74cef47af4cd7af556a5a9541d983fa40d0b8f116e0f39ae4693ca4ae63ea3db11becc58e080
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
# Greyscale Record
|
2
2
|
|
3
|
-
`GreyscaleRecord` is a
|
3
|
+
`GreyscaleRecord` is a flat-file ORM, designed for users whose data is perfectly static and is stored in a flat format (e.g. yaml files). It is a clone of [YamlBSides](https://github.com/gaorlov/yaml_b_sides), but is extended to support multiple backend drivers (YAML files, JSON APIs)
|
4
4
|
|
5
5
|
## Installation
|
6
6
|
|
@@ -46,12 +46,6 @@ Note: `Greyscale Record` expects your class names to match the fixture names (e.
|
|
46
46
|
|
47
47
|
Your `Person` class now responds to
|
48
48
|
|
49
|
-
### Query Methods
|
50
|
-
|
51
|
-
* `all` : will give you all of the records in the table
|
52
|
-
* `first` : wil return the first record in the table
|
53
|
-
* `find( id )` : will find a single record with the specified yaml key
|
54
|
-
* `find_by( properties = {} )` : will find all the recored that match all the proerties in the hash
|
55
49
|
|
56
50
|
### Indexing
|
57
51
|
|
@@ -153,6 +147,82 @@ Associations have some of the standard ActiveRecord options. Namely:
|
|
153
147
|
#...
|
154
148
|
```
|
155
149
|
|
150
|
+
### Query Methods
|
151
|
+
|
152
|
+
* `all` : will give you all of the records in the table
|
153
|
+
* `first` : wil return the first record in the table
|
154
|
+
* `find( id )` : will find a single record with the specified yaml key
|
155
|
+
* `find_by( properties = {} )` : will find all the recored that match all the proerties in the hash
|
156
|
+
|
157
|
+
#### Relation methods
|
158
|
+
|
159
|
+
These act as you would expect them to in ActiveRecord. These can be chained and applied to associations
|
160
|
+
* `where( params )` : will return a relation which can be interacted with as if it were the resulting array
|
161
|
+
* `and( params )` : identical to `where`, but nicer to read
|
162
|
+
|
163
|
+
```ruby
|
164
|
+
Person.where( url_slug: "greg" ) #=> [<Person>]
|
165
|
+
Person.where( url_slug: "greg", name: "Greg Orlov" ).and( id: "greg") #=> [<Person>]
|
166
|
+
Person.where( url_slug: "greg", name: "Greg Orlov" ).where( id: "greg") #=> [<Person>]
|
167
|
+
# from the code above
|
168
|
+
Person.first.images.where( some_property: "value" )
|
169
|
+
```
|
170
|
+
|
171
|
+
### Data Sourcing
|
172
|
+
|
173
|
+
As mentioned in the summary above, `GreyscaleRecord` can be connected to different data sources. The structure that we use is a `Driver`.
|
174
|
+
There is a built in driver to use as an example: Yaml Driver. This is the structure that controls data flow from the original data store,
|
175
|
+
such as YAML files or an API, and formats it to be consumed by the internal `GreyscaleRecord::DataStore::Store` object, which expects the
|
176
|
+
result set to look roughly like
|
177
|
+
|
178
|
+
```
|
179
|
+
{ table_name: {
|
180
|
+
record_id: { attribute: value, ... },
|
181
|
+
...
|
182
|
+
},
|
183
|
+
...
|
184
|
+
}
|
185
|
+
```
|
186
|
+
|
187
|
+
`GreyscaleRecord` will populate the id field for you from the record keys.
|
188
|
+
|
189
|
+
### Data Patching
|
190
|
+
|
191
|
+
If you have a data set that you want to temporarily augment, you can apply a [JSON patch](http://jsonpatch.com/) to the data store.
|
192
|
+
This will apply the patch within the context of your current thread until it is removed
|
193
|
+
|
194
|
+
```ruby
|
195
|
+
patch = ::Hana::Patch.new [ { 'op' => 'add', 'path' => '/people/mike', 'value' => { id: "mike", name: "Mike Uchman" } } ]
|
196
|
+
|
197
|
+
# this will stick around indefinitely in this thread
|
198
|
+
data_store.apply_patch patch
|
199
|
+
|
200
|
+
Person.find( 'mike' ).name # => "Mike Uchman"
|
201
|
+
Person.where( id: 'mike' ).first.name #=> "Mike Uchman"
|
202
|
+
|
203
|
+
# let's go back to the original set
|
204
|
+
data_store.remove_patch
|
205
|
+
|
206
|
+
Person.where( id: "mike" ) #=> []
|
207
|
+
```
|
208
|
+
|
209
|
+
Or, if you are doing something simple and can fir your code in a single block, you can do:
|
210
|
+
|
211
|
+
```ruby
|
212
|
+
data_store.with_patch patch do
|
213
|
+
|
214
|
+
Person.find( 'mike' ).name # => "Mike Uchman"
|
215
|
+
Person.where( id: 'mike' ).first.name #=> "Mike Uchman"
|
216
|
+
|
217
|
+
end
|
218
|
+
|
219
|
+
Person.where( id: "mike" ) #=> []
|
220
|
+
```
|
221
|
+
|
222
|
+
__NOTE__: The patch interface that `Store` expects is that of [Hana](https://github.com/tenderlove/hana). You don't have to use it, but it has
|
223
|
+
to respond to `patch.apply( doc )`.
|
224
|
+
|
225
|
+
|
156
226
|
### Example
|
157
227
|
|
158
228
|
To use the `People` class from earlier, a fully fleshed out model would look something like:
|
@@ -198,8 +268,8 @@ The setup is pretty straightforward. Greyscale Record wants a logger and a base
|
|
198
268
|
```ruby
|
199
269
|
GreyscaleRecord::logger = Rails.logger
|
200
270
|
# for now this is the only driver
|
201
|
-
|
202
|
-
GreyscaleRecord::
|
271
|
+
yaml_driver = GreyscaleRecord::Drivers::Yaml.new File.expand_path("./db/fixtures", File.dirname(__FILE__))
|
272
|
+
GreyscaleRecord::Base.data_store = GreyscaleRecord::DataStore::Engine.new(yaml_driver)
|
203
273
|
|
204
274
|
|
205
275
|
# in development.rb
|
data/greyscale_record.gemspec
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# coding: utf-8
|
1
|
+
# coding: utf-8
|
2
2
|
lib = File.expand_path('../lib', __FILE__)
|
3
3
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
4
|
require 'greyscale_record/version'
|
@@ -30,5 +30,5 @@ Gem::Specification.new do |spec|
|
|
30
30
|
spec.add_development_dependency "minitest", "~> 5.0"
|
31
31
|
spec.add_development_dependency "simplecov"
|
32
32
|
spec.add_development_dependency "m"
|
33
|
-
|
33
|
+
spec.add_development_dependency "hana"
|
34
34
|
end
|
@@ -8,40 +8,20 @@ module GreyscaleRecord
|
|
8
8
|
include Instanceable
|
9
9
|
include Queriable
|
10
10
|
|
11
|
-
class_attribute :
|
12
|
-
class_attribute :driver
|
11
|
+
class_attribute :data_store
|
13
12
|
|
14
13
|
class << self
|
15
14
|
|
16
15
|
def load!
|
17
|
-
|
18
|
-
return unless @data
|
19
|
-
|
20
|
-
# let's preemptively index by id so that when we do a find_by id:, or a where id: it won't table scan
|
21
|
-
idify_data!
|
22
|
-
|
23
|
-
index :id unless GreyscaleRecord.live_reload
|
16
|
+
data_store.add_table name
|
24
17
|
end
|
25
18
|
|
26
19
|
def inherited(subclass)
|
27
20
|
subclass.load!
|
28
21
|
end
|
29
22
|
|
30
|
-
|
31
|
-
|
32
|
-
def _class_name
|
33
|
-
self.name.pluralize.downcase
|
34
|
-
end
|
35
|
-
|
36
|
-
def idify_data!
|
37
|
-
@data.each do |k, v|
|
38
|
-
v[:id] = k
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
def data
|
43
|
-
load! if GreyscaleRecord.live_reload
|
44
|
-
@data
|
23
|
+
def name
|
24
|
+
self.to_s.pluralize.downcase
|
45
25
|
end
|
46
26
|
end
|
47
27
|
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
module DataStore
|
3
|
+
class Engine
|
4
|
+
|
5
|
+
# A data store can only have one driver.
|
6
|
+
# It's like a database connection. If you want models to connect to different
|
7
|
+
# databases, you have them inherit from different base classes that specify
|
8
|
+
# a db to connect to.
|
9
|
+
|
10
|
+
# Pros:
|
11
|
+
# * All the data in your store comes from the same place. Easy to reason about
|
12
|
+
# * No chance of data source collisions. Can't populate tables from multiple sources
|
13
|
+
# * Cleaner table-adding interface
|
14
|
+
# store.add_table( name )
|
15
|
+
# vs
|
16
|
+
# store.add_table( name, driver )
|
17
|
+
# # where does driver get initialized?
|
18
|
+
# * Straight forward patching: one complete patch at a time
|
19
|
+
# How do you patch across differntly driven tables?
|
20
|
+
# Do you need a patch stack?
|
21
|
+
# * Depending on how your remote is built, you can pull in all the data at once
|
22
|
+
#
|
23
|
+
# Cons:
|
24
|
+
# * Limiting if you want to have some tables come from YAML and some from remote
|
25
|
+
#
|
26
|
+
# Yeah. OK.
|
27
|
+
|
28
|
+
delegate :apply_patch, :remove_patch, :with_patch, :table, to: :store
|
29
|
+
|
30
|
+
def initialize( driver )
|
31
|
+
@driver = driver
|
32
|
+
@store = Store.new
|
33
|
+
end
|
34
|
+
|
35
|
+
# Read only store. No writes allowed.
|
36
|
+
|
37
|
+
def find( options = {} )
|
38
|
+
table = store.table( options.delete(:_table) )
|
39
|
+
if GreyscaleRecord.live_reload
|
40
|
+
load_table!( table )
|
41
|
+
end
|
42
|
+
|
43
|
+
# TODO: this is where all the meat is
|
44
|
+
table.find options
|
45
|
+
end
|
46
|
+
|
47
|
+
def add_table( name )
|
48
|
+
load_table! name
|
49
|
+
end
|
50
|
+
|
51
|
+
def add_index( name, column )
|
52
|
+
store.table( name ).add_index( column )
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def store
|
58
|
+
@store
|
59
|
+
end
|
60
|
+
|
61
|
+
def load_table!( name )
|
62
|
+
store.init_table name, @driver.load!( name )
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
module DataStore
|
3
|
+
class Index
|
4
|
+
def initialize(field, data)
|
5
|
+
@indexed_data = {}
|
6
|
+
data.each do |id, datum|
|
7
|
+
key = datum[field]
|
8
|
+
|
9
|
+
# storing key => array of matching ids
|
10
|
+
@indexed_data[key] = Array(@indexed_data[key]) + [id]
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
# returns ids
|
15
|
+
def find(values)
|
16
|
+
# find all the arrays of ids for the values,
|
17
|
+
# get rid of nils (value not present),
|
18
|
+
# and compact for a single array result
|
19
|
+
values.map do |value|
|
20
|
+
@indexed_data[value]
|
21
|
+
end.compact.flatten
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
module DataStore
|
3
|
+
class Store
|
4
|
+
def initialize
|
5
|
+
@data = {}
|
6
|
+
@tables = {}
|
7
|
+
end
|
8
|
+
|
9
|
+
def []( name )
|
10
|
+
data[ name ]
|
11
|
+
end
|
12
|
+
|
13
|
+
def table( name )
|
14
|
+
unless @tables[name]
|
15
|
+
raise GreyscaleRecord::Errors::DataStoreError, "Data Store error: table '#{name}' does not exist"
|
16
|
+
end
|
17
|
+
|
18
|
+
@tables[name]
|
19
|
+
end
|
20
|
+
|
21
|
+
def init_table( name, rows )
|
22
|
+
@data[name] = rows
|
23
|
+
@tables[name] = Table.new( name, self )
|
24
|
+
end
|
25
|
+
|
26
|
+
def with_patch( patch )
|
27
|
+
apply_patch patch
|
28
|
+
yield
|
29
|
+
remove_patch
|
30
|
+
end
|
31
|
+
|
32
|
+
# This only allows for one patch at a time.
|
33
|
+
# Is there ever a case when we would need, like a stack of these things?
|
34
|
+
# I don't think so?
|
35
|
+
|
36
|
+
def apply_patch( patch )
|
37
|
+
Thread.current[patch_key] = patched_data patch
|
38
|
+
end
|
39
|
+
|
40
|
+
def remove_patch
|
41
|
+
Thread.current[patch_key] = nil
|
42
|
+
end
|
43
|
+
|
44
|
+
def patched?
|
45
|
+
Thread.current[patch_key].present?
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
def patched_data(patch)
|
51
|
+
unless patch.respond_to? :apply
|
52
|
+
raise GreyscaleRecord::Errors::DataStoreError, "Data Store Error: apply_patch: patch must respond to 'apply(doc)'."
|
53
|
+
end
|
54
|
+
|
55
|
+
patch.apply( @data.deep_dup )
|
56
|
+
end
|
57
|
+
|
58
|
+
def patch_key
|
59
|
+
@key ||= "#{object_id}_patch"
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
def data
|
64
|
+
if patched?
|
65
|
+
Thread.current[patch_key]
|
66
|
+
else
|
67
|
+
@data
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,80 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
module DataStore
|
3
|
+
class Table
|
4
|
+
|
5
|
+
def initialize(name, store)
|
6
|
+
@name = name
|
7
|
+
@store = store
|
8
|
+
|
9
|
+
# initialize the index array for later use
|
10
|
+
@indices = {}
|
11
|
+
|
12
|
+
# generate IDs for the records based on YAML keys
|
13
|
+
generate_ids!
|
14
|
+
|
15
|
+
# preemptively index the IDs
|
16
|
+
add_index :id
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
rows.values
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_index( column )
|
24
|
+
return if @store.patched?
|
25
|
+
@indices = @indices.merge( { column => Index.new(column, rows) } )
|
26
|
+
end
|
27
|
+
|
28
|
+
def find( params = {} )
|
29
|
+
return all if params.empty?
|
30
|
+
sets = params.map do | column, values |
|
31
|
+
if !patched? && indexed?( column )
|
32
|
+
find_in_index column, values
|
33
|
+
else
|
34
|
+
GreyscaleRecord.logger.warn "You are running a query on #{@name}.#{column} which is not indexed. This will perform a table scan."
|
35
|
+
find_in_column column, values
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
sets.inject( sets.first ) do |result, subset|
|
40
|
+
result & subset
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def rows
|
47
|
+
@store[@name]
|
48
|
+
end
|
49
|
+
|
50
|
+
def patched?
|
51
|
+
@store.patched?
|
52
|
+
end
|
53
|
+
|
54
|
+
def indexed?(column)
|
55
|
+
@indices[column].present?
|
56
|
+
end
|
57
|
+
|
58
|
+
def find_in_column( column, values )
|
59
|
+
rows.values.select do |datum|
|
60
|
+
Array( values ).include? datum[ column ]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def find_in_index( column, values )
|
65
|
+
keys = @indices[column].find( Array( values ) )
|
66
|
+
|
67
|
+
keys.map do |id|
|
68
|
+
rows[id]
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_ids!
|
73
|
+
# init IDs
|
74
|
+
rows.each do |k, v|
|
75
|
+
v[:id] = k
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
@@ -0,0 +1,8 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
module DataStore
|
3
|
+
autoload :Engine, 'greyscale_record/data_store/engine'
|
4
|
+
autoload :Index, 'greyscale_record/data_store/index'
|
5
|
+
autoload :Store, 'greyscale_record/data_store/store'
|
6
|
+
autoload :Table, 'greyscale_record/data_store/table'
|
7
|
+
end
|
8
|
+
end
|
@@ -2,29 +2,31 @@ module GreyscaleRecord
|
|
2
2
|
module Drivers
|
3
3
|
class Base
|
4
4
|
|
5
|
-
|
5
|
+
attr_reader :root
|
6
|
+
|
7
|
+
def initialize( root )
|
8
|
+
@root = root
|
9
|
+
end
|
6
10
|
|
7
|
-
|
8
|
-
def load!(class_name)
|
11
|
+
def load!(object)
|
9
12
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
13
|
+
raise GreyscaleRecord::Errors::DriverError, "driver needs to define a `root`" unless root
|
14
|
+
|
15
|
+
data = load_data(object)
|
16
|
+
|
17
|
+
GreyscaleRecord.logger.info "#{object} successfully loaded data"
|
15
18
|
|
16
|
-
|
19
|
+
data
|
17
20
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
21
|
+
rescue => e
|
22
|
+
GreyscaleRecord.logger.error "#{self.class} failed to load data for #{object}: #{e}`"
|
23
|
+
{}
|
24
|
+
end
|
22
25
|
|
23
|
-
|
26
|
+
private
|
24
27
|
|
25
|
-
|
26
|
-
|
27
|
-
end
|
28
|
+
def load_data
|
29
|
+
raise NotImplementedError, "load_data is not implemented"
|
28
30
|
end
|
29
31
|
end
|
30
32
|
end
|
@@ -2,17 +2,14 @@ module GreyscaleRecord
|
|
2
2
|
module Drivers
|
3
3
|
class Yaml < Base
|
4
4
|
|
5
|
-
|
5
|
+
private
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
YAML.load_file( data_file( class_name ) ).with_indifferent_access
|
11
|
-
end
|
7
|
+
def load_data( object )
|
8
|
+
YAML.load_file( data_file( object ) ).with_indifferent_access
|
9
|
+
end
|
12
10
|
|
13
|
-
|
14
|
-
|
15
|
-
end
|
11
|
+
def data_file( object )
|
12
|
+
[root, "#{object}.yml"].compact.join("/")
|
16
13
|
end
|
17
14
|
end
|
18
15
|
end
|
@@ -3,31 +3,12 @@ module GreyscaleRecord
|
|
3
3
|
extend ActiveSupport::Concern
|
4
4
|
|
5
5
|
included do
|
6
|
-
class_attribute :__indices
|
7
|
-
self.__indices = { }
|
8
|
-
|
9
6
|
class << self
|
7
|
+
# DEPRICATED
|
8
|
+
# TODO: remove
|
10
9
|
def index(field)
|
11
10
|
return if GreyscaleRecord.live_reload
|
12
|
-
|
13
|
-
end
|
14
|
-
|
15
|
-
def find_in_index(field, values)
|
16
|
-
keys = Array(index_for(field).find(values))
|
17
|
-
|
18
|
-
keys.map do |id|
|
19
|
-
data[id]
|
20
|
-
end
|
21
|
-
end
|
22
|
-
|
23
|
-
def indexed?(field)
|
24
|
-
__indices[field].present?
|
25
|
-
end
|
26
|
-
|
27
|
-
protected
|
28
|
-
|
29
|
-
def index_for(field)
|
30
|
-
__indices[field]
|
11
|
+
data_store.add_index( name, field )
|
31
12
|
end
|
32
13
|
end
|
33
14
|
end
|
@@ -5,69 +5,27 @@ module GreyscaleRecord
|
|
5
5
|
included do
|
6
6
|
class << self
|
7
7
|
def find(id)
|
8
|
-
|
9
|
-
raise Errors::RecordNotFound, "#{self}: Record not found: #{id}"
|
10
|
-
|
8
|
+
records = where( id: id.to_s )
|
9
|
+
raise Errors::RecordNotFound, "#{ self }: Record not found: #{ id }" if records.empty?
|
10
|
+
records.first
|
11
11
|
end
|
12
12
|
|
13
|
-
def find_by(params = {})
|
13
|
+
def find_by( params = { } )
|
14
14
|
results = where params
|
15
|
-
raise Errors::RecordNotFound, "#{self}: Could not find record that matches: #{params.inspect}" if results.empty?
|
15
|
+
raise Errors::RecordNotFound, "#{ self }: Could not find record that matches: #{ params.inspect }" if results.empty?
|
16
16
|
results.first
|
17
17
|
end
|
18
18
|
|
19
19
|
def all
|
20
|
-
|
21
|
-
new obj
|
22
|
-
end
|
20
|
+
where
|
23
21
|
end
|
24
22
|
|
25
23
|
def first
|
26
|
-
|
24
|
+
all.first
|
27
25
|
end
|
28
26
|
|
29
|
-
|
30
|
-
|
31
|
-
if all_indexed?(params.keys)
|
32
|
-
results = find_by_indexed(params)
|
33
|
-
else
|
34
|
-
results = find_by_scan(params)
|
35
|
-
end
|
36
|
-
results.map do |result|
|
37
|
-
new result
|
38
|
-
end
|
39
|
-
end
|
40
|
-
|
41
|
-
private
|
42
|
-
|
43
|
-
def find_by_scan(params)
|
44
|
-
data.values.select do |datum|
|
45
|
-
params.all? do |param, expected_value|
|
46
|
-
val = Array(expected_value).include? datum[param]
|
47
|
-
end
|
48
|
-
end
|
49
|
-
end
|
50
|
-
|
51
|
-
def find_by_indexed(params)
|
52
|
-
sets = []
|
53
|
-
params.each do |index, values|
|
54
|
-
sets << find_in_index(index, Array(values))
|
55
|
-
end
|
56
|
-
|
57
|
-
# find the intersection of all the sets
|
58
|
-
sets.inject( sets.first ) do |result, subset|
|
59
|
-
result & subset
|
60
|
-
end
|
61
|
-
end
|
62
|
-
|
63
|
-
def all_indexed?(fields)
|
64
|
-
fields.all? do |field|
|
65
|
-
indexed = indexed? field
|
66
|
-
unless indexed
|
67
|
-
GreyscaleRecord.logger.warn "You are running a query on #{self}.#{field} which is not indexed. This will perform a table scan."
|
68
|
-
end
|
69
|
-
indexed
|
70
|
-
end
|
27
|
+
def where( params = {} )
|
28
|
+
Relation.new self, params
|
71
29
|
end
|
72
30
|
end
|
73
31
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module GreyscaleRecord
|
2
|
+
class Relation
|
3
|
+
|
4
|
+
delegate :present?, :empty?, :==, to: :all
|
5
|
+
|
6
|
+
def initialize( base, params )
|
7
|
+
@base = base
|
8
|
+
@params = params.dup.merge!( _table: @base.name )
|
9
|
+
end
|
10
|
+
|
11
|
+
def where( params )
|
12
|
+
self.class.new @base, @params.merge( params )
|
13
|
+
end
|
14
|
+
|
15
|
+
def and( params )
|
16
|
+
self.class.new @base, @params.merge( params )
|
17
|
+
end
|
18
|
+
|
19
|
+
def all
|
20
|
+
@all ||= @base.data_store.find( @params.dup ).map do | result |
|
21
|
+
@base.new result
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def method_missing( method, *args, &block )
|
26
|
+
all.send method, *args, &block
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
data/lib/greyscale_record.rb
CHANGED
@@ -5,6 +5,8 @@ require 'active_support'
|
|
5
5
|
require 'active_support/concern'
|
6
6
|
require 'active_support/core_ext/class/attribute'
|
7
7
|
require 'active_support/core_ext/hash'
|
8
|
+
require 'active_support/core_ext/hash/keys'
|
9
|
+
require 'active_support/core_ext/object/deep_dup'
|
8
10
|
require 'yaml'
|
9
11
|
|
10
12
|
module GreyscaleRecord
|
@@ -12,13 +14,14 @@ module GreyscaleRecord
|
|
12
14
|
autoload :Associations, 'greyscale_record/associations'
|
13
15
|
autoload :Base, 'greyscale_record/base'
|
14
16
|
autoload :Cacheable, 'greyscale_record/cacheable'
|
17
|
+
autoload :DataStore, 'greyscale_record/data_store'
|
15
18
|
autoload :Drivers, 'greyscale_record/drivers'
|
16
19
|
autoload :Errors, 'greyscale_record/errors'
|
17
20
|
autoload :Instanceable, 'greyscale_record/instanceable'
|
18
|
-
autoload :Index, 'greyscale_record/index'
|
19
21
|
autoload :Indexable, 'greyscale_record/indexable'
|
20
22
|
autoload :Propertiable, 'greyscale_record/propertiable'
|
21
23
|
autoload :Queriable, 'greyscale_record/queriable'
|
24
|
+
autoload :Relation, 'greyscale_record/relation'
|
22
25
|
|
23
26
|
class << self
|
24
27
|
attr_accessor :live_reload
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: greyscale_record
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Greg Orlov
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2017-
|
11
|
+
date: 2017-07-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activesupport
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - ">="
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: hana
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
111
125
|
description: An ActiveRecord-like interface for reading from & queryeing flat data
|
112
126
|
interfaces
|
113
127
|
email:
|
@@ -136,15 +150,20 @@ files:
|
|
136
150
|
- lib/greyscale_record/associations/hasable.rb
|
137
151
|
- lib/greyscale_record/base.rb
|
138
152
|
- lib/greyscale_record/cacheable.rb
|
153
|
+
- lib/greyscale_record/data_store.rb
|
154
|
+
- lib/greyscale_record/data_store/engine.rb
|
155
|
+
- lib/greyscale_record/data_store/index.rb
|
156
|
+
- lib/greyscale_record/data_store/store.rb
|
157
|
+
- lib/greyscale_record/data_store/table.rb
|
139
158
|
- lib/greyscale_record/drivers.rb
|
140
159
|
- lib/greyscale_record/drivers/base.rb
|
141
160
|
- lib/greyscale_record/drivers/yaml.rb
|
142
161
|
- lib/greyscale_record/errors.rb
|
143
|
-
- lib/greyscale_record/index.rb
|
144
162
|
- lib/greyscale_record/indexable.rb
|
145
163
|
- lib/greyscale_record/instanceable.rb
|
146
164
|
- lib/greyscale_record/propertiable.rb
|
147
165
|
- lib/greyscale_record/queriable.rb
|
166
|
+
- lib/greyscale_record/relation.rb
|
148
167
|
- lib/greyscale_record/version.rb
|
149
168
|
homepage: https://github.com/greyscale-io/greyscale_record
|
150
169
|
licenses:
|
@@ -1,23 +0,0 @@
|
|
1
|
-
module GreyscaleRecord
|
2
|
-
class Index
|
3
|
-
def initialize(field, data)
|
4
|
-
@indexed_data = {}
|
5
|
-
data.each do |id, datum|
|
6
|
-
key = datum[field]
|
7
|
-
|
8
|
-
# storing key => array of matching ids
|
9
|
-
@indexed_data[key] = Array(@indexed_data[key]) + [id]
|
10
|
-
end
|
11
|
-
end
|
12
|
-
|
13
|
-
# returns ids
|
14
|
-
def find(values)
|
15
|
-
# find all the arrays of ids for the values,
|
16
|
-
# get rid of nils (value not present),
|
17
|
-
# and compact for a single array result
|
18
|
-
values.map do |value|
|
19
|
-
@indexed_data[value]
|
20
|
-
end.compact.flatten
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|