motion_record 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Code Climate](https://codeclimate.com/github/magoosh/motion_record/badges/gpa.svg)](https://codeclimate.com/github/magoosh/motion_record) [![Test Coverage](https://codeclimate.com/github/magoosh/motion_record/badges/coverage.svg)](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
|