amountable 0.0.8 → 0.1.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 +11 -0
- data/lib/amountable.rb +27 -49
- data/lib/amountable/amount.rb +7 -0
- data/lib/amountable/jsonb_methods.rb +51 -0
- data/lib/amountable/table_methods.rb +58 -0
- data/lib/amountable/version.rb +1 -1
- data/spec/amountable/amount_spec.rb +4 -0
- data/spec/amountable/amountable_spec.rb +70 -40
- data/spec/internal/app/models/order.rb +1 -1
- data/spec/internal/app/models/subscription.rb +12 -0
- data/spec/internal/db/schema.rb +8 -0
- data/spec/support/database.rb +10 -0
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a50976a37bd54567185d9369a702a40dd5b38795
|
4
|
+
data.tar.gz: df9bce5b74e8ce773e8b068787be2b80a2184234
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f644fbc7e790baa2cb469b681e80bd0c1f5dfb461d0364b808fe0a414cb37cbf9513c6c9063de6803eb7dd4b26b97235f382fff620bf9522973f55f1c11cf190
|
7
|
+
data.tar.gz: 121c40b69da25971c5bcd59a2c7d385d26ea8e19e9d296e4054e24be18c93973cd910a80d2cc52ca736a2599f7bb860c14b9cfe50a74f0c6df119f52c828288b
|
data/README.md
CHANGED
@@ -40,6 +40,7 @@ Setup your model
|
|
40
40
|
```ruby
|
41
41
|
class Order < ActiveRecord::Base
|
42
42
|
include Amountable
|
43
|
+
act_as_amountable
|
43
44
|
amount :subtotal, sets: [:total]
|
44
45
|
amount :delivery_fee, sets: [:total, :fees]
|
45
46
|
amount :bags_fee, sets: [:total, :fees]
|
@@ -48,6 +49,14 @@ class Order < ActiveRecord::Base
|
|
48
49
|
end
|
49
50
|
```
|
50
51
|
|
52
|
+
`act_as_amountable` can take the `storage` option:
|
53
|
+
|
54
|
+
```
|
55
|
+
act_as_amountable storage: :table
|
56
|
+
```
|
57
|
+
|
58
|
+
where `storage` can be either `:table`, the `amounts` table will be used to store amounts, or `:jsonb`, a JSONB field will be used on the amountable object to store amounts are json. If you use the JSONB format, you can specify the name of the column with the `column` option. The default value for the `storage` option is `:table`.
|
59
|
+
|
51
60
|
then create it
|
52
61
|
|
53
62
|
```ruby
|
@@ -74,3 +83,5 @@ When you run the migration, an `amounts` table will be created with a polymorphi
|
|
74
83
|
When the an order is created with some amounts, the associated `Amount` objects are persisted.
|
75
84
|
|
76
85
|
`Amount` objects are persisted in bulk, so there are no N + 1 queries. If an amount is zero, no `Amount` model is created.
|
86
|
+
|
87
|
+
If you choose the JSONB storage option, the `amounts` table will not be used. Instead a JSONB column on the target model will be used.
|
data/lib/amountable.rb
CHANGED
@@ -4,31 +4,37 @@ module Amountable
|
|
4
4
|
extend ActiveSupport::Autoload
|
5
5
|
autoload :Amount
|
6
6
|
autoload :VERSION
|
7
|
+
autoload :TableMethods
|
8
|
+
autoload :JsonbMethods
|
7
9
|
|
8
10
|
class InvalidAmountName < StandardError; end
|
11
|
+
class MissingColumn < StandardError; end
|
12
|
+
|
13
|
+
ALLOWED_STORAGE = %i(table json).freeze
|
9
14
|
|
10
15
|
def self.included(base)
|
11
16
|
|
12
17
|
base.extend Amountable::ClassMethods
|
13
18
|
|
14
19
|
base.class_eval do
|
15
|
-
has_many :amounts, as: :amountable, dependent: :destroy, autosave: false
|
16
20
|
validate :validate_amount_names
|
17
21
|
class_attribute :amount_names
|
18
22
|
class_attribute :amount_sets
|
23
|
+
class_attribute :amounts_column_name
|
19
24
|
self.amount_sets = Hash.new { |h, k| h[k] = Set.new }
|
20
25
|
self.amount_names = Set.new
|
26
|
+
self.amounts_column_name = 'amounts'
|
21
27
|
|
22
28
|
def all_amounts
|
23
29
|
@all_amounts ||= amounts.to_set
|
24
30
|
end
|
25
31
|
|
26
32
|
def find_amount(name)
|
27
|
-
(@amounts_by_name ||= {})[name.to_sym] ||=
|
33
|
+
(@amounts_by_name ||= {})[name.to_sym] ||= amounts.to_set.find { |am| am.name == name.to_s }
|
28
34
|
end
|
29
35
|
|
30
36
|
def find_amounts(names)
|
31
|
-
|
37
|
+
amounts.to_set.select { |am| names.include?(am.name.to_sym) }
|
32
38
|
end
|
33
39
|
|
34
40
|
def validate_amount_names
|
@@ -49,48 +55,31 @@ module Amountable
|
|
49
55
|
end
|
50
56
|
end
|
51
57
|
end
|
52
|
-
|
53
|
-
def save(args = {})
|
54
|
-
ActiveRecord::Base.transaction do
|
55
|
-
save_amounts if super(args)
|
56
|
-
end
|
57
|
-
end
|
58
|
-
|
59
|
-
def save!(args = {})
|
60
|
-
ActiveRecord::Base.transaction do
|
61
|
-
save_amounts! if super(args)
|
62
|
-
end
|
63
|
-
end
|
64
|
-
|
65
|
-
def save_amounts(bang: false)
|
66
|
-
amounts_to_insert = []
|
67
|
-
amounts.each do |amount|
|
68
|
-
if amount.new_record?
|
69
|
-
amount.amountable_id = self.id
|
70
|
-
amounts_to_insert << amount
|
71
|
-
else
|
72
|
-
bang ? amount.save! : amount.save
|
73
|
-
end
|
74
|
-
end
|
75
|
-
Amount.import(amounts_to_insert, timestamps: true, validate: false)
|
76
|
-
amounts_to_insert.each do |amount|
|
77
|
-
amount.instance_variable_set(:@new_record, false)
|
78
|
-
end
|
79
|
-
true
|
80
|
-
end
|
81
|
-
|
82
|
-
def save_amounts!; save_amounts(bang: true); end
|
83
|
-
|
84
58
|
end
|
85
59
|
end
|
86
60
|
|
87
61
|
module ClassMethods
|
88
62
|
|
63
|
+
# Possible storage values: [:table, :jsonb]
|
64
|
+
def act_as_amountable(options = {})
|
65
|
+
case (options[:storage] || :table).to_sym
|
66
|
+
when :table
|
67
|
+
has_many :amounts, as: :amountable, dependent: :destroy, autosave: false
|
68
|
+
include Amountable::TableMethods
|
69
|
+
when :jsonb
|
70
|
+
self.amounts_column_name = options[:column].to_s if options[:column]
|
71
|
+
raise MissingColumn.new("You need an amounts jsonb field on the #{self.table_name} table.") unless column_names.include?(self.amounts_column_name)
|
72
|
+
include Amountable::JsonbMethods
|
73
|
+
else
|
74
|
+
raise ArgumentError.new("Please specify a storage: #{ALLOWED_STORAGE}")
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
89
78
|
def amount_set(set_name, component)
|
90
79
|
self.amount_sets[set_name.to_sym] << component.to_sym
|
91
80
|
|
92
81
|
define_method set_name do
|
93
|
-
|
82
|
+
get_set(set_name)
|
94
83
|
end
|
95
84
|
end
|
96
85
|
|
@@ -98,22 +87,11 @@ module Amountable
|
|
98
87
|
(self.amount_names ||= Set.new) << name
|
99
88
|
|
100
89
|
define_method name do
|
101
|
-
(find_amount(name) || NilAmount.new).value
|
90
|
+
(find_amount(name) || ::NilAmount.new).value
|
102
91
|
end
|
103
92
|
|
104
93
|
define_method "#{name}=" do |value|
|
105
|
-
|
106
|
-
amount.value = value.to_money
|
107
|
-
if value.zero?
|
108
|
-
amounts.delete(amount)
|
109
|
-
all_amounts.delete(amount)
|
110
|
-
@amounts_by_name.delete(name)
|
111
|
-
amount.destroy if amount.persisted?
|
112
|
-
else
|
113
|
-
all_amounts << amount if amount.new_record?
|
114
|
-
(@amounts_by_name ||= {})[name.to_sym] = amount
|
115
|
-
end
|
116
|
-
value.to_money
|
94
|
+
set_amount(name, value)
|
117
95
|
end
|
118
96
|
|
119
97
|
Array(options[:summable] || options[:summables] || options[:set] || options[:sets] || options[:amount_set] || options[:amount_sets]).each do |set|
|
data/lib/amountable/amount.rb
CHANGED
@@ -9,6 +9,13 @@ class Amount < ActiveRecord::Base
|
|
9
9
|
validates :name, presence: true
|
10
10
|
validates :name, uniqueness: {scope: [:amountable_id, :amountable_type]}
|
11
11
|
|
12
|
+
attr_accessor :persistable
|
13
|
+
|
14
|
+
def save
|
15
|
+
raise StandardError.new("Can't persist amount to database") if persistable == false
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
12
19
|
module Operations
|
13
20
|
|
14
21
|
def +(other_value)
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# Copyright 2015-2016, Instacart
|
2
|
+
|
3
|
+
module Amountable
|
4
|
+
module JsonbMethods
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
def amounts
|
8
|
+
@_amounts ||= attribute(amounts_column_name).to_h['amounts'].to_h.map do |name, amount|
|
9
|
+
Amount.new(name: name, value_cents: amount['cents'], value_currency: amount['value_currency'], persistable: false, amountable: self)
|
10
|
+
end.to_set
|
11
|
+
end
|
12
|
+
|
13
|
+
def set_amount(name, value)
|
14
|
+
value = value.to_money
|
15
|
+
return value if value.zero?
|
16
|
+
initialize_column
|
17
|
+
amounts_json = attribute(amounts_column_name)
|
18
|
+
amounts_json['amounts'] ||= {}
|
19
|
+
amounts_json['amounts'][name.to_s] = {'cents' => value.fractional, 'currency' => value.currency.iso_code}
|
20
|
+
set_json(amounts_json)
|
21
|
+
@_amounts = nil
|
22
|
+
@amounts_by_name = nil
|
23
|
+
refresh_sets
|
24
|
+
value
|
25
|
+
end
|
26
|
+
|
27
|
+
def refresh_sets
|
28
|
+
initialize_column
|
29
|
+
amounts_json = attribute(amounts_column_name)
|
30
|
+
amounts_json['sets'] = {}
|
31
|
+
amount_sets.each do |name, amount_names|
|
32
|
+
sum = find_amounts(amount_names).sum(Money.zero, &:value)
|
33
|
+
amounts_json['sets'][name.to_s] = {'cents' => sum.fractional, 'currency' => sum.currency.iso_code}
|
34
|
+
end
|
35
|
+
set_json(amounts_json)
|
36
|
+
end
|
37
|
+
|
38
|
+
def get_set(name)
|
39
|
+
value = attribute(amounts_column_name).to_h['sets'].to_h[name.to_s].to_h
|
40
|
+
Money.new(value['cents'].to_i, value['currency'] || 'USD')
|
41
|
+
end
|
42
|
+
|
43
|
+
def set_json(json)
|
44
|
+
send("#{amounts_column_name}=", json)
|
45
|
+
end
|
46
|
+
|
47
|
+
def initialize_column
|
48
|
+
send("#{amounts_column_name}=", {}) if attribute(amounts_column_name).nil?
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# Copyright 2015-2016, Instacart
|
2
|
+
|
3
|
+
module Amountable
|
4
|
+
module TableMethods
|
5
|
+
extend ActiveSupport::Autoload
|
6
|
+
|
7
|
+
def set_amount(name, value)
|
8
|
+
amount = find_amount(name) || amounts.build(name: name)
|
9
|
+
amount.value = value.to_money
|
10
|
+
if value.zero?
|
11
|
+
amounts.delete(amount)
|
12
|
+
all_amounts.delete(amount)
|
13
|
+
@amounts_by_name.delete(name)
|
14
|
+
amount.destroy if amount.persisted?
|
15
|
+
else
|
16
|
+
all_amounts << amount if amount.new_record?
|
17
|
+
(@amounts_by_name ||= {})[name.to_sym] = amount
|
18
|
+
end
|
19
|
+
amount.value
|
20
|
+
end
|
21
|
+
|
22
|
+
def save(args = {})
|
23
|
+
ActiveRecord::Base.transaction do
|
24
|
+
save_amounts if super(args)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def save!(args = {})
|
29
|
+
ActiveRecord::Base.transaction do
|
30
|
+
save_amounts! if super(args)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def save_amounts(bang: false)
|
35
|
+
amounts_to_insert = []
|
36
|
+
amounts.each do |amount|
|
37
|
+
if amount.new_record?
|
38
|
+
amount.amountable_id = self.id
|
39
|
+
amounts_to_insert << amount
|
40
|
+
else
|
41
|
+
bang ? amount.save! : amount.save
|
42
|
+
end
|
43
|
+
end
|
44
|
+
Amount.import(amounts_to_insert, timestamps: true, validate: false)
|
45
|
+
amounts_to_insert.each do |amount|
|
46
|
+
amount.instance_variable_set(:@new_record, false)
|
47
|
+
end
|
48
|
+
true
|
49
|
+
end
|
50
|
+
|
51
|
+
def save_amounts!; save_amounts(bang: true); end
|
52
|
+
|
53
|
+
def get_set(name)
|
54
|
+
find_amounts(self.amount_sets[name.to_sym]).sum(Money.zero, &:value)
|
55
|
+
end
|
56
|
+
|
57
|
+
end
|
58
|
+
end
|
data/lib/amountable/version.rb
CHANGED
@@ -4,55 +4,85 @@ require 'spec_helper'
|
|
4
4
|
|
5
5
|
describe Amountable do
|
6
6
|
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
7
|
+
context 'storage == :table' do
|
8
|
+
it 'should' do
|
9
|
+
order = Order.new
|
10
|
+
expect { order.save }.not_to change { Amount.count }
|
11
|
+
%i(sub_total taxes total).each do |name|
|
12
|
+
expect(order.send(name)).to eq(Money.zero)
|
13
|
+
end
|
14
|
+
order.sub_total = Money.new(100)
|
15
|
+
expect(order.sub_total).to eq(Money.new(100))
|
16
|
+
expect(order.total).to eq(Money.new(100))
|
17
|
+
expect(order.all_amounts.size).to eq(1)
|
18
|
+
order.all_amounts.first.tap do |amount|
|
19
|
+
expect(amount.name).to eq('sub_total')
|
20
|
+
expect(amount.value).to eq(Money.new(100))
|
21
|
+
expect(amount.new_record?).to be true
|
22
|
+
expect { order.save }.to change { Amount.count }.by(1)
|
23
|
+
expect(amount.persisted?).to be true
|
24
|
+
end
|
25
|
+
expect do
|
26
|
+
expect(order.update_attributes(sub_total: Money.new(200)))
|
27
|
+
end.not_to change { Amount.count }
|
23
28
|
end
|
24
|
-
expect do
|
25
|
-
expect(order.update_attributes(sub_total: Money.new(200)))
|
26
|
-
end.not_to change { Amount.count }
|
27
|
-
end
|
28
29
|
|
29
|
-
|
30
|
-
|
30
|
+
describe 'name=' do
|
31
|
+
let (:order) { Order.create }
|
31
32
|
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
33
|
+
it 'should not persist Money.zero' do
|
34
|
+
expect(order.sub_total = Money.zero).to eq(Money.zero)
|
35
|
+
expect { order.save }.not_to change { Amount.count }
|
36
|
+
end
|
36
37
|
|
37
|
-
|
38
|
-
|
39
|
-
|
38
|
+
it 'should not persist Money.zero if using ActiveRecord persistence' do
|
39
|
+
expect { order.update(sub_total: Money.zero) }.not_to change { Amount.count }
|
40
|
+
end
|
41
|
+
|
42
|
+
it 'should work with ActiveRecord#update' do
|
43
|
+
expect { order.update(sub_total: Money.new(1)) }.to change { Amount.count }.by(1)
|
44
|
+
end
|
40
45
|
|
41
|
-
|
42
|
-
|
46
|
+
it 'should destroy Amount if exist and assigning Money.zero' do
|
47
|
+
order.update(sub_total: Money.new(1))
|
48
|
+
expect { order.sub_total = Money.zero }.to change { Amount.count }.by(-1)
|
49
|
+
expect(order.amounts.empty?).to be true
|
50
|
+
end
|
43
51
|
end
|
44
52
|
|
45
|
-
it 'should
|
46
|
-
order
|
47
|
-
expect
|
48
|
-
|
53
|
+
it 'should insert amounts in bulk' do
|
54
|
+
order = Order.create
|
55
|
+
expect do
|
56
|
+
order.update(sub_total: Money.new(100), taxes: Money.new(200))
|
57
|
+
end.to make_database_queries(count: 1, manipulative: true)
|
49
58
|
end
|
50
59
|
end
|
51
60
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
61
|
+
context 'storage == :jsonb' do
|
62
|
+
it 'should' do
|
63
|
+
subscription = Subscription.new
|
64
|
+
expect { subscription.save }.not_to change { Amount.count }
|
65
|
+
expect(subscription.amounts).to eq(Set.new)
|
66
|
+
expect(subscription.attributes['amounts']).to be_nil
|
67
|
+
%i(sub_total taxes total).each do |name|
|
68
|
+
expect(subscription.send(name)).to eq(Money.zero)
|
69
|
+
end
|
70
|
+
subscription.sub_total = Money.new(100)
|
71
|
+
expect(subscription.sub_total).to eq(Money.new(100))
|
72
|
+
expect(subscription.attributes['amounts']).to eq({'amounts' => {'sub_total' => {'cents' => 100, 'currency' => 'USD'}}, 'sets' => {'total' => {'cents' => 100, 'currency' => 'USD'}}})
|
73
|
+
expect(subscription.total).to eq(Money.new(100))
|
74
|
+
expect(subscription.amounts.size).to eq(1)
|
75
|
+
subscription.amounts.first.tap do |amount|
|
76
|
+
expect(amount.name).to eq('sub_total')
|
77
|
+
expect(amount.value).to eq(Money.new(100))
|
78
|
+
expect(amount.new_record?).to be true
|
79
|
+
expect { subscription.save }.not_to change { Amount.count }
|
80
|
+
expect(amount.persisted?).to be false
|
81
|
+
end
|
82
|
+
subscription.update_attributes(sub_total: Money.new(200))
|
83
|
+
expect(subscription.sub_total).to eq(Money.new(200))
|
84
|
+
expect(subscription.total).to eq(Money.new(200))
|
85
|
+
end
|
57
86
|
end
|
87
|
+
|
58
88
|
end
|
data/spec/internal/db/schema.rb
CHANGED
@@ -17,4 +17,12 @@ ActiveRecord::Schema.define do
|
|
17
17
|
t.datetime "created_at"
|
18
18
|
t.datetime "updated_at"
|
19
19
|
end
|
20
|
+
|
21
|
+
if jsonb_available?
|
22
|
+
create_table "subscriptions", force: :cascade do |t|
|
23
|
+
t.datetime "created_at"
|
24
|
+
t.datetime "updated_at"
|
25
|
+
t.jsonb "amounts"
|
26
|
+
end
|
27
|
+
end
|
20
28
|
end
|
data/spec/support/database.rb
CHANGED
@@ -29,6 +29,16 @@ rescue
|
|
29
29
|
ActiveRecord::Base.establish_connection(config)
|
30
30
|
end
|
31
31
|
|
32
|
+
def jsonb_available?
|
33
|
+
return @@jsonb_available if defined?(@@jsonb_available)
|
34
|
+
@@jsonb_available = if ActiveRecord::Base.connection.class.ancestors.include?(ActiveRecord::Import::PostgreSQLAdapter)
|
35
|
+
version = /PostgreSQL\s(\d+.\d+.\d+)\s/.match(ActiveRecord::Base.connection.execute("select version();")[0]['version'])[1].split('.').map(&:to_i)
|
36
|
+
version[0] >= 9 && version[1] >= 3
|
37
|
+
else
|
38
|
+
false
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
32
42
|
require_relative '../internal/db/schema'
|
33
43
|
|
34
44
|
Dir[spec_dir.join('internal/app/models/*.rb')].each { |file| require_relative file }
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: amountable
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Emmanuel Turlay
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2016-
|
11
|
+
date: 2016-10-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: activerecord
|
@@ -209,11 +209,14 @@ files:
|
|
209
209
|
- db/migrate/0_create_amounts.rb
|
210
210
|
- lib/amountable.rb
|
211
211
|
- lib/amountable/amount.rb
|
212
|
+
- lib/amountable/jsonb_methods.rb
|
213
|
+
- lib/amountable/table_methods.rb
|
212
214
|
- lib/amountable/version.rb
|
213
215
|
- spec/amountable/amount_spec.rb
|
214
216
|
- spec/amountable/amountable_spec.rb
|
215
217
|
- spec/amountable/nil_amount_spec.rb
|
216
218
|
- spec/internal/app/models/order.rb
|
219
|
+
- spec/internal/app/models/subscription.rb
|
217
220
|
- spec/internal/config/database.yml
|
218
221
|
- spec/internal/config/database.yml.sample
|
219
222
|
- spec/internal/db/schema.rb
|
@@ -239,7 +242,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
239
242
|
version: '0'
|
240
243
|
requirements: []
|
241
244
|
rubyforge_project:
|
242
|
-
rubygems_version: 2.5
|
245
|
+
rubygems_version: 2.4.5
|
243
246
|
signing_key:
|
244
247
|
specification_version: 4
|
245
248
|
summary: Easy Money fields for your Rails models.
|
@@ -248,6 +251,7 @@ test_files:
|
|
248
251
|
- spec/amountable/amountable_spec.rb
|
249
252
|
- spec/amountable/nil_amount_spec.rb
|
250
253
|
- spec/internal/app/models/order.rb
|
254
|
+
- spec/internal/app/models/subscription.rb
|
251
255
|
- spec/internal/config/database.yml
|
252
256
|
- spec/internal/config/database.yml.sample
|
253
257
|
- spec/internal/db/schema.rb
|