superstore 1.2.0 → 2.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/.travis.yml +6 -13
- data/CHANGELOG.md +16 -0
- data/Gemfile +0 -5
- data/README.md +15 -33
- data/lib/superstore/adapters/jsonb_adapter.rb +245 -0
- data/lib/superstore/associations/association.rb +38 -0
- data/lib/superstore/associations/belongs_to.rb +35 -0
- data/lib/superstore/associations/builder/association.rb +38 -0
- data/lib/superstore/associations/builder/belongs_to.rb +7 -0
- data/lib/superstore/associations/builder/has_many.rb +7 -0
- data/lib/superstore/associations/builder/has_one.rb +7 -0
- data/lib/superstore/associations/has_many.rb +26 -0
- data/lib/superstore/associations/has_one.rb +24 -0
- data/lib/superstore/associations/reflection.rb +65 -0
- data/lib/superstore/associations.rb +72 -0
- data/lib/superstore/attribute_methods/definition.rb +5 -10
- data/lib/superstore/attribute_methods/dirty.rb +12 -2
- data/lib/superstore/attribute_methods/typecasting.rb +6 -12
- data/lib/superstore/base.rb +3 -4
- data/lib/superstore/connection.rb +3 -5
- data/lib/superstore/core.rb +0 -5
- data/lib/superstore/model.rb +32 -33
- data/lib/superstore/persistence.rb +4 -10
- data/lib/superstore/railtie.rb +2 -20
- data/lib/superstore/scope/batches.rb +17 -22
- data/lib/superstore/scope/finder_methods.rb +33 -35
- data/lib/superstore/scope/query_methods.rb +38 -44
- data/lib/superstore/scope.rb +24 -0
- data/lib/superstore/type.rb +3 -3
- data/lib/superstore/types/array_type.rb +2 -9
- data/lib/superstore/types/base_type.rb +4 -7
- data/lib/superstore/types/boolean_type.rb +2 -1
- data/lib/superstore/types/float_type.rb +6 -5
- data/lib/superstore/types/integer_type.rb +3 -3
- data/lib/superstore/types/json_type.rb +0 -21
- data/lib/superstore.rb +16 -5
- data/superstore.gemspec +2 -1
- data/test/support/jsonb.rb +8 -0
- data/test/support/{issue.rb → models.rb} +9 -0
- data/test/support/pg.rb +11 -15
- data/test/test_helper.rb +7 -6
- data/test/unit/{belongs_to_test.rb → associations/belongs_to_test.rb} +1 -10
- data/test/unit/associations/has_many_test.rb +13 -0
- data/test/unit/associations/has_one_test.rb +14 -0
- data/test/unit/{belongs_to → associations}/reflection_test.rb +2 -2
- data/test/unit/attribute_methods/definition_test.rb +6 -3
- data/test/unit/attribute_methods/dirty_test.rb +17 -14
- data/test/unit/attribute_methods/typecasting_test.rb +0 -14
- data/test/unit/base_test.rb +3 -3
- data/test/unit/connection_test.rb +0 -4
- data/test/unit/persistence_test.rb +4 -4
- data/test/unit/schema_test.rb +9 -17
- data/test/unit/scope/query_methods_test.rb +10 -1
- data/test/unit/types/array_type_test.rb +12 -10
- data/test/unit/types/base_type_test.rb +2 -10
- data/test/unit/types/boolean_type_test.rb +15 -13
- data/test/unit/types/date_type_test.rb +3 -3
- data/test/unit/types/float_type_test.rb +14 -7
- data/test/unit/types/integer_type_test.rb +11 -9
- data/test/unit/types/json_type_test.rb +0 -23
- data/test/unit/types/string_type_test.rb +6 -6
- data/test/unit/types/time_type_test.rb +7 -7
- metadata +35 -26
- data/CHANGELOG +0 -0
- data/lib/superstore/adapters/cassandra_adapter.rb +0 -203
- data/lib/superstore/adapters/hstore_adapter.rb +0 -170
- data/lib/superstore/belongs_to/association.rb +0 -65
- data/lib/superstore/belongs_to/builder.rb +0 -40
- data/lib/superstore/belongs_to/reflection.rb +0 -38
- data/lib/superstore/belongs_to.rb +0 -63
- data/lib/superstore/cassandra_schema/statements.rb +0 -52
- data/lib/superstore/cassandra_schema/tasks.rb +0 -47
- data/lib/superstore/cassandra_schema.rb +0 -9
- data/lib/superstore/log_subscriber.rb +0 -44
- data/lib/superstore/railties/controller_runtime.rb +0 -45
- data/lib/superstore/tasks/ks.rake +0 -59
- data/test/support/cassandra.rb +0 -46
- data/test/support/hstore.rb +0 -24
- data/test/support/user.rb +0 -2
- data/test/unit/cassandra_schema/statements_test.rb +0 -47
- data/test/unit/cassandra_schema/tasks_test.rb +0 -31
- data/test/unit/log_subscriber_test.rb +0 -26
- data/test/unit/railties/controller_runtime_test.rb +0 -48
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2e09c0c96fee4aa9bbe63b30515cea262876069d
|
4
|
+
data.tar.gz: f045d35fdb783ec088617d8b3e1fff9c32c26394
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d479634ee3bf4b165a495038f19e8c0ba50ea2f4bbcba6674306d71d2cd3142266a22e4971de3af4c2af7241cbcfe2147425a0c4b2bcb76f6e2f6eaef64a9bd
|
7
|
+
data.tar.gz: 972a07eeb6a39fbd07a7cfef12203d23a34dd1a616e87434e8d5b3b5b1995872c237256d47165e7f3a48dfd55dd57b185bebd1d55982c76c9d6e59d1d60cf37f
|
data/.travis.yml
CHANGED
@@ -1,16 +1,9 @@
|
|
1
1
|
language: ruby
|
2
2
|
rvm:
|
3
|
-
-
|
4
|
-
- 2.
|
5
|
-
- 2.
|
6
|
-
- 2.2.0
|
3
|
+
- 2.0.0-p647
|
4
|
+
- 2.1.7
|
5
|
+
- 2.2.3
|
7
6
|
sudo: false
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
- KS_VERSION=1.2.19
|
12
|
-
- KS_NAME=apache-cassandra-$KS_VERSION
|
13
|
-
before_install:
|
14
|
-
- if [[ ! -f $KS_NAME/conf/cassandra.yaml ]]; then wget http://archive.apache.org/dist/cassandra/$KS_VERSION/$KS_NAME-bin.tar.gz; tar xf $KS_NAME-bin.tar.gz; sh -c "echo 'JVM_OPTS=\"\${JVM_OPTS} -Xss256k -Djava.net.preferIPv4Stack=false\"' >> $KS_NAME/conf/cassandra-env.sh"; fi
|
15
|
-
- sed -i -e 's:/var/.*\?/cassandra:/tmp/cassandra-store:g' $KS_NAME/conf/{cassandra.yaml,log4j-server.properties}
|
16
|
-
- (cd $KS_NAME && ./bin/cassandra 2>&1 >> cassandra.log)
|
7
|
+
cache: bundler
|
8
|
+
addons:
|
9
|
+
postgresql: '9.4'
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# 2.0.0
|
2
|
+
|
3
|
+
Released YYYY-MM-DD.
|
4
|
+
|
5
|
+
## New Features
|
6
|
+
|
7
|
+
* **Added Postgres JSONB adapter.** (#9)
|
8
|
+
* Added support for `has_many` association. (#13)
|
9
|
+
* Added support for `has_one` association. (#14)
|
10
|
+
|
11
|
+
## Breaking Changes
|
12
|
+
|
13
|
+
* **Removed Cassandra and Postgres hstore adapters.** (#12)
|
14
|
+
* `find_each` now uses SQL cursors, so it's not constrained by the limitations of ActiveRecord's
|
15
|
+
implementation. (#11)
|
16
|
+
* Default values have been re-implemented correctly. (#15)
|
data/Gemfile
CHANGED
@@ -2,7 +2,6 @@ source "http://rubygems.org"
|
|
2
2
|
gemspec
|
3
3
|
|
4
4
|
gem 'rake'
|
5
|
-
gem 'thin'
|
6
5
|
|
7
6
|
group :test do
|
8
7
|
gem 'rails'
|
@@ -10,7 +9,3 @@ group :test do
|
|
10
9
|
gem 'activerecord', '~> 4.2.0'
|
11
10
|
gem 'mocha', require: false
|
12
11
|
end
|
13
|
-
|
14
|
-
group :cassandra do
|
15
|
-
gem 'cassandra-cql', "1.1.4"
|
16
|
-
end
|
data/README.md
CHANGED
@@ -1,17 +1,15 @@
|
|
1
1
|
# Superstore
|
2
|
-
[](http://travis-ci.org/data-axle/superstore) [](http://travis-ci.org/data-axle/superstore) [](https://codeclimate.com/github/data-axle/superstore)
|
3
3
|
|
4
|
-
|
4
|
+
Superstore uses ActiveModel to mimic much of the behavior in ActiveRecord.
|
5
5
|
|
6
6
|
## Installation
|
7
7
|
|
8
|
-
Add the following to
|
8
|
+
Add the following to the `Gemfile`:
|
9
9
|
```ruby
|
10
10
|
gem 'superstore'
|
11
11
|
```
|
12
12
|
|
13
|
-
Change the version of Cassandra accordingly. Recent versions have not been backward compatible.
|
14
|
-
|
15
13
|
## Defining Models
|
16
14
|
|
17
15
|
```ruby
|
@@ -30,51 +28,34 @@ end
|
|
30
28
|
```
|
31
29
|
|
32
30
|
The table name defaults to the case-sensitive, pluralized name of the model class. To specify a
|
33
|
-
custom name, set the
|
31
|
+
custom name, set the `table_name` attribute on the class:
|
34
32
|
|
35
33
|
```ruby
|
36
34
|
class MyWidget < Superstore::Base
|
37
35
|
table_name = 'my_widgets'
|
38
36
|
end
|
39
37
|
```
|
40
|
-
## Using with Cassandra
|
41
|
-
|
42
|
-
Add the cassandra-cql gem to Gemfile:
|
43
|
-
|
44
|
-
```ruby
|
45
|
-
gem 'cassandra-cql'
|
46
|
-
```
|
47
|
-
|
48
|
-
Add a config/superstore.yml:
|
49
|
-
|
50
|
-
```yaml
|
51
|
-
development:
|
52
|
-
adapter: cassandra
|
53
|
-
keyspace: my_app_development
|
54
|
-
servers: 127.0.0.1:9160
|
55
|
-
thrift:
|
56
|
-
timeout: 20
|
57
|
-
retries: 2
|
58
|
-
```
|
59
38
|
|
60
|
-
## Using
|
39
|
+
## Using the PostgreSQL JSONB adapter
|
61
40
|
|
62
|
-
Add the pg gem to
|
41
|
+
Add the `pg` gem to the `Gemfile`:
|
63
42
|
|
64
43
|
```ruby
|
65
44
|
gem 'pg'
|
66
45
|
```
|
67
46
|
|
68
|
-
|
47
|
+
Add a `config/superstore.yml`:
|
69
48
|
|
70
49
|
```yaml
|
71
50
|
development:
|
72
|
-
adapter:
|
51
|
+
adapter: jsonb
|
73
52
|
```
|
74
53
|
|
54
|
+
Superstore will share the existing ActiveRecord database connection.
|
55
|
+
|
75
56
|
## Creating and updating records
|
76
57
|
|
77
|
-
|
58
|
+
Superstore has equivalent methods to ActiveRecord:
|
78
59
|
|
79
60
|
```ruby
|
80
61
|
widget = Widget.new
|
@@ -102,8 +83,9 @@ end
|
|
102
83
|
## Scoping
|
103
84
|
|
104
85
|
Some lightweight scoping features are available:
|
86
|
+
|
105
87
|
```ruby
|
106
|
-
|
107
|
-
|
108
|
-
|
88
|
+
Widget.where('color' => 'red')
|
89
|
+
Widget.select(['name', 'color'])
|
90
|
+
Widget.limit(10)
|
109
91
|
```
|
@@ -0,0 +1,245 @@
|
|
1
|
+
gem 'pg'
|
2
|
+
require 'pg'
|
3
|
+
|
4
|
+
module Superstore
|
5
|
+
module Adapters
|
6
|
+
class JsonbAdapter < AbstractAdapter
|
7
|
+
class QueryBuilder
|
8
|
+
def initialize(adapter, scope)
|
9
|
+
@adapter = adapter
|
10
|
+
@scope = scope
|
11
|
+
end
|
12
|
+
|
13
|
+
def to_query
|
14
|
+
[
|
15
|
+
"SELECT #{select_string} FROM #{@scope.klass.table_name}",
|
16
|
+
where_string,
|
17
|
+
order_string,
|
18
|
+
limit_string
|
19
|
+
].delete_if(&:blank?) * ' '
|
20
|
+
end
|
21
|
+
|
22
|
+
def select_string
|
23
|
+
if @scope.select_values.any?
|
24
|
+
"id, jsonb_slice(document, #{@adapter.fields_to_postgres_array(@scope.select_values)}) as document"
|
25
|
+
else
|
26
|
+
'*'
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def where_string
|
31
|
+
wheres = where_values_as_strings
|
32
|
+
|
33
|
+
if @scope.id_values.any?
|
34
|
+
wheres << @adapter.create_ids_where_clause(@scope.id_values)
|
35
|
+
end
|
36
|
+
|
37
|
+
if wheres.any?
|
38
|
+
"WHERE #{wheres * ' AND '}"
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def order_string
|
43
|
+
if @scope.order_values.any?
|
44
|
+
orders = @scope.order_values.join(', ')
|
45
|
+
"ORDER BY #{orders}"
|
46
|
+
elsif @scope.id_values.many?
|
47
|
+
id_orders = @scope.id_values.map { |id| "ID=#{@adapter.quote(id)} DESC" }.join(',')
|
48
|
+
"ORDER BY #{id_orders}"
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def limit_string
|
53
|
+
if @scope.limit_value
|
54
|
+
"LIMIT #{@scope.limit_value}"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def where_values_as_strings
|
59
|
+
@scope.where_values.map do |where_value|
|
60
|
+
if where_value.is_a?(Hash)
|
61
|
+
key = where_value.keys.first
|
62
|
+
value = where_value.values.first
|
63
|
+
|
64
|
+
if value.nil?
|
65
|
+
"(document->>'#{key}') IS NULL"
|
66
|
+
elsif value.is_a?(Array)
|
67
|
+
typecasted_values = value.map { |v| "'#{v}'" }.join(',')
|
68
|
+
"document->>'#{key}' IN (#{typecasted_values})"
|
69
|
+
else
|
70
|
+
"document->>'#{key}' = '#{value}'"
|
71
|
+
end
|
72
|
+
else
|
73
|
+
where_value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def primary_key_column
|
80
|
+
'id'
|
81
|
+
end
|
82
|
+
|
83
|
+
def connection
|
84
|
+
active_record_klass.connection
|
85
|
+
end
|
86
|
+
|
87
|
+
def active_record_klass=(klass)
|
88
|
+
@active_record_klass = klass
|
89
|
+
end
|
90
|
+
|
91
|
+
def active_record_klass
|
92
|
+
@active_record_klass ||= ActiveRecord::Base
|
93
|
+
end
|
94
|
+
|
95
|
+
def execute(statement)
|
96
|
+
connection.execute statement
|
97
|
+
end
|
98
|
+
|
99
|
+
def select(scope)
|
100
|
+
statement = QueryBuilder.new(self, scope).to_query
|
101
|
+
|
102
|
+
connection.execute(statement).each do |result|
|
103
|
+
yield result[primary_key_column], Oj.compat_load(result['document'])
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def scroll(scope, batch_size)
|
108
|
+
statement = QueryBuilder.new(self, scope).to_query
|
109
|
+
cursor_name = "cursor_#{SecureRandom.hex(6)}"
|
110
|
+
fetch_sql = "FETCH FORWARD #{batch_size} FROM #{cursor_name}"
|
111
|
+
|
112
|
+
connection.transaction do
|
113
|
+
connection.execute "DECLARE #{cursor_name} NO SCROLL CURSOR FOR (#{statement})"
|
114
|
+
|
115
|
+
while (batch = connection.execute(fetch_sql)).any?
|
116
|
+
batch.each do |result|
|
117
|
+
yield result[primary_key_column], Oj.compat_load(result['document'])
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
def insert(table, id, attributes)
|
124
|
+
not_nil_attributes = attributes.reject { |key, value| value.nil? }
|
125
|
+
statement = "INSERT INTO #{table} (#{primary_key_column}, document) VALUES (#{quote(id)}, #{to_quoted_jsonb(not_nil_attributes)})"
|
126
|
+
execute_batchable statement
|
127
|
+
end
|
128
|
+
|
129
|
+
def update(table, id, attributes)
|
130
|
+
return if attributes.empty?
|
131
|
+
|
132
|
+
not_nil_attributes = attributes.reject { |key, value| value.nil? }
|
133
|
+
nil_attributes = attributes.select { |key, value| value.nil? }
|
134
|
+
|
135
|
+
if not_nil_attributes.any? && nil_attributes.any?
|
136
|
+
value_update = "jsonb_merge(jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)}), #{to_quoted_jsonb(not_nil_attributes)})"
|
137
|
+
elsif not_nil_attributes.any?
|
138
|
+
value_update = "jsonb_merge(document, #{to_quoted_jsonb(not_nil_attributes)})"
|
139
|
+
elsif nil_attributes.any?
|
140
|
+
value_update = "jsonb_delete(document, #{fields_to_postgres_array(nil_attributes.keys)})"
|
141
|
+
end
|
142
|
+
|
143
|
+
statement = "UPDATE #{table} SET document = #{value_update} WHERE #{primary_key_column} = #{quote(id)}"
|
144
|
+
execute_batchable statement
|
145
|
+
end
|
146
|
+
|
147
|
+
def delete(table, ids)
|
148
|
+
statement = "DELETE FROM #{table} WHERE #{create_ids_where_clause(ids)}"
|
149
|
+
|
150
|
+
execute_batchable statement
|
151
|
+
end
|
152
|
+
|
153
|
+
def execute_batch(statements)
|
154
|
+
connection.transaction do
|
155
|
+
execute(statements * ";\n")
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def create_table(table_name, options = {})
|
160
|
+
ActiveRecord::Migration.create_table table_name, id: false do |t|
|
161
|
+
t.string :id, null: false
|
162
|
+
t.jsonb :document, null: false
|
163
|
+
end
|
164
|
+
connection.execute "ALTER TABLE \"#{table_name}\" ADD CONSTRAINT #{table_name}_pkey PRIMARY KEY (id)"
|
165
|
+
end
|
166
|
+
|
167
|
+
def drop_table(table_name)
|
168
|
+
ActiveRecord::Migration.drop_table table_name
|
169
|
+
end
|
170
|
+
|
171
|
+
def create_ids_where_clause(ids)
|
172
|
+
ids = ids.first if ids.is_a?(Array) && ids.one?
|
173
|
+
|
174
|
+
if ids.is_a?(Array)
|
175
|
+
id_list = ids.map { |id| quote(id) }.join(',')
|
176
|
+
"#{primary_key_column} IN (#{id_list})"
|
177
|
+
else
|
178
|
+
"#{primary_key_column} = #{quote(ids)}"
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def quote(value)
|
183
|
+
connection.quote(value)
|
184
|
+
end
|
185
|
+
|
186
|
+
def fields_to_postgres_array(fields)
|
187
|
+
quoted_fields = fields.map { |field| quote(field) }.join(',')
|
188
|
+
"ARRAY[#{quoted_fields}]"
|
189
|
+
end
|
190
|
+
|
191
|
+
OJ_OPTIONS = {mode: :compat}
|
192
|
+
def to_quoted_jsonb(data)
|
193
|
+
"#{quote(Oj.dump(data, OJ_OPTIONS))}::JSONB"
|
194
|
+
end
|
195
|
+
|
196
|
+
JSON_FUNCTIONS = {
|
197
|
+
# SELECT jsonb_slice('{"b": 2, "c": 3, "a": 4}', '{b, c}');
|
198
|
+
'jsonb_slice(data jsonb, keys text[])' => %{
|
199
|
+
SELECT json_object_agg(key, value)::jsonb
|
200
|
+
FROM (
|
201
|
+
SELECT * FROM jsonb_each(data)
|
202
|
+
) t
|
203
|
+
WHERE key =ANY(keys);
|
204
|
+
},
|
205
|
+
|
206
|
+
# SELECT jsonb_merge('{"a": 1}', '{"b": 2, "c": 3, "a": 4}');
|
207
|
+
'jsonb_merge(data jsonb, merge_data jsonb)' => %{
|
208
|
+
SELECT json_object_agg(key, value)::jsonb
|
209
|
+
FROM (
|
210
|
+
WITH to_merge AS (
|
211
|
+
SELECT * FROM jsonb_each(merge_data)
|
212
|
+
)
|
213
|
+
SELECT *
|
214
|
+
FROM jsonb_each(data)
|
215
|
+
WHERE key NOT IN (SELECT key FROM to_merge)
|
216
|
+
UNION ALL
|
217
|
+
SELECT * FROM to_merge
|
218
|
+
) t;
|
219
|
+
},
|
220
|
+
|
221
|
+
# SELECT jsonb_delete('{"b": 2, "c": 3, "a": 4}', '{b, c}');
|
222
|
+
'jsonb_delete(data jsonb, keys text[])' => %{
|
223
|
+
SELECT json_object_agg(key, value)::jsonb
|
224
|
+
FROM (
|
225
|
+
SELECT * FROM jsonb_each(data)
|
226
|
+
WHERE key <>ALL(keys)
|
227
|
+
) t;
|
228
|
+
},
|
229
|
+
}
|
230
|
+
def define_jsonb_functions!
|
231
|
+
JSON_FUNCTIONS.each do |signature, body|
|
232
|
+
connection.execute %{
|
233
|
+
CREATE OR REPLACE FUNCTION public.#{signature}
|
234
|
+
RETURNS jsonb
|
235
|
+
IMMUTABLE
|
236
|
+
LANGUAGE sql
|
237
|
+
AS $$
|
238
|
+
#{body}
|
239
|
+
$$;
|
240
|
+
}
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Superstore
|
2
|
+
module Associations
|
3
|
+
class Association
|
4
|
+
attr_reader :owner, :reflection
|
5
|
+
delegate :options, to: :reflection
|
6
|
+
|
7
|
+
def initialize(owner, reflection)
|
8
|
+
@owner = owner
|
9
|
+
@reflection = reflection
|
10
|
+
end
|
11
|
+
|
12
|
+
def association_class
|
13
|
+
association_class_name.constantize
|
14
|
+
end
|
15
|
+
|
16
|
+
def association_class_name
|
17
|
+
reflection.polymorphic? ? owner.send(reflection.polymorphic_column) : reflection.class_name
|
18
|
+
end
|
19
|
+
|
20
|
+
def target=(target)
|
21
|
+
@target = target
|
22
|
+
loaded!
|
23
|
+
end
|
24
|
+
|
25
|
+
def target
|
26
|
+
@target
|
27
|
+
end
|
28
|
+
|
29
|
+
def loaded?
|
30
|
+
@loaded
|
31
|
+
end
|
32
|
+
|
33
|
+
def loaded!
|
34
|
+
@loaded = true
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module Superstore
|
2
|
+
module Associations
|
3
|
+
class BelongsTo < Association
|
4
|
+
def reader
|
5
|
+
unless loaded?
|
6
|
+
self.target = get_record
|
7
|
+
end
|
8
|
+
|
9
|
+
target
|
10
|
+
end
|
11
|
+
|
12
|
+
def writer(record)
|
13
|
+
self.target = record
|
14
|
+
owner.send("#{reflection.foreign_key}=", record.try(reflection.primary_key))
|
15
|
+
if reflection.polymorphic?
|
16
|
+
owner.send("#{reflection.polymorphic_column}=", record.class.name)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def get_record
|
23
|
+
record_id = owner.send(reflection.foreign_key).presence
|
24
|
+
return unless record_id
|
25
|
+
|
26
|
+
if reflection.default_primary_key?
|
27
|
+
association_class.find_by_id(record_id)
|
28
|
+
else
|
29
|
+
association_class.find_by(reflection.primary_key => record_id)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
module Superstore::Associations::Builder
|
2
|
+
class Association
|
3
|
+
def self.build(model, name, options)
|
4
|
+
new(model, name, options).build
|
5
|
+
end
|
6
|
+
|
7
|
+
attr_reader :model, :name, :options
|
8
|
+
def initialize(model, name, options)
|
9
|
+
@model, @name, @options = model, name, options
|
10
|
+
end
|
11
|
+
|
12
|
+
def build
|
13
|
+
define_writer
|
14
|
+
define_reader
|
15
|
+
|
16
|
+
reflection = Superstore::Associations::Reflection.new(macro, name, model, options)
|
17
|
+
model.association_reflections = model.association_reflections.merge(name => reflection)
|
18
|
+
end
|
19
|
+
|
20
|
+
def mixin
|
21
|
+
model.generated_association_methods
|
22
|
+
end
|
23
|
+
|
24
|
+
def define_writer
|
25
|
+
name = self.name
|
26
|
+
mixin.redefine_method("#{name}=") do |records|
|
27
|
+
association(name).writer(records)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def define_reader
|
32
|
+
name = self.name
|
33
|
+
mixin.redefine_method(name) do
|
34
|
+
association(name).reader
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module Superstore
|
2
|
+
module Associations
|
3
|
+
class HasMany < Association
|
4
|
+
def reader
|
5
|
+
unless loaded?
|
6
|
+
self.target = load_collection
|
7
|
+
end
|
8
|
+
|
9
|
+
target
|
10
|
+
end
|
11
|
+
|
12
|
+
def writer(records)
|
13
|
+
reader.instance_variable_set :@records, records
|
14
|
+
reader.instance_variable_set :@loaded, true
|
15
|
+
loaded!
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
|
20
|
+
def load_collection
|
21
|
+
association_class.where(reflection.foreign_key => owner.try(reflection.primary_key))
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module Superstore
|
2
|
+
module Associations
|
3
|
+
class HasOne < Association
|
4
|
+
def reader
|
5
|
+
unless loaded?
|
6
|
+
self.target = load_target
|
7
|
+
end
|
8
|
+
|
9
|
+
target
|
10
|
+
end
|
11
|
+
|
12
|
+
def writer(record)
|
13
|
+
self.target = record
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def load_target
|
19
|
+
association_class.where(reflection.foreign_key => owner.try(reflection.primary_key)).first
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module Superstore
|
2
|
+
module Associations
|
3
|
+
class Reflection
|
4
|
+
attr_reader :macro, :name, :model, :options
|
5
|
+
def initialize(macro, name, model, options)
|
6
|
+
@macro = macro
|
7
|
+
@name = name
|
8
|
+
@model = model
|
9
|
+
@options = options
|
10
|
+
end
|
11
|
+
|
12
|
+
def association_class
|
13
|
+
case macro
|
14
|
+
when :belongs_to
|
15
|
+
Superstore::Associations::BelongsTo
|
16
|
+
when :has_many
|
17
|
+
Superstore::Associations::HasMany
|
18
|
+
when :has_one
|
19
|
+
Superstore::Associations::HasOne
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
def instance_variable_name
|
25
|
+
"@#{name}"
|
26
|
+
end
|
27
|
+
|
28
|
+
def foreign_key
|
29
|
+
@foreign_key ||= options[:foreign_key] || derive_foreign_key
|
30
|
+
end
|
31
|
+
|
32
|
+
def primary_key
|
33
|
+
options[:primary_key] || "id"
|
34
|
+
end
|
35
|
+
|
36
|
+
def default_primary_key?
|
37
|
+
primary_key == "id"
|
38
|
+
end
|
39
|
+
|
40
|
+
def polymorphic_column
|
41
|
+
"#{name}_type"
|
42
|
+
end
|
43
|
+
|
44
|
+
def polymorphic?
|
45
|
+
options[:polymorphic]
|
46
|
+
end
|
47
|
+
|
48
|
+
def class_name
|
49
|
+
@class_name ||= (options[:class_name] || name.to_s.classify)
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def derive_foreign_key
|
55
|
+
case macro
|
56
|
+
when :has_many, :has_one
|
57
|
+
model.name.foreign_key
|
58
|
+
when :belongs_to
|
59
|
+
"#{name}_id"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|