greyscale_record 0.0.1 → 1.0.0
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.
- 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
|