superstore 1.2.0 → 2.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://secure.travis-ci.org/data-axle/superstore.png?rvm=2.0.0)](http://travis-ci.org/data-axle/superstore) [![Code Climate](https://codeclimate.com/github/data-axle/superstore.
|
2
|
+
[![Build Status](https://secure.travis-ci.org/data-axle/superstore.png?rvm=2.0.0)](http://travis-ci.org/data-axle/superstore) [![Code Climate](https://codeclimate.com/github/data-axle/superstore/badges/gpa.svg)](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
|