lotus-model 0.3.2 → 0.4.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/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
|