motion_record 0.0.1
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 +15 -0
- data/.gitignore +17 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +246 -0
- data/Rakefile +1 -0
- data/lib/motion_record/base.rb +54 -0
- data/lib/motion_record/connection_adapters/sqlite_adapter.rb +160 -0
- data/lib/motion_record/persistence.rb +99 -0
- data/lib/motion_record/schema/column_definition.rb +111 -0
- data/lib/motion_record/schema/index_definition.rb +35 -0
- data/lib/motion_record/schema/migration.rb +25 -0
- data/lib/motion_record/schema/migration_definition.rb +32 -0
- data/lib/motion_record/schema/migrator.rb +39 -0
- data/lib/motion_record/schema/migrator_definition.rb +18 -0
- data/lib/motion_record/schema/table_definition.rb +46 -0
- data/lib/motion_record/schema.rb +35 -0
- data/lib/motion_record/scope.rb +129 -0
- data/lib/motion_record/scope_helpers.rb +89 -0
- data/lib/motion_record/serialization/base_serializer.rb +20 -0
- data/lib/motion_record/serialization/boolean_serializer.rb +26 -0
- data/lib/motion_record/serialization/default_serializer.rb +14 -0
- data/lib/motion_record/serialization/json_serializer.rb +38 -0
- data/lib/motion_record/serialization/time_serializer.rb +84 -0
- data/lib/motion_record/serialization.rb +68 -0
- data/lib/motion_record/version.rb +3 -0
- data/lib/motion_record.rb +37 -0
- data/motion_record.gemspec +23 -0
- metadata +99 -0
checksums.yaml
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
---
|
2
|
+
!binary "U0hBMQ==":
|
3
|
+
metadata.gz: !binary |-
|
4
|
+
NDdiYmE3NTlmNjMzMmVjN2UwMmJhNjk1YTRlOGFlYmQxYjVmMmNkYQ==
|
5
|
+
data.tar.gz: !binary |-
|
6
|
+
YTgzNmU2OTFhMzVhNjRiYWZlNGY1MjZiZDQ0MWI5Njk0Y2VlMTQ3Zg==
|
7
|
+
SHA512:
|
8
|
+
metadata.gz: !binary |-
|
9
|
+
NWVkNjgwMjUwYzBkNjgzY2FhNmY5OTM1YWI0N2M1Nzk4ZjQ2N2M5YWE5MTc4
|
10
|
+
NDViNWZkYmQ0NjhlZTFlMzM5NjAyOGIwNTMzYjQzNzEzMDhkNGE0OTkzZWM4
|
11
|
+
MTRkNDg4ZTA1N2NiYWJmNjBiZDcwY2I0Mjk5NTI0NzdjZWNjZmI=
|
12
|
+
data.tar.gz: !binary |-
|
13
|
+
ZDkxM2Y1ODI4NTFmYmExZjMwZjM2MzEzOTgxMjRiNWYyYWM1ODlmNWE4YWJk
|
14
|
+
MTNlNTY5ZTMyOTExZWE2ODRjYzRkYTZjODYyNDYzZGQ4ZGUxYjJjMmNhMmEy
|
15
|
+
YjMyYTRlMmRiYzhmNDIwOTUyMzEzOWM4MmRmMWI1Njk2M2U2MDY=
|
data/.gitignore
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2015 Zach Millman
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,246 @@
|
|
1
|
+
MotionRecord
|
2
|
+
============
|
3
|
+
|
4
|
+
*Miniature ActiveRecord for RubyMotion*
|
5
|
+
|
6
|
+
Everything you need to start using SQLite as the datastore for your RubyMotion
|
7
|
+
app.
|
8
|
+
|
9
|
+
:turtle: Android support should be coming soon
|
10
|
+
|
11
|
+
[](https://codeclimate.com/github/magoosh/motion_record) [](https://codeclimate.com/github/magoosh/motion_record)
|
12
|
+
|
13
|
+
Installation
|
14
|
+
------------
|
15
|
+
|
16
|
+
Add this line to your Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem "motion_record"
|
20
|
+
```
|
21
|
+
|
22
|
+
On iOS, MotionRecord uses [motion-sqlite3](https://github.com/mattgreen/motion-sqlite3)
|
23
|
+
as a wrapper for connecting to SQLite, so add these too:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem "motion-sqlite3"
|
27
|
+
# Requires the most recent unpublished version of motion.h
|
28
|
+
# https://github.com/kastiglione/motion.h/issues/11
|
29
|
+
gem "motion.h", :git => "https://github.com/kastiglione/motion.h"
|
30
|
+
```
|
31
|
+
|
32
|
+
And then execute:
|
33
|
+
|
34
|
+
```
|
35
|
+
$ bundle
|
36
|
+
```
|
37
|
+
|
38
|
+
* TODO: Android???
|
39
|
+
|
40
|
+
MotionRecord::Schema
|
41
|
+
--------------------
|
42
|
+
|
43
|
+
Define and run all pending SQLite migrations with the `up!` DSL.
|
44
|
+
|
45
|
+
```ruby
|
46
|
+
def application(application, didFinishLaunchingWithOptions:launchOptions)
|
47
|
+
MotionRecord::Schema.up! do
|
48
|
+
migration 1, "Create messages table" do
|
49
|
+
create_table :messages do |t|
|
50
|
+
t.text :subject, null: false
|
51
|
+
t.text :body
|
52
|
+
t.integer :read_at
|
53
|
+
t.integer :remote_id
|
54
|
+
t.float :satisfaction, default: 0.0
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
migration 2, "Index messages table" do
|
59
|
+
add_index :messages, :remote_id, :unique => true
|
60
|
+
add_index :messages, [:subject, :read_at]
|
61
|
+
end
|
62
|
+
end
|
63
|
+
# ...
|
64
|
+
end
|
65
|
+
```
|
66
|
+
|
67
|
+
#### Schema Configuration
|
68
|
+
|
69
|
+
By default, MotionRecord will print all SQL statements and use a file named
|
70
|
+
`"app.sqlite3"` in the application's Application Support folder. To disable
|
71
|
+
logging (for release) or change the filename, pass configuration options to `up!`
|
72
|
+
|
73
|
+
```ruby
|
74
|
+
resource_file = File.join(NSBundle.mainBundle.resourcePath, "data.sqlite3")
|
75
|
+
MotionRecord::Schema.up!(file: resource_file, debug: false) # ...
|
76
|
+
```
|
77
|
+
|
78
|
+
You can also specify that MotionRecord should use an in-memory SQLite database
|
79
|
+
which will be cleared every time the app process is killed.
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
MotionRecord::Schema.up!(file: :memory) # ...
|
83
|
+
```
|
84
|
+
|
85
|
+
MotionRecord::Base
|
86
|
+
------------------
|
87
|
+
|
88
|
+
MotionRecord::Base provides a superclass for defining objects which are stored
|
89
|
+
in the database.
|
90
|
+
|
91
|
+
```ruby
|
92
|
+
class Message < MotionRecord::Base
|
93
|
+
# That's all!
|
94
|
+
end
|
95
|
+
```
|
96
|
+
|
97
|
+
Attribute methods are inferred from the associated SQLite table definition.
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
message = Message.new(subject: "Welcome!", body: "If you have any questions...")
|
101
|
+
# => #<Message: @id=nil @subject="Welcome!" @body="If you have any..." ...>
|
102
|
+
message.satisfaction
|
103
|
+
# => 0.0
|
104
|
+
```
|
105
|
+
|
106
|
+
Manage persistence with `save!`, `delete!`, and `persisted?`
|
107
|
+
|
108
|
+
```ruby
|
109
|
+
message = Message.new(subject: "Welcome!", body: "If you have any questions...")
|
110
|
+
message.save!
|
111
|
+
message.id
|
112
|
+
# => 1
|
113
|
+
message.delete!
|
114
|
+
message.persisted?
|
115
|
+
# => false
|
116
|
+
```
|
117
|
+
|
118
|
+
* TODO: Better default inflection of class names to table names
|
119
|
+
|
120
|
+
MotionRecord::Scope
|
121
|
+
-------------------
|
122
|
+
|
123
|
+
Build scopes on MotionRecord::Base classes with `where`, `order` and `limit`.
|
124
|
+
|
125
|
+
```ruby
|
126
|
+
Message.where(body: nil).order("read_at DESC").limit(3).find_all
|
127
|
+
```
|
128
|
+
|
129
|
+
Run queries on scopes with `exists?`, `first`, `find`, `find_all`, `pluck`,
|
130
|
+
`update_all`, and `delete_all`.
|
131
|
+
|
132
|
+
```ruby
|
133
|
+
Message.where(remote_id: 2).exists?
|
134
|
+
# => false
|
135
|
+
Message.find(21)
|
136
|
+
# => #<Message @id=21 @subject="What's updog?" ...>
|
137
|
+
Message.where(read_at: nil).pluck(:subject)
|
138
|
+
# => ["What's updog?", "What's updog?", "What's updog?"]
|
139
|
+
Message.where(read_at: nil).find_all
|
140
|
+
# => [#<Message @id=20 ...>, #<Message @id=21 ...>, #<Message @id=22 ...>]
|
141
|
+
Message.where(read_at: nil).update_all(read_at: Time.now.to_i)
|
142
|
+
```
|
143
|
+
|
144
|
+
* TODO: Return "rows modified" count for update_all and delete_all
|
145
|
+
|
146
|
+
Run calculations on scopes with `count`, `sum`, `maximum`, `minimum`, and
|
147
|
+
`average`.
|
148
|
+
|
149
|
+
```ruby
|
150
|
+
Message.where(subject: "Welcome!").count
|
151
|
+
# => 1
|
152
|
+
Message.where(subject: "How do you like the app?").maximum(:satisfaction)
|
153
|
+
# => 10.0
|
154
|
+
```
|
155
|
+
|
156
|
+
* TODO: Predicates for comparisons other than `=`
|
157
|
+
* TODO: Handle datatype conversion in `where` and `update_all`
|
158
|
+
|
159
|
+
MotionRecord::Serialization
|
160
|
+
----------------------------------
|
161
|
+
|
162
|
+
SQLite has a very limited set of datatypes (TEXT, INTEGER, and REAL), but you
|
163
|
+
can easily store other objects as attributes in the database with serializers.
|
164
|
+
|
165
|
+
#### Built-in Serializers
|
166
|
+
|
167
|
+
MotionRecord provides a built-in serializer for Time objects to any column
|
168
|
+
datatype.
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
class Message < MotionRecord::Base
|
172
|
+
serialize :read_at, :time
|
173
|
+
end
|
174
|
+
|
175
|
+
Message.create!(subject: "Hello!", read_at: Time.now)
|
176
|
+
# SQL: INSERT INTO messages (subject, body, read_at, ...) VALUES (?, ?, ?...)
|
177
|
+
# Params: ["Hello!", nil, 1420099200, ...]
|
178
|
+
Message.first.read_at
|
179
|
+
# => 2015-01-01 00:00:00 -0800
|
180
|
+
```
|
181
|
+
|
182
|
+
Boolean attributes can be serialized to INTEGER columns where 0 and NULL are
|
183
|
+
`false` and any other value is `true`.
|
184
|
+
|
185
|
+
```ruby
|
186
|
+
class Message < MotionRecord::Base
|
187
|
+
serialize :satisfaction_submitted, :boolean
|
188
|
+
end
|
189
|
+
```
|
190
|
+
|
191
|
+
Objects can also be stored to TEXT columns as JSON.
|
192
|
+
|
193
|
+
```ruby
|
194
|
+
class Survey < MotionRecord::Base
|
195
|
+
serialize :response, :json
|
196
|
+
end
|
197
|
+
|
198
|
+
survey = Survey.new(response: {nps: 10, what_can_we_improve: "Nothing :)"})
|
199
|
+
survey.save!
|
200
|
+
# SQL: INSERT INTO surveys (response) VALUES (?)
|
201
|
+
# Params: ['{"nps":10, "what_can_we_improve":"Nothing :)"}']
|
202
|
+
Survey.first
|
203
|
+
# => #<Survey: @id=1 @response={"nps"=>10, "what_can_we_improve"=>"Nothing :)"}>
|
204
|
+
```
|
205
|
+
|
206
|
+
* TODO: Make JSON serializer cross-platform
|
207
|
+
|
208
|
+
#### Custom Serializers
|
209
|
+
|
210
|
+
To write a custom serializer, extend MotionRecord::Serialization::BaseSerializer
|
211
|
+
and provide your class to `serialize` instead of a symbol.
|
212
|
+
|
213
|
+
```ruby
|
214
|
+
class MoneySerializer < MotionRecord::Serialization::BaseSerializer
|
215
|
+
def serialize(value)
|
216
|
+
raise "Wrong column type!" unless @column.type == :integer
|
217
|
+
value.cents
|
218
|
+
end
|
219
|
+
|
220
|
+
def deserialize(value)
|
221
|
+
raise "Wrong column type!" unless @column.type == :integer
|
222
|
+
Money.new(value)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
class Purchase < MotionRecord::Base
|
227
|
+
serialize :amount_paid_cents, MoneySerializer
|
228
|
+
end
|
229
|
+
```
|
230
|
+
|
231
|
+
MotionRecord::Association
|
232
|
+
-------------------------
|
233
|
+
|
234
|
+
* TODO: has_many and belongs_to
|
235
|
+
|
236
|
+
|
237
|
+
Contributing
|
238
|
+
------------
|
239
|
+
|
240
|
+
Please do!
|
241
|
+
|
242
|
+
1. Fork it
|
243
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
244
|
+
3. Commit your changes (`git commit -am 'Add some feature'`)
|
245
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
246
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
@@ -0,0 +1,54 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
class Base
|
3
|
+
include Persistence
|
4
|
+
|
5
|
+
def initialize(attributes={})
|
6
|
+
initialize_from_attribute_hash(attributes)
|
7
|
+
end
|
8
|
+
|
9
|
+
def to_attribute_hash
|
10
|
+
self.class.attribute_names.each_with_object({}) do |name, hash|
|
11
|
+
hash[name] = self.instance_variable_get "@#{name}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
def connection
|
16
|
+
self.class.connection
|
17
|
+
end
|
18
|
+
|
19
|
+
protected
|
20
|
+
|
21
|
+
def initialize_from_attribute_hash(hash)
|
22
|
+
self.class.attribute_defaults.merge(hash).each do |name, value|
|
23
|
+
self.instance_variable_set "@#{name}", value
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
class << self
|
28
|
+
# Add attribute methods to the model
|
29
|
+
#
|
30
|
+
# name - Symobl name of the attribute
|
31
|
+
# options - optional configuration Hash:
|
32
|
+
# :default - default value for the attribute (nil otherwise)
|
33
|
+
def define_attribute(name, options = {})
|
34
|
+
attr_accessor name
|
35
|
+
self.attribute_names << name.to_sym
|
36
|
+
if options[:default]
|
37
|
+
self.attribute_defaults[name.to_sym] = options[:default]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def attribute_names
|
42
|
+
@attribute_names ||= []
|
43
|
+
end
|
44
|
+
|
45
|
+
def attribute_defaults
|
46
|
+
@attribute_defaults ||= {}
|
47
|
+
end
|
48
|
+
|
49
|
+
def connection
|
50
|
+
ConnectionAdapters::SQLiteAdapter.instance
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module ConnectionAdapters
|
3
|
+
class SQLiteAdapter
|
4
|
+
class << self
|
5
|
+
# Configure the SQLite connection
|
6
|
+
#
|
7
|
+
# options - Hash of configuration options for the SQLite connection
|
8
|
+
# :file - full name of the database file, or :memory for
|
9
|
+
# in-memory database files (default is "app.sqlite3"
|
10
|
+
# in the app's `/Library/Application Support` folder)
|
11
|
+
# :debug - set to false to turn off SQL debug logging
|
12
|
+
def configure(options={})
|
13
|
+
@configuration_options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def instance
|
17
|
+
@instance ||= ConnectionAdapters::SQLiteAdapter.new(filename, debug?)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Full filename of the database file
|
21
|
+
def filename
|
22
|
+
if (file = @configuration_options[:file])
|
23
|
+
if file == :memory
|
24
|
+
":memory:"
|
25
|
+
else
|
26
|
+
file
|
27
|
+
end
|
28
|
+
else
|
29
|
+
create_default_database_file
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# Returns true if debug logging is enabled for the database
|
34
|
+
def debug?
|
35
|
+
if @configuration_options.has_key?(:debug)
|
36
|
+
!!@configuration_options[:debug]
|
37
|
+
else
|
38
|
+
true
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
protected
|
43
|
+
|
44
|
+
# Create the default database file in `Library/Application Support` if
|
45
|
+
# it doesn't exist and return the file's full path
|
46
|
+
def create_default_database_file
|
47
|
+
fm = NSFileManager.defaultManager
|
48
|
+
|
49
|
+
support_path = fm.URLsForDirectory(NSApplicationSupportDirectory, inDomains: NSUserDomainMask).first.path
|
50
|
+
file_path = File.join(support_path, "app.sqlite3")
|
51
|
+
|
52
|
+
unless fm.fileExistsAtPath(file_path)
|
53
|
+
fm.createDirectoryAtPath(support_path, withIntermediateDirectories:true, attributes:nil, error:nil)
|
54
|
+
success = fm.createFileAtPath(file_path, contents: nil, attributes: nil)
|
55
|
+
raise "Couldn't create file #{path}" unless success
|
56
|
+
end
|
57
|
+
|
58
|
+
file_path
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def initialize(file, debug=true)
|
63
|
+
@db = SQLite3::Database.new(file)
|
64
|
+
@db.logging = debug
|
65
|
+
end
|
66
|
+
|
67
|
+
def execute(command)
|
68
|
+
@db.execute(command)
|
69
|
+
end
|
70
|
+
|
71
|
+
def table_exists?(table_name)
|
72
|
+
# FIXME: This statement is totally vulnerable to SQL injection
|
73
|
+
@db.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='#{table_name}'").any?
|
74
|
+
end
|
75
|
+
|
76
|
+
# Load all records for a scope
|
77
|
+
#
|
78
|
+
# scope - A MotionRecord::Scope
|
79
|
+
#
|
80
|
+
# Returns an Array of row Hashes
|
81
|
+
def select(scope)
|
82
|
+
select_statement = "SELECT * FROM #{scope.klass.table_name} #{scope.predicate}"
|
83
|
+
@db.execute(select_statement, scope.predicate_values)
|
84
|
+
end
|
85
|
+
|
86
|
+
# Add a row to a table
|
87
|
+
#
|
88
|
+
# table_name - name of the table
|
89
|
+
# params - Hash of column names to values to insert
|
90
|
+
def insert(table_name, params)
|
91
|
+
pairs = params.to_a
|
92
|
+
param_names = pairs.map(&:first)
|
93
|
+
param_values = pairs.map(&:last)
|
94
|
+
param_marks = Array.new(param_names.size, "?").join(", ")
|
95
|
+
|
96
|
+
insert_statement = "INSERT INTO #{table_name} (#{param_names.join(", ")}) VALUES (#{param_marks})"
|
97
|
+
|
98
|
+
@db.execute insert_statement, param_values
|
99
|
+
end
|
100
|
+
|
101
|
+
# Add a row to a table
|
102
|
+
#
|
103
|
+
# scope - A MotionRecord::Scope
|
104
|
+
# params - Hash of column names to values to update
|
105
|
+
def update(scope, params)
|
106
|
+
pairs = params.to_a
|
107
|
+
param_names = pairs.map(&:first)
|
108
|
+
param_values = pairs.map(&:last)
|
109
|
+
param_marks = param_names.map { |param| "#{param} = ?" }.join(", ")
|
110
|
+
|
111
|
+
update_statement = "UPDATE #{scope.klass.table_name} SET #{param_marks} #{scope.predicate}"
|
112
|
+
|
113
|
+
@db.execute update_statement, param_values + scope.predicate_values
|
114
|
+
end
|
115
|
+
|
116
|
+
# Delete rows from a table
|
117
|
+
#
|
118
|
+
# scope - MotionRecord::Scope defining the set of rows to delete
|
119
|
+
def delete(scope)
|
120
|
+
delete_statement = "DELETE FROM #{scope.klass.table_name} #{scope.predicate}"
|
121
|
+
|
122
|
+
@db.execute delete_statement, scope.predicate_values
|
123
|
+
end
|
124
|
+
|
125
|
+
# Run a calculation on a set of rows
|
126
|
+
#
|
127
|
+
# scope - MotionRecord::Scope which defines the set of rows
|
128
|
+
# method - one of :count, :maximum, :minimum, :sum, :average
|
129
|
+
# column - name of the column to run the calculation on
|
130
|
+
#
|
131
|
+
# Returns the numerical value of calculation or nil if there were no rows
|
132
|
+
# in the scope
|
133
|
+
def calculate(scope, method, column)
|
134
|
+
case method
|
135
|
+
when :count
|
136
|
+
calculation = "COUNT(#{column || "*"})"
|
137
|
+
when :maximum
|
138
|
+
calculation = "MAX(#{column})"
|
139
|
+
when :minimum
|
140
|
+
calculation = "MIN(#{column})"
|
141
|
+
when :sum
|
142
|
+
calculation = "SUM(#{column})"
|
143
|
+
when :average
|
144
|
+
calculation = "AVG(#{column})"
|
145
|
+
else
|
146
|
+
raise "Unrecognized calculation: #{method.inspect}"
|
147
|
+
end
|
148
|
+
|
149
|
+
calculate_statement = "SELECT #{calculation} AS #{method} FROM #{scope.klass.table_name} #{scope.predicate}"
|
150
|
+
|
151
|
+
if (row = @db.execute(calculate_statement, scope.predicate_values).first)
|
152
|
+
row[method]
|
153
|
+
else
|
154
|
+
nil
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module MotionRecord
|
2
|
+
module Persistence
|
3
|
+
|
4
|
+
def save!
|
5
|
+
persist!
|
6
|
+
end
|
7
|
+
|
8
|
+
def delete!
|
9
|
+
if persisted?
|
10
|
+
self.class.where(primary_key_condition).delete_all
|
11
|
+
else
|
12
|
+
raise "Can't delete unpersisted records"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def primary_key_condition
|
17
|
+
{self.class.primary_key => self.instance_variable_get("@#{self.class.primary_key}")}
|
18
|
+
end
|
19
|
+
|
20
|
+
def persisted?
|
21
|
+
!!@persisted
|
22
|
+
end
|
23
|
+
|
24
|
+
def mark_persisted!
|
25
|
+
@persisted = true
|
26
|
+
end
|
27
|
+
|
28
|
+
def mark_unpersisted!
|
29
|
+
@persisted = false
|
30
|
+
end
|
31
|
+
|
32
|
+
protected
|
33
|
+
|
34
|
+
def persist!
|
35
|
+
# HACK: Must ensure that attribute definitions are loaded from the table
|
36
|
+
self.class.table_columns
|
37
|
+
|
38
|
+
params = self.to_attribute_hash.reject { |k, _v| k == self.class.primary_key }
|
39
|
+
table_params = self.class.to_table_params(params)
|
40
|
+
|
41
|
+
if persisted?
|
42
|
+
self.class.where(primary_key_condition).update_all(table_params)
|
43
|
+
else
|
44
|
+
connection.insert self.class.table_name, table_params
|
45
|
+
end
|
46
|
+
|
47
|
+
self.mark_persisted!
|
48
|
+
end
|
49
|
+
|
50
|
+
module ClassMethods
|
51
|
+
include MotionRecord::ScopeHelpers::ClassMethods
|
52
|
+
include MotionRecord::Serialization::ClassMethods
|
53
|
+
|
54
|
+
def create!(attributes={})
|
55
|
+
self.new(attributes).save!
|
56
|
+
end
|
57
|
+
|
58
|
+
# Sybmol name of the primary key column
|
59
|
+
def primary_key
|
60
|
+
:id
|
61
|
+
end
|
62
|
+
|
63
|
+
def table_name
|
64
|
+
# HACK: poor-man's .pluralize
|
65
|
+
self.to_s.downcase + "s"
|
66
|
+
end
|
67
|
+
|
68
|
+
def table_columns
|
69
|
+
unless @table_columns
|
70
|
+
@table_columns = get_columns_from_schema.each_with_object({}) do |column, hash|
|
71
|
+
hash[column.name] = column
|
72
|
+
end
|
73
|
+
@table_columns.values.each do |column|
|
74
|
+
define_attribute_from_column(column)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@table_columns
|
78
|
+
end
|
79
|
+
|
80
|
+
protected
|
81
|
+
|
82
|
+
# Internal: Fetch column definitions from the database
|
83
|
+
def get_columns_from_schema
|
84
|
+
pragma_columns = connection.execute "PRAGMA table_info(#{table_name});"
|
85
|
+
pragma_columns.map { |p| Schema::ColumnDefinition.from_pragma(p) }
|
86
|
+
end
|
87
|
+
|
88
|
+
# Interal: Set up setter/getter methods to correspond with a table column
|
89
|
+
def define_attribute_from_column(column)
|
90
|
+
# TODO: handle options
|
91
|
+
define_attribute column.name, default: column.default
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def self.included(base)
|
96
|
+
base.extend(ClassMethods)
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|