lotus-model 0.3.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +53 -0
- data/lib/lotus/model/adapters/sql/query.rb +2 -1
- data/lib/lotus/model/configuration.rb +88 -0
- data/lib/lotus/model/migrator.rb +321 -0
- data/lib/lotus/model/migrator/adapter.rb +169 -0
- data/lib/lotus/model/migrator/mysql_adapter.rb +64 -0
- data/lib/lotus/model/migrator/postgres_adapter.rb +99 -0
- data/lib/lotus/model/migrator/sqlite_adapter.rb +110 -0
- data/lib/lotus/model/version.rb +1 -1
- data/lib/lotus/repository.rb +1 -1
- data/lotus-model.gemspec +1 -1
- metadata +10 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 446ed910e7e39e23fe19ceb56cf66ec8bc545145
|
4
|
+
data.tar.gz: 121c4c8b74f095a7a5e07396d6a37f2fdbeb6a5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4df597074019ffca04b8552579a0febd2382462ca6a9472be3b99b5a780b590f0485ec9872018a4dd9dcb31b1243f3f4838096aa4b4030882f36b81971b336f3
|
7
|
+
data.tar.gz: 5dd2f57855b6b08011e8b8e230b610b5f3c3a9edfd4d1f89203faa34b08b25664310d4713f44190c9c7aedc85ebf02c907bfa0aaccc2ee69d0dd39aa56077cef
|
data/CHANGELOG.md
CHANGED
@@ -1,6 +1,13 @@
|
|
1
1
|
# Lotus::Model
|
2
2
|
A persistence layer for Lotus
|
3
3
|
|
4
|
+
## v0.4.0 - 2015-06-23
|
5
|
+
### Added
|
6
|
+
- [Luca Guidi] Database migrations
|
7
|
+
|
8
|
+
### Changed
|
9
|
+
- [Matthew Bellantoni] Made `Repository.execute` not callable from the outside (private Ruby method, public API).
|
10
|
+
|
4
11
|
## v0.3.2 - 2015-05-22
|
5
12
|
### Added
|
6
13
|
- [Dmitry Tymchuk & Luca Guidi] Fix for dirty tracking of attributes changed in place (eg. `book.tags << 'non-fiction'`)
|
data/README.md
CHANGED
@@ -298,6 +298,36 @@ class ArticleRepository
|
|
298
298
|
end
|
299
299
|
```
|
300
300
|
|
301
|
+
You can also extract the common logic from your repository into a module to reuse it in other repositories. Here is a pagination example:
|
302
|
+
|
303
|
+
```ruby
|
304
|
+
module RepositoryHelpers
|
305
|
+
module Pagination
|
306
|
+
def paginate(limit: 10, offset: 0)
|
307
|
+
query do
|
308
|
+
limit(limit).offset(offset)
|
309
|
+
end
|
310
|
+
end
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
class ArticleRepository
|
315
|
+
include Lotus::Repository
|
316
|
+
extend RepositoryHelpers::Pagination
|
317
|
+
|
318
|
+
def self.published
|
319
|
+
query do
|
320
|
+
where(published: true)
|
321
|
+
end
|
322
|
+
end
|
323
|
+
|
324
|
+
# other repository-specific methods here
|
325
|
+
end
|
326
|
+
```
|
327
|
+
|
328
|
+
That will allow `.paginate` usage on `ArticleRepository`, for example:
|
329
|
+
`ArticleRepository.published.paginate(15, 0)`
|
330
|
+
|
301
331
|
**Your models and repositories have to be in the same namespace.** Otherwise `Lotus::Model::Mapper#load!`
|
302
332
|
will not initialize your repositories correctly.
|
303
333
|
|
@@ -413,6 +443,29 @@ In the example above, we reuse the adapter because the target tables (`people` a
|
|
413
443
|
|
414
444
|
An object that implements an interface for querying the database.
|
415
445
|
This interface may vary, according to the adapter's specifications.
|
446
|
+
|
447
|
+
Here is common interface for existing class:
|
448
|
+
|
449
|
+
* `.all` - Resolves the query by fetching records from the database and translating them into entities
|
450
|
+
* `.where`, `.and` - Adds a condition that behaves like SQL `WHERE`
|
451
|
+
* `.or` - Adds a condition that behaves like SQL `OR`
|
452
|
+
* `.exclude`, `.not` - Logical negation of a #where condition
|
453
|
+
* `.select` - Selects only the specified columns
|
454
|
+
* `.order`, `.asc` - Specify the ascending order of the records, sorted by the given columns
|
455
|
+
* `.reverse_order`, `.desc` - Specify the descending order of the records, sorted by the given columns
|
456
|
+
* `.limit` - Limit the number of records to return
|
457
|
+
* `.offset` - Specify an `OFFSET` clause. Due to SQL syntax restriction, offset MUST be used with `#limit`
|
458
|
+
* `.sum` - Returns the sum of the values for the given column
|
459
|
+
* `.average`, `.avg` - Returns the average of the values for the given column
|
460
|
+
* `.max` - Returns the maximum value for the given column
|
461
|
+
* `.min` - Returns the minimum value for the given column
|
462
|
+
* `.interval` - Returns the difference between the MAX and MIN for the given column
|
463
|
+
* `.range` - Returns a range of values between the MAX and the MIN for the given column
|
464
|
+
* `.exist?` - Checks if at least one record exists for the current conditions
|
465
|
+
* `.count` - Returns a count of the records for the current conditions
|
466
|
+
|
467
|
+
If you need more information regarding those methods, you can use comments from [memory](https://github.com/lotus/model/blob/master/lib/lotus/model/adapters/memory/query.rb#L29) or [sql](https://github.com/lotus/model/blob/master/lib/lotus/model/adapters/sql/query.rb#L28) adapters interface.
|
468
|
+
|
416
469
|
Think of an adapter for Redis, it will probably employ different strategies to filter records than an SQL query object.
|
417
470
|
|
418
471
|
### Conventions
|
@@ -207,13 +207,14 @@ module Lotus
|
|
207
207
|
# .exclude(company: 'enterprise')
|
208
208
|
#
|
209
209
|
# # => SELECT * FROM `projects` WHERE (`language` != 'java') AND (`company` != 'enterprise')
|
210
|
+
#
|
210
211
|
# @example Expressions
|
211
212
|
#
|
212
213
|
# query.exclude{ age > 31 }
|
213
214
|
#
|
214
215
|
# # => SELECT * FROM `users` WHERE (`age` <= 31)
|
215
216
|
def exclude(condition = nil, &blk)
|
216
|
-
_push_to_conditions(:exclude, condition || blk)
|
217
|
+
_push_to_conditions(:exclude, condition || blk)
|
217
218
|
self
|
218
219
|
end
|
219
220
|
|
@@ -10,6 +10,21 @@ module Lotus
|
|
10
10
|
#
|
11
11
|
# @since 0.2.0
|
12
12
|
class Configuration
|
13
|
+
# Default migrations path
|
14
|
+
#
|
15
|
+
# @since 0.4.0
|
16
|
+
# @api private
|
17
|
+
#
|
18
|
+
# @see Lotus::Model::Configuration#migrations
|
19
|
+
DEFAULT_MIGRATIONS_PATH = Pathname.new('db/migrations').freeze
|
20
|
+
|
21
|
+
# Default schema path
|
22
|
+
#
|
23
|
+
# @since 0.4.0
|
24
|
+
# @api private
|
25
|
+
#
|
26
|
+
# @see Lotus::Model::Configuration#schema
|
27
|
+
DEFAULT_SCHEMA_PATH = Pathname.new('db/schema.sql').freeze
|
13
28
|
|
14
29
|
# The persistence mapper
|
15
30
|
#
|
@@ -45,6 +60,8 @@ module Lotus
|
|
45
60
|
@adapter_config = nil
|
46
61
|
@mapper = NullMapper.new
|
47
62
|
@mapper_config = nil
|
63
|
+
@migrations = DEFAULT_MIGRATIONS_PATH
|
64
|
+
@schema = DEFAULT_SCHEMA_PATH
|
48
65
|
end
|
49
66
|
|
50
67
|
alias_method :unload!, :reset!
|
@@ -137,6 +154,77 @@ module Lotus
|
|
137
154
|
@mapper_config = Lotus::Model::Config::Mapper.new(path, &blk)
|
138
155
|
end
|
139
156
|
|
157
|
+
# Migrations directory
|
158
|
+
#
|
159
|
+
# It defaults to <tt>db/migrations</tt>.
|
160
|
+
#
|
161
|
+
# @overload migrations
|
162
|
+
# Get migrations directory
|
163
|
+
# @return [Pathname] migrations directory
|
164
|
+
#
|
165
|
+
# @overload migrations(path)
|
166
|
+
# Set migrations directory
|
167
|
+
# @param path [String,Pathname] the path
|
168
|
+
# @raise [Errno::ENOENT] if the given path doesn't exist
|
169
|
+
#
|
170
|
+
# @since 0.4.0
|
171
|
+
#
|
172
|
+
# @see Lotus::Model::Migrations::DEFAULT_MIGRATIONS_PATH
|
173
|
+
#
|
174
|
+
# @example Set Custom Path
|
175
|
+
# require 'lotus/model'
|
176
|
+
#
|
177
|
+
# Lotus::Model.configure do
|
178
|
+
# # ...
|
179
|
+
# migrations 'path/to/migrations'
|
180
|
+
# end
|
181
|
+
def migrations(path = nil)
|
182
|
+
if path.nil?
|
183
|
+
@migrations
|
184
|
+
else
|
185
|
+
@migrations = root.join(path).realpath
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
# Schema
|
190
|
+
#
|
191
|
+
# It defaults to <tt>db/schema.sql</tt>.
|
192
|
+
#
|
193
|
+
# @overload schema
|
194
|
+
# Get schema path
|
195
|
+
# @return [Pathname] schema path
|
196
|
+
#
|
197
|
+
# @overload schema(path)
|
198
|
+
# Set schema path
|
199
|
+
# @param path [String,Pathname] the path
|
200
|
+
#
|
201
|
+
# @since 0.4.0
|
202
|
+
#
|
203
|
+
# @see Lotus::Model::Migrations::DEFAULT_SCHEMA_PATH
|
204
|
+
#
|
205
|
+
# @example Set Custom Path
|
206
|
+
# require 'lotus/model'
|
207
|
+
#
|
208
|
+
# Lotus::Model.configure do
|
209
|
+
# # ...
|
210
|
+
# schema 'path/to/schema.sql'
|
211
|
+
# end
|
212
|
+
def schema(path = nil)
|
213
|
+
if path.nil?
|
214
|
+
@schema
|
215
|
+
else
|
216
|
+
@schema = root.join(path)
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Root directory
|
221
|
+
#
|
222
|
+
# @since 0.4.0
|
223
|
+
# @api private
|
224
|
+
def root
|
225
|
+
Lotus.respond_to?(:root) ? Lotus.root : Pathname.pwd
|
226
|
+
end
|
227
|
+
|
140
228
|
# Duplicate by copying the settings in a new instance.
|
141
229
|
#
|
142
230
|
# @return [Lotus::Model::Configuration] a copy of the configuration
|
@@ -0,0 +1,321 @@
|
|
1
|
+
require 'sequel'
|
2
|
+
require 'sequel/extensions/migration'
|
3
|
+
require 'lotus/model/migrator/adapter'
|
4
|
+
|
5
|
+
module Lotus
|
6
|
+
module Model
|
7
|
+
# Migration error
|
8
|
+
#
|
9
|
+
# @since 0.4.0
|
10
|
+
class MigrationError < ::StandardError
|
11
|
+
end
|
12
|
+
|
13
|
+
# Define a migration
|
14
|
+
#
|
15
|
+
# It must define an up/down strategy to write schema changes (up) and to
|
16
|
+
# rollback them (down).
|
17
|
+
#
|
18
|
+
# We can use <tt>up</tt> and <tt>down</tt> blocks for custom strategies, or
|
19
|
+
# only one <tt>change</tt> block that automatically implements "down" strategy.
|
20
|
+
#
|
21
|
+
# @param blk [Proc] a block that defines up/down or change database migration
|
22
|
+
#
|
23
|
+
# @since 0.4.0
|
24
|
+
#
|
25
|
+
# @example Use up/down blocks
|
26
|
+
# Lotus::Model.migration do
|
27
|
+
# up do
|
28
|
+
# create_table :books do
|
29
|
+
# primary_key :id
|
30
|
+
# column :book, String
|
31
|
+
# end
|
32
|
+
# end
|
33
|
+
#
|
34
|
+
# down do
|
35
|
+
# drop_table :books
|
36
|
+
# end
|
37
|
+
# end
|
38
|
+
#
|
39
|
+
# @example Use change block
|
40
|
+
# Lotus::Model.migration do
|
41
|
+
# change do
|
42
|
+
# create_table :books do
|
43
|
+
# primary_key :id
|
44
|
+
# column :book, String
|
45
|
+
# end
|
46
|
+
# end
|
47
|
+
#
|
48
|
+
# # DOWN strategy is automatically generated
|
49
|
+
# end
|
50
|
+
def self.migration(&blk)
|
51
|
+
Sequel.migration(&blk)
|
52
|
+
end
|
53
|
+
|
54
|
+
# Database schema migrator
|
55
|
+
#
|
56
|
+
# @since 0.4.0
|
57
|
+
module Migrator
|
58
|
+
# Create database defined by current configuration.
|
59
|
+
#
|
60
|
+
# It's only implemented for the following databases:
|
61
|
+
#
|
62
|
+
# * SQLite3
|
63
|
+
# * PostgreSQL
|
64
|
+
# * MySQL
|
65
|
+
#
|
66
|
+
# @raise [Lotus::Model::MigrationError] if an error occurs
|
67
|
+
#
|
68
|
+
# @since 0.4.0
|
69
|
+
#
|
70
|
+
# @see Lotus::Model::Configuration#adapter
|
71
|
+
#
|
72
|
+
# @example
|
73
|
+
# require 'lotus/model'
|
74
|
+
# require 'lotus/model/migrator'
|
75
|
+
#
|
76
|
+
# Lotus::Model.configure do
|
77
|
+
# # ...
|
78
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
79
|
+
# end
|
80
|
+
#
|
81
|
+
# Lotus::Model::Migrator.create # Creates `foo' database
|
82
|
+
def self.create
|
83
|
+
adapter(connection).create
|
84
|
+
end
|
85
|
+
|
86
|
+
# Drop database defined by current configuration.
|
87
|
+
#
|
88
|
+
# It's only implemented for the following databases:
|
89
|
+
#
|
90
|
+
# * SQLite3
|
91
|
+
# * PostgreSQL
|
92
|
+
# * MySQL
|
93
|
+
#
|
94
|
+
# @raise [Lotus::Model::MigrationError] if an error occurs
|
95
|
+
#
|
96
|
+
# @since 0.4.0
|
97
|
+
#
|
98
|
+
# @see Lotus::Model::Configuration#adapter
|
99
|
+
#
|
100
|
+
# @example
|
101
|
+
# require 'lotus/model'
|
102
|
+
# require 'lotus/model/migrator'
|
103
|
+
#
|
104
|
+
# Lotus::Model.configure do
|
105
|
+
# # ...
|
106
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
107
|
+
# end
|
108
|
+
#
|
109
|
+
# Lotus::Model::Migrator.drop # Drops `foo' database
|
110
|
+
def self.drop
|
111
|
+
adapter(connection).drop
|
112
|
+
end
|
113
|
+
|
114
|
+
# Migrate database schema
|
115
|
+
#
|
116
|
+
# It's possible to migrate "down" by specifying a version
|
117
|
+
# (eg. <tt>"20150610133853"</tt>)
|
118
|
+
#
|
119
|
+
# @param version [String,NilClass] target version
|
120
|
+
#
|
121
|
+
# @raise [Lotus::Model::MigrationError] if an error occurs
|
122
|
+
#
|
123
|
+
# @since 0.4.0
|
124
|
+
#
|
125
|
+
# @see Lotus::Model::Configuration#adapter
|
126
|
+
# @see Lotus::Model::Configuration#migrations
|
127
|
+
#
|
128
|
+
# @example Migrate Up
|
129
|
+
# require 'lotus/model'
|
130
|
+
# require 'lotus/model/migrator'
|
131
|
+
#
|
132
|
+
# Lotus::Model.configure do
|
133
|
+
# # ...
|
134
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
135
|
+
# migrations 'db/migrations'
|
136
|
+
# end
|
137
|
+
#
|
138
|
+
# # Reads all files from "db/migrations" and apply them
|
139
|
+
# Lotus::Model::Migrator.migrate
|
140
|
+
#
|
141
|
+
# @example Migrate Down
|
142
|
+
# require 'lotus/model'
|
143
|
+
# require 'lotus/model/migrator'
|
144
|
+
#
|
145
|
+
# Lotus::Model.configure do
|
146
|
+
# # ...
|
147
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
148
|
+
# migrations 'db/migrations'
|
149
|
+
# end
|
150
|
+
#
|
151
|
+
# # Reads all files from "db/migrations" and apply them
|
152
|
+
# Lotus::Model::Migrator.migrate
|
153
|
+
#
|
154
|
+
# # Migrate to a specifiy version
|
155
|
+
# Lotus::Model::Migrator.migrate(version: "20150610133853")
|
156
|
+
def self.migrate(version: nil)
|
157
|
+
version = Integer(version) unless version.nil?
|
158
|
+
|
159
|
+
Sequel::Migrator.run(connection, migrations, target: version, allow_missing_migration_files: true) if migrations?
|
160
|
+
rescue Sequel::Migrator::Error => e
|
161
|
+
raise MigrationError.new(e.message)
|
162
|
+
end
|
163
|
+
|
164
|
+
# Migrate, dump schema, delete migrations.
|
165
|
+
#
|
166
|
+
# This is an experimental feature.
|
167
|
+
# It may change or be removed in the future.
|
168
|
+
#
|
169
|
+
# Actively developed applications accumulate tons of migrations.
|
170
|
+
# In the long term they are hard to maintain and slow to execute.
|
171
|
+
#
|
172
|
+
# "Apply" feature solves this problem.
|
173
|
+
#
|
174
|
+
# It keeps an updated SQL file with the structure of the database.
|
175
|
+
# This file can be used to create fresh databases for developer machines
|
176
|
+
# or during testing. This is faster than to run dozen or hundred migrations.
|
177
|
+
#
|
178
|
+
# When we use "apply", it eliminates all the migrations that are no longer
|
179
|
+
# necessary.
|
180
|
+
#
|
181
|
+
# @raise [Lotus::Model::MigrationError] if an error occurs
|
182
|
+
#
|
183
|
+
# @since 0.4.0
|
184
|
+
#
|
185
|
+
# @see Lotus::Model::Configuration#adapter
|
186
|
+
# @see Lotus::Model::Configuration#migrations
|
187
|
+
#
|
188
|
+
# @example Apply Migrations
|
189
|
+
# require 'lotus/model'
|
190
|
+
# require 'lotus/model/migrator'
|
191
|
+
#
|
192
|
+
# Lotus::Model.configure do
|
193
|
+
# # ...
|
194
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
195
|
+
# migrations 'db/migrations'
|
196
|
+
# schema 'db/schema.sql'
|
197
|
+
# end
|
198
|
+
#
|
199
|
+
# # Reads all files from "db/migrations" and apply and delete them.
|
200
|
+
# # It generates an updated version of "db/schema.sql"
|
201
|
+
# Lotus::Model::Migrator.apply
|
202
|
+
def self.apply
|
203
|
+
migrate
|
204
|
+
adapter(connection).dump
|
205
|
+
delete_migrations
|
206
|
+
end
|
207
|
+
|
208
|
+
# Prepare database: drop, create, load schema (if any), migrate.
|
209
|
+
#
|
210
|
+
# This is designed for development machines and testing mode.
|
211
|
+
# It works faster if used with <tt>apply</tt>.
|
212
|
+
#
|
213
|
+
# @raise [Lotus::Model::MigrationError] if an error occurs
|
214
|
+
#
|
215
|
+
# @since 0.4.0
|
216
|
+
#
|
217
|
+
# @see Lotus::Model::Migrator.apply
|
218
|
+
#
|
219
|
+
# @example Prepare Database
|
220
|
+
# require 'lotus/model'
|
221
|
+
# require 'lotus/model/migrator'
|
222
|
+
#
|
223
|
+
# Lotus::Model.configure do
|
224
|
+
# # ...
|
225
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
226
|
+
# migrations 'db/migrations'
|
227
|
+
# end
|
228
|
+
#
|
229
|
+
# Lotus::Model::Migrator.prepare # => creates `foo' and run migrations
|
230
|
+
#
|
231
|
+
# @example Prepare Database (with schema dump)
|
232
|
+
# require 'lotus/model'
|
233
|
+
# require 'lotus/model/migrator'
|
234
|
+
#
|
235
|
+
# Lotus::Model.configure do
|
236
|
+
# # ...
|
237
|
+
# adapter type: :sql, uri: 'postgres://localhost/foo'
|
238
|
+
# migrations 'db/migrations'
|
239
|
+
# schema 'db/schema.sql'
|
240
|
+
# end
|
241
|
+
#
|
242
|
+
# Lotus::Model::Migrator.apply # => updates schema dump
|
243
|
+
# Lotus::Model::Migrator.prepare # => creates `foo', load schema and run pending migrations (if any)
|
244
|
+
def self.prepare
|
245
|
+
drop rescue nil
|
246
|
+
create
|
247
|
+
adapter(connection).load
|
248
|
+
migrate
|
249
|
+
end
|
250
|
+
|
251
|
+
# Return current database version timestamp
|
252
|
+
#
|
253
|
+
# If no migrations were ran, it returns <tt>nil</tt>.
|
254
|
+
#
|
255
|
+
# @return [String,NilClass] current version, if previously migrated
|
256
|
+
#
|
257
|
+
# @since 0.4.0
|
258
|
+
#
|
259
|
+
# @example
|
260
|
+
# # Given last migrations is:
|
261
|
+
# # 20150610133853_create_books.rb
|
262
|
+
#
|
263
|
+
# Lotus::Model::Migrator.version # => "20150610133853"
|
264
|
+
def self.version
|
265
|
+
adapter(connection).version
|
266
|
+
end
|
267
|
+
|
268
|
+
private
|
269
|
+
|
270
|
+
# Loads an adapter for the given connection
|
271
|
+
#
|
272
|
+
# @since 0.4.0
|
273
|
+
# @api private
|
274
|
+
def self.adapter(connection)
|
275
|
+
Adapter.for(connection)
|
276
|
+
end
|
277
|
+
|
278
|
+
# Delete all the migrations
|
279
|
+
#
|
280
|
+
# @since 0.4.0
|
281
|
+
# @api private
|
282
|
+
def self.delete_migrations
|
283
|
+
migrations.each_child(&:delete)
|
284
|
+
end
|
285
|
+
|
286
|
+
# Database connection
|
287
|
+
#
|
288
|
+
# @since 0.4.0
|
289
|
+
# @api private
|
290
|
+
def self.connection
|
291
|
+
Sequel.connect(
|
292
|
+
configuration.adapter.uri
|
293
|
+
)
|
294
|
+
end
|
295
|
+
|
296
|
+
# Lotus::Model configuration
|
297
|
+
#
|
298
|
+
# @since 0.4.0
|
299
|
+
# @api private
|
300
|
+
def self.configuration
|
301
|
+
Model.configuration
|
302
|
+
end
|
303
|
+
|
304
|
+
# Migrations directory
|
305
|
+
#
|
306
|
+
# @since 0.4.0
|
307
|
+
# @api private
|
308
|
+
def self.migrations
|
309
|
+
configuration.migrations
|
310
|
+
end
|
311
|
+
|
312
|
+
# Check if there are migrations
|
313
|
+
#
|
314
|
+
# @since 0.4.0
|
315
|
+
# @api private
|
316
|
+
def self.migrations?
|
317
|
+
migrations.children.any?
|
318
|
+
end
|
319
|
+
end
|
320
|
+
end
|
321
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'uri'
|
2
|
+
require 'shellwords'
|
3
|
+
|
4
|
+
module Lotus
|
5
|
+
module Model
|
6
|
+
module Migrator
|
7
|
+
# Migrator base adapter
|
8
|
+
#
|
9
|
+
# @since 0.4.0
|
10
|
+
# @api private
|
11
|
+
class Adapter
|
12
|
+
# Migrations table to store migrations metadata.
|
13
|
+
#
|
14
|
+
# @since 0.4.0
|
15
|
+
# @api private
|
16
|
+
MIGRATIONS_TABLE = :schema_migrations
|
17
|
+
|
18
|
+
# Migrations table version column
|
19
|
+
#
|
20
|
+
# @since 0.4.0
|
21
|
+
# @api private
|
22
|
+
MIGRATIONS_TABLE_VERSION_COLUMN = :filename
|
23
|
+
|
24
|
+
# Loads and returns a specific adapter for the given connection.
|
25
|
+
#
|
26
|
+
# @since 0.4.0
|
27
|
+
# @api private
|
28
|
+
def self.for(connection)
|
29
|
+
case connection.database_type
|
30
|
+
when :sqlite
|
31
|
+
require 'lotus/model/migrator/sqlite_adapter'
|
32
|
+
SQLiteAdapter
|
33
|
+
when :postgres
|
34
|
+
require 'lotus/model/migrator/postgres_adapter'
|
35
|
+
PostgresAdapter
|
36
|
+
when :mysql
|
37
|
+
require 'lotus/model/migrator/mysql_adapter'
|
38
|
+
MySQLAdapter
|
39
|
+
else
|
40
|
+
self
|
41
|
+
end.new(connection)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Initialize an adapter
|
45
|
+
#
|
46
|
+
# @since 0.4.0
|
47
|
+
# @api private
|
48
|
+
def initialize(connection)
|
49
|
+
@connection = connection
|
50
|
+
end
|
51
|
+
|
52
|
+
# Create database.
|
53
|
+
# It must be implemented by subclasses.
|
54
|
+
#
|
55
|
+
# @since 0.4.0
|
56
|
+
# @api private
|
57
|
+
#
|
58
|
+
# @see Lotus::Model::Migrator.create
|
59
|
+
def create
|
60
|
+
raise MigrationError.new("Current adapter (#{ @connection.database_type }) doesn't support create.")
|
61
|
+
end
|
62
|
+
|
63
|
+
# Drop database.
|
64
|
+
# It must be implemented by subclasses.
|
65
|
+
#
|
66
|
+
# @since 0.4.0
|
67
|
+
# @api private
|
68
|
+
#
|
69
|
+
# @see Lotus::Model::Migrator.drop
|
70
|
+
def drop
|
71
|
+
raise MigrationError.new("Current adapter (#{ @connection.database_type }) doesn't support drop.")
|
72
|
+
end
|
73
|
+
|
74
|
+
# Load database schema.
|
75
|
+
# It must be implemented by subclasses.
|
76
|
+
#
|
77
|
+
# @since 0.4.0
|
78
|
+
# @api private
|
79
|
+
#
|
80
|
+
# @see Lotus::Model::Migrator.prepare
|
81
|
+
def load
|
82
|
+
raise MigrationError.new("Current adapter (#{ @connection.database_type }) doesn't support load.")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Database version.
|
86
|
+
#
|
87
|
+
# @since 0.4.0
|
88
|
+
# @api private
|
89
|
+
def version
|
90
|
+
return unless @connection.tables.include?(MIGRATIONS_TABLE)
|
91
|
+
|
92
|
+
if record = @connection[MIGRATIONS_TABLE].order(MIGRATIONS_TABLE_VERSION_COLUMN).last
|
93
|
+
record.fetch(MIGRATIONS_TABLE_VERSION_COLUMN).scan(/\A[\d]{14}/).first.to_s
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
private
|
98
|
+
|
99
|
+
# @since 0.4.0
|
100
|
+
# @api private
|
101
|
+
def new_connection
|
102
|
+
uri = URI.parse(@connection.uri)
|
103
|
+
scheme, userinfo, host, port = uri.select(:scheme, :userinfo, :host, :port)
|
104
|
+
|
105
|
+
uri = "#{ scheme }://"
|
106
|
+
uri += "#{ userinfo }@" unless userinfo.nil?
|
107
|
+
uri += host
|
108
|
+
uri += ":#{ port }" unless port.nil?
|
109
|
+
|
110
|
+
Sequel.connect(uri)
|
111
|
+
end
|
112
|
+
|
113
|
+
# @since 0.4.0
|
114
|
+
# @api private
|
115
|
+
def database
|
116
|
+
escape options.fetch(:database)
|
117
|
+
end
|
118
|
+
|
119
|
+
# @since 0.4.0
|
120
|
+
# @api private
|
121
|
+
def host
|
122
|
+
escape options.fetch(:host)
|
123
|
+
end
|
124
|
+
|
125
|
+
# @since 0.4.0
|
126
|
+
# @api private
|
127
|
+
def port
|
128
|
+
escape options.fetch(:port)
|
129
|
+
end
|
130
|
+
|
131
|
+
# @since 0.4.0
|
132
|
+
# @api private
|
133
|
+
def username
|
134
|
+
escape options.fetch(:user)
|
135
|
+
end
|
136
|
+
|
137
|
+
# @since 0.4.0
|
138
|
+
# @api private
|
139
|
+
def password
|
140
|
+
escape options.fetch(:password)
|
141
|
+
end
|
142
|
+
|
143
|
+
# @since 0.4.0
|
144
|
+
# @api private
|
145
|
+
def schema
|
146
|
+
Model.configuration.schema
|
147
|
+
end
|
148
|
+
|
149
|
+
# @since 0.4.0
|
150
|
+
# @api private
|
151
|
+
def migrations_table
|
152
|
+
escape MIGRATIONS_TABLE
|
153
|
+
end
|
154
|
+
|
155
|
+
# @since 0.4.0
|
156
|
+
# @api private
|
157
|
+
def options
|
158
|
+
@connection.opts
|
159
|
+
end
|
160
|
+
|
161
|
+
# @since 0.4.0
|
162
|
+
# @api private
|
163
|
+
def escape(string)
|
164
|
+
Shellwords.escape(string) unless string.nil?
|
165
|
+
end
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# MySQL adapter
|
5
|
+
#
|
6
|
+
# @since 0.4.0
|
7
|
+
# @api private
|
8
|
+
class MySQLAdapter < Adapter
|
9
|
+
# @since 0.4.0
|
10
|
+
# @api private
|
11
|
+
def create
|
12
|
+
new_connection.run %(CREATE DATABASE #{ database };)
|
13
|
+
end
|
14
|
+
|
15
|
+
# @since 0.4.0
|
16
|
+
# @api private
|
17
|
+
def drop
|
18
|
+
new_connection.run %(DROP DATABASE #{ database };)
|
19
|
+
rescue Sequel::DatabaseError => e
|
20
|
+
message = if e.message.match(/doesn\'t exist/)
|
21
|
+
"Cannot find database: #{ database }"
|
22
|
+
else
|
23
|
+
e.message
|
24
|
+
end
|
25
|
+
|
26
|
+
raise MigrationError.new(message)
|
27
|
+
end
|
28
|
+
|
29
|
+
# @since 0.4.0
|
30
|
+
# @api private
|
31
|
+
def dump
|
32
|
+
dump_structure
|
33
|
+
dump_migrations_data
|
34
|
+
end
|
35
|
+
|
36
|
+
# @since 0.4.0
|
37
|
+
# @api private
|
38
|
+
def load
|
39
|
+
load_structure
|
40
|
+
end
|
41
|
+
|
42
|
+
private
|
43
|
+
|
44
|
+
# @since 0.4.0
|
45
|
+
# @api private
|
46
|
+
def dump_structure
|
47
|
+
system "mysqldump --user=#{ username } --password=#{ password } --no-data --skip-comments --ignore-table=#{ database }.#{ migrations_table } #{ database } > #{ schema }"
|
48
|
+
end
|
49
|
+
|
50
|
+
# @since 0.4.0
|
51
|
+
# @api private
|
52
|
+
def load_structure
|
53
|
+
system "mysql --user=#{ username } --password=#{ password } #{ database } < #{ escape(schema) }" if schema.exist?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @since 0.4.0
|
57
|
+
# @api private
|
58
|
+
def dump_migrations_data
|
59
|
+
system "mysqldump --user=#{ username } --password=#{ password } --skip-comments #{ database } #{ migrations_table } >> #{ schema }"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
module Lotus
|
2
|
+
module Model
|
3
|
+
module Migrator
|
4
|
+
# PostgreSQL adapter
|
5
|
+
#
|
6
|
+
# @since 0.4.0
|
7
|
+
# @api private
|
8
|
+
class PostgresAdapter < Adapter
|
9
|
+
# @since 0.4.0
|
10
|
+
# @api private
|
11
|
+
HOST = 'PGHOST'.freeze
|
12
|
+
|
13
|
+
# @since 0.4.0
|
14
|
+
# @api private
|
15
|
+
PORT = 'PGPORT'.freeze
|
16
|
+
|
17
|
+
# @since 0.4.0
|
18
|
+
# @api private
|
19
|
+
USER = 'PGUSER'.freeze
|
20
|
+
|
21
|
+
# @since 0.4.0
|
22
|
+
# @api private
|
23
|
+
PASSWORD = 'PGPASSWORD'.freeze
|
24
|
+
|
25
|
+
# @since 0.4.0
|
26
|
+
# @api private
|
27
|
+
def create
|
28
|
+
new_connection.run %(CREATE DATABASE "#{ database }"#{ create_options })
|
29
|
+
end
|
30
|
+
|
31
|
+
# @since 0.4.0
|
32
|
+
# @api private
|
33
|
+
def drop
|
34
|
+
new_connection.run %(DROP DATABASE "#{ database }")
|
35
|
+
rescue Sequel::DatabaseError => e
|
36
|
+
message = if e.message.match(/does not exist/)
|
37
|
+
"Cannot find database: #{ database }"
|
38
|
+
else
|
39
|
+
e.message
|
40
|
+
end
|
41
|
+
|
42
|
+
raise MigrationError.new(message)
|
43
|
+
end
|
44
|
+
|
45
|
+
# @since 0.4.0
|
46
|
+
# @api private
|
47
|
+
def dump
|
48
|
+
set_environment_variables
|
49
|
+
dump_structure
|
50
|
+
dump_migrations_data
|
51
|
+
end
|
52
|
+
|
53
|
+
# @since 0.4.0
|
54
|
+
# @api private
|
55
|
+
def load
|
56
|
+
set_environment_variables
|
57
|
+
load_structure
|
58
|
+
end
|
59
|
+
|
60
|
+
private
|
61
|
+
|
62
|
+
# @since 0.4.0
|
63
|
+
# @api private
|
64
|
+
def create_options
|
65
|
+
result = ""
|
66
|
+
result += %( OWNER "#{ username }") unless username.nil?
|
67
|
+
result
|
68
|
+
end
|
69
|
+
|
70
|
+
# @since 0.4.0
|
71
|
+
# @api private
|
72
|
+
def set_environment_variables
|
73
|
+
ENV[HOST] = host unless host.nil?
|
74
|
+
ENV[PORT] = port.to_s unless port.nil?
|
75
|
+
ENV[PASSWORD] = password unless password.nil?
|
76
|
+
ENV[USER] = username unless username.nil?
|
77
|
+
end
|
78
|
+
|
79
|
+
# @since 0.4.0
|
80
|
+
# @api private
|
81
|
+
def dump_structure
|
82
|
+
system "pg_dump -i -s -x -O -T #{ migrations_table } -f #{ escape(schema) } #{ database }"
|
83
|
+
end
|
84
|
+
|
85
|
+
# @since 0.4.0
|
86
|
+
# @api private
|
87
|
+
def load_structure
|
88
|
+
system "psql -X -q -f #{ escape(schema) } #{ database }" if schema.exist?
|
89
|
+
end
|
90
|
+
|
91
|
+
# @since 0.4.0
|
92
|
+
# @api private
|
93
|
+
def dump_migrations_data
|
94
|
+
system "pg_dump -t #{ migrations_table } #{ database } >> #{ escape(schema) }"
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
|
3
|
+
module Lotus
|
4
|
+
module Model
|
5
|
+
module Migrator
|
6
|
+
# SQLite3 Migrator
|
7
|
+
#
|
8
|
+
# @since 0.4.0
|
9
|
+
# @api private
|
10
|
+
class SQLiteAdapter < Adapter
|
11
|
+
# No-op for in-memory databases
|
12
|
+
#
|
13
|
+
# @since 0.4.0
|
14
|
+
# @api private
|
15
|
+
module Memory
|
16
|
+
# @since 0.4.0
|
17
|
+
# @api private
|
18
|
+
def create
|
19
|
+
end
|
20
|
+
|
21
|
+
# @since 0.4.0
|
22
|
+
# @api private
|
23
|
+
def drop
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
# Initialize adapter
|
28
|
+
#
|
29
|
+
# @since 0.4.0
|
30
|
+
# @api private
|
31
|
+
def initialize(connection)
|
32
|
+
super
|
33
|
+
extend Memory if memory?
|
34
|
+
end
|
35
|
+
|
36
|
+
# @since 0.4.0
|
37
|
+
# @api private
|
38
|
+
def create
|
39
|
+
path.dirname.mkpath
|
40
|
+
FileUtils.touch(path)
|
41
|
+
rescue Errno::EACCES
|
42
|
+
raise MigrationError.new("Permission denied: #{ path.sub(/\A\/\//, '') }")
|
43
|
+
end
|
44
|
+
|
45
|
+
# @since 0.4.0
|
46
|
+
# @api private
|
47
|
+
def drop
|
48
|
+
path.delete
|
49
|
+
rescue Errno::ENOENT
|
50
|
+
raise MigrationError.new("Cannot find database: #{ path.sub(/\A\/\//, '') }")
|
51
|
+
end
|
52
|
+
|
53
|
+
# @since 0.4.0
|
54
|
+
# @api private
|
55
|
+
def dump
|
56
|
+
dump_structure
|
57
|
+
dump_migrations_data
|
58
|
+
end
|
59
|
+
|
60
|
+
# @since 0.4.0
|
61
|
+
# @api private
|
62
|
+
def load
|
63
|
+
load_structure
|
64
|
+
end
|
65
|
+
|
66
|
+
private
|
67
|
+
|
68
|
+
# @since 0.4.0
|
69
|
+
# @api private
|
70
|
+
def path
|
71
|
+
root.join(
|
72
|
+
@connection.uri.sub(/#{ @connection.adapter_scheme }\:\/\//, '')
|
73
|
+
)
|
74
|
+
end
|
75
|
+
|
76
|
+
# @since 0.4.0
|
77
|
+
# @api private
|
78
|
+
def root
|
79
|
+
Lotus::Model.configuration.root
|
80
|
+
end
|
81
|
+
|
82
|
+
# @since 0.4.0
|
83
|
+
# @api private
|
84
|
+
def memory?
|
85
|
+
uri = path.to_s
|
86
|
+
uri.match(/sqlite\:\/\z/) ||
|
87
|
+
uri.match(/\:memory\:/)
|
88
|
+
end
|
89
|
+
|
90
|
+
# @since 0.4.0
|
91
|
+
# @api private
|
92
|
+
def dump_structure
|
93
|
+
system "sqlite3 #{ escape(path) } .schema > #{ escape(schema) }"
|
94
|
+
end
|
95
|
+
|
96
|
+
# @since 0.4.0
|
97
|
+
# @api private
|
98
|
+
def load_structure
|
99
|
+
system "sqlite3 #{ escape(path) } < #{ escape(schema) }" if schema.exist?
|
100
|
+
end
|
101
|
+
|
102
|
+
# @since 0.4.0
|
103
|
+
# @api private
|
104
|
+
def dump_migrations_data
|
105
|
+
system %(sqlite3 #{ escape(path) } .dump | grep '^INSERT INTO "#{ migrations_table }"' >> #{ escape(schema) })
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
data/lib/lotus/model/version.rb
CHANGED
data/lib/lotus/repository.rb
CHANGED
@@ -551,6 +551,7 @@ module Lotus
|
|
551
551
|
end
|
552
552
|
end
|
553
553
|
|
554
|
+
private
|
554
555
|
# Executes the given raw statement on the adapter.
|
555
556
|
#
|
556
557
|
# Please note that it's only supported by some databases,
|
@@ -595,7 +596,6 @@ module Lotus
|
|
595
596
|
@adapter.execute(raw)
|
596
597
|
end
|
597
598
|
|
598
|
-
private
|
599
599
|
# Fabricates a query and yields the given block to access the low level
|
600
600
|
# APIs exposed by the query itself.
|
601
601
|
#
|
data/lotus-model.gemspec
CHANGED
@@ -19,7 +19,7 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.require_paths = ['lib']
|
20
20
|
spec.required_ruby_version = '>= 2.0.0'
|
21
21
|
|
22
|
-
spec.add_runtime_dependency 'lotus-utils', '~> 0.
|
22
|
+
spec.add_runtime_dependency 'lotus-utils', '~> 0.5'
|
23
23
|
spec.add_runtime_dependency 'sequel', '~> 4.9'
|
24
24
|
|
25
25
|
spec.add_development_dependency 'bundler', '~> 1.6'
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: lotus-model
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Luca Guidi
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date: 2015-
|
12
|
+
date: 2015-06-23 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: lotus-utils
|
@@ -17,14 +17,14 @@ dependencies:
|
|
17
17
|
requirements:
|
18
18
|
- - "~>"
|
19
19
|
- !ruby/object:Gem::Version
|
20
|
-
version: '0.
|
20
|
+
version: '0.5'
|
21
21
|
type: :runtime
|
22
22
|
prerelease: false
|
23
23
|
version_requirements: !ruby/object:Gem::Requirement
|
24
24
|
requirements:
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
|
-
version: '0.
|
27
|
+
version: '0.5'
|
28
28
|
- !ruby/object:Gem::Dependency
|
29
29
|
name: sequel
|
30
30
|
requirement: !ruby/object:Gem::Requirement
|
@@ -122,6 +122,11 @@ files:
|
|
122
122
|
- lib/lotus/model/mapping/coercer.rb
|
123
123
|
- lib/lotus/model/mapping/coercions.rb
|
124
124
|
- lib/lotus/model/mapping/collection.rb
|
125
|
+
- lib/lotus/model/migrator.rb
|
126
|
+
- lib/lotus/model/migrator/adapter.rb
|
127
|
+
- lib/lotus/model/migrator/mysql_adapter.rb
|
128
|
+
- lib/lotus/model/migrator/postgres_adapter.rb
|
129
|
+
- lib/lotus/model/migrator/sqlite_adapter.rb
|
125
130
|
- lib/lotus/model/version.rb
|
126
131
|
- lib/lotus/repository.rb
|
127
132
|
- lotus-model.gemspec
|
@@ -145,7 +150,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
145
150
|
version: '0'
|
146
151
|
requirements: []
|
147
152
|
rubyforge_project:
|
148
|
-
rubygems_version: 2.4.
|
153
|
+
rubygems_version: 2.4.8
|
149
154
|
signing_key:
|
150
155
|
specification_version: 4
|
151
156
|
summary: A persistence layer for Lotus
|