hanami-model 0.0.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -0
  3. data/EXAMPLE.md +212 -0
  4. data/LICENSE.md +22 -0
  5. data/README.md +600 -7
  6. data/hanami-model.gemspec +17 -12
  7. data/lib/hanami-model.rb +1 -0
  8. data/lib/hanami/entity.rb +298 -0
  9. data/lib/hanami/entity/dirty_tracking.rb +74 -0
  10. data/lib/hanami/model.rb +204 -2
  11. data/lib/hanami/model/adapters/abstract.rb +281 -0
  12. data/lib/hanami/model/adapters/file_system_adapter.rb +288 -0
  13. data/lib/hanami/model/adapters/implementation.rb +111 -0
  14. data/lib/hanami/model/adapters/memory/collection.rb +132 -0
  15. data/lib/hanami/model/adapters/memory/command.rb +113 -0
  16. data/lib/hanami/model/adapters/memory/query.rb +653 -0
  17. data/lib/hanami/model/adapters/memory_adapter.rb +179 -0
  18. data/lib/hanami/model/adapters/null_adapter.rb +24 -0
  19. data/lib/hanami/model/adapters/sql/collection.rb +287 -0
  20. data/lib/hanami/model/adapters/sql/command.rb +73 -0
  21. data/lib/hanami/model/adapters/sql/console.rb +33 -0
  22. data/lib/hanami/model/adapters/sql/consoles/mysql.rb +49 -0
  23. data/lib/hanami/model/adapters/sql/consoles/postgresql.rb +48 -0
  24. data/lib/hanami/model/adapters/sql/consoles/sqlite.rb +26 -0
  25. data/lib/hanami/model/adapters/sql/query.rb +788 -0
  26. data/lib/hanami/model/adapters/sql_adapter.rb +296 -0
  27. data/lib/hanami/model/coercer.rb +74 -0
  28. data/lib/hanami/model/config/adapter.rb +116 -0
  29. data/lib/hanami/model/config/mapper.rb +45 -0
  30. data/lib/hanami/model/configuration.rb +275 -0
  31. data/lib/hanami/model/error.rb +7 -0
  32. data/lib/hanami/model/mapper.rb +124 -0
  33. data/lib/hanami/model/mapping.rb +48 -0
  34. data/lib/hanami/model/mapping/attribute.rb +85 -0
  35. data/lib/hanami/model/mapping/coercers.rb +314 -0
  36. data/lib/hanami/model/mapping/collection.rb +490 -0
  37. data/lib/hanami/model/mapping/collection_coercer.rb +79 -0
  38. data/lib/hanami/model/migrator.rb +324 -0
  39. data/lib/hanami/model/migrator/adapter.rb +170 -0
  40. data/lib/hanami/model/migrator/connection.rb +133 -0
  41. data/lib/hanami/model/migrator/mysql_adapter.rb +72 -0
  42. data/lib/hanami/model/migrator/postgres_adapter.rb +119 -0
  43. data/lib/hanami/model/migrator/sqlite_adapter.rb +110 -0
  44. data/lib/hanami/model/version.rb +4 -1
  45. data/lib/hanami/repository.rb +872 -0
  46. metadata +100 -16
  47. data/.gitignore +0 -9
  48. data/Gemfile +0 -4
  49. data/Rakefile +0 -2
  50. data/bin/console +0 -14
  51. data/bin/setup +0 -8
@@ -0,0 +1,79 @@
1
+ module Hanami
2
+ module Model
3
+ module Mapping
4
+ # Translates values from/to the database with the corresponding Ruby type.
5
+ #
6
+ # @api private
7
+ # @since 0.1.0
8
+ class CollectionCoercer
9
+ # Initialize a coercer for the given collection.
10
+ #
11
+ # @param collection [Hanami::Model::Mapping::Collection] the collection
12
+ #
13
+ # @api private
14
+ # @since 0.1.0
15
+ def initialize(collection)
16
+ @collection = collection
17
+ _compile!
18
+ end
19
+
20
+ # Translates the given entity into a format compatible with the database.
21
+ #
22
+ # @param entity [Object] the entity
23
+ #
24
+ # @return [Hash]
25
+ #
26
+ # @api private
27
+ # @since 0.1.0
28
+ def to_record(entity)
29
+ end
30
+
31
+ # Translates the given record into a Ruby object.
32
+ #
33
+ # @param record [Hash]
34
+ #
35
+ # @return [Object]
36
+ #
37
+ # @api private
38
+ # @since 0.1.0
39
+ def from_record(record)
40
+ end
41
+
42
+ private
43
+ # Compile itself for performance boost.
44
+ #
45
+ # @api private
46
+ # @since 0.1.0
47
+ def _compile!
48
+ code = @collection.attributes.map do |_,attr|
49
+ %{
50
+ def deserialize_#{ attr.mapped }(value)
51
+ #{ attr.load_coercer }(value)
52
+ end
53
+ }
54
+ end.join("\n")
55
+
56
+ instance_eval <<-EVAL, __FILE__, __LINE__
57
+ def to_record(entity)
58
+ if entity.id
59
+ Hash[#{ @collection.attributes.map{|name,attr| ":#{ attr.mapped },#{ attr.dump_coercer }(entity.#{name})"}.join(',') }]
60
+ else
61
+ Hash[].tap do |record|
62
+ #{ @collection.attributes.reject{|name,_| name == @collection.identity }.map{|name,attr| "value = #{ attr.dump_coercer }(entity.#{name}); record[:#{attr.mapped}] = value unless value.nil?"}.join('; ') }
63
+ end
64
+ end
65
+ end
66
+
67
+ def from_record(record)
68
+ ::#{ @collection.entity }.new(
69
+ Hash[#{ @collection.attributes.map{|name,attr| ":#{name},#{attr.load_coercer}(record[:#{attr.mapped}])"}.join(',') }]
70
+ )
71
+ end
72
+
73
+ #{ code }
74
+ EVAL
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,324 @@
1
+ require 'sequel'
2
+ require 'sequel/extensions/migration'
3
+ require 'hanami/model/migrator/connection'
4
+ require 'hanami/model/migrator/adapter'
5
+
6
+ module Hanami
7
+ module Model
8
+ # Migration error
9
+ #
10
+ # @since 0.4.0
11
+ class MigrationError < Hanami::Model::Error
12
+ end
13
+
14
+ # Define a migration
15
+ #
16
+ # It must define an up/down strategy to write schema changes (up) and to
17
+ # rollback them (down).
18
+ #
19
+ # We can use <tt>up</tt> and <tt>down</tt> blocks for custom strategies, or
20
+ # only one <tt>change</tt> block that automatically implements "down" strategy.
21
+ #
22
+ # @param blk [Proc] a block that defines up/down or change database migration
23
+ #
24
+ # @since 0.4.0
25
+ #
26
+ # @example Use up/down blocks
27
+ # Hanami::Model.migration do
28
+ # up do
29
+ # create_table :books do
30
+ # primary_key :id
31
+ # column :book, String
32
+ # end
33
+ # end
34
+ #
35
+ # down do
36
+ # drop_table :books
37
+ # end
38
+ # end
39
+ #
40
+ # @example Use change block
41
+ # Hanami::Model.migration do
42
+ # change do
43
+ # create_table :books do
44
+ # primary_key :id
45
+ # column :book, String
46
+ # end
47
+ # end
48
+ #
49
+ # # DOWN strategy is automatically generated
50
+ # end
51
+ def self.migration(&blk)
52
+ Sequel.migration(&blk)
53
+ end
54
+
55
+ # Database schema migrator
56
+ #
57
+ # @since 0.4.0
58
+ module Migrator
59
+ # Create database defined by current configuration.
60
+ #
61
+ # It's only implemented for the following databases:
62
+ #
63
+ # * SQLite3
64
+ # * PostgreSQL
65
+ # * MySQL
66
+ #
67
+ # @raise [Hanami::Model::MigrationError] if an error occurs
68
+ #
69
+ # @since 0.4.0
70
+ #
71
+ # @see Hanami::Model::Configuration#adapter
72
+ #
73
+ # @example
74
+ # require 'hanami/model'
75
+ # require 'hanami/model/migrator'
76
+ #
77
+ # Hanami::Model.configure do
78
+ # # ...
79
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
80
+ # end
81
+ #
82
+ # Hanami::Model::Migrator.create # Creates `foo' database
83
+ def self.create
84
+ adapter(connection).create
85
+ end
86
+
87
+ # Drop database defined by current configuration.
88
+ #
89
+ # It's only implemented for the following databases:
90
+ #
91
+ # * SQLite3
92
+ # * PostgreSQL
93
+ # * MySQL
94
+ #
95
+ # @raise [Hanami::Model::MigrationError] if an error occurs
96
+ #
97
+ # @since 0.4.0
98
+ #
99
+ # @see Hanami::Model::Configuration#adapter
100
+ #
101
+ # @example
102
+ # require 'hanami/model'
103
+ # require 'hanami/model/migrator'
104
+ #
105
+ # Hanami::Model.configure do
106
+ # # ...
107
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
108
+ # end
109
+ #
110
+ # Hanami::Model::Migrator.drop # Drops `foo' database
111
+ def self.drop
112
+ adapter(connection).drop
113
+ end
114
+
115
+ # Migrate database schema
116
+ #
117
+ # It's possible to migrate "down" by specifying a version
118
+ # (eg. <tt>"20150610133853"</tt>)
119
+ #
120
+ # @param version [String,NilClass] target version
121
+ #
122
+ # @raise [Hanami::Model::MigrationError] if an error occurs
123
+ #
124
+ # @since 0.4.0
125
+ #
126
+ # @see Hanami::Model::Configuration#adapter
127
+ # @see Hanami::Model::Configuration#migrations
128
+ #
129
+ # @example Migrate Up
130
+ # require 'hanami/model'
131
+ # require 'hanami/model/migrator'
132
+ #
133
+ # Hanami::Model.configure do
134
+ # # ...
135
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
136
+ # migrations 'db/migrations'
137
+ # end
138
+ #
139
+ # # Reads all files from "db/migrations" and apply them
140
+ # Hanami::Model::Migrator.migrate
141
+ #
142
+ # @example Migrate Down
143
+ # require 'hanami/model'
144
+ # require 'hanami/model/migrator'
145
+ #
146
+ # Hanami::Model.configure do
147
+ # # ...
148
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
149
+ # migrations 'db/migrations'
150
+ # end
151
+ #
152
+ # # Reads all files from "db/migrations" and apply them
153
+ # Hanami::Model::Migrator.migrate
154
+ #
155
+ # # Migrate to a specifiy version
156
+ # Hanami::Model::Migrator.migrate(version: "20150610133853")
157
+ def self.migrate(version: nil)
158
+ version = Integer(version) unless version.nil?
159
+
160
+ Sequel::Migrator.run(connection, migrations, target: version, allow_missing_migration_files: true) if migrations?
161
+ rescue Sequel::Migrator::Error => e
162
+ raise MigrationError.new(e.message)
163
+ end
164
+
165
+ # Migrate, dump schema, delete migrations.
166
+ #
167
+ # This is an experimental feature.
168
+ # It may change or be removed in the future.
169
+ #
170
+ # Actively developed applications accumulate tons of migrations.
171
+ # In the long term they are hard to maintain and slow to execute.
172
+ #
173
+ # "Apply" feature solves this problem.
174
+ #
175
+ # It keeps an updated SQL file with the structure of the database.
176
+ # This file can be used to create fresh databases for developer machines
177
+ # or during testing. This is faster than to run dozen or hundred migrations.
178
+ #
179
+ # When we use "apply", it eliminates all the migrations that are no longer
180
+ # necessary.
181
+ #
182
+ # @raise [Hanami::Model::MigrationError] if an error occurs
183
+ #
184
+ # @since 0.4.0
185
+ #
186
+ # @see Hanami::Model::Configuration#adapter
187
+ # @see Hanami::Model::Configuration#migrations
188
+ #
189
+ # @example Apply Migrations
190
+ # require 'hanami/model'
191
+ # require 'hanami/model/migrator'
192
+ #
193
+ # Hanami::Model.configure do
194
+ # # ...
195
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
196
+ # migrations 'db/migrations'
197
+ # schema 'db/schema.sql'
198
+ # end
199
+ #
200
+ # # Reads all files from "db/migrations" and apply and delete them.
201
+ # # It generates an updated version of "db/schema.sql"
202
+ # Hanami::Model::Migrator.apply
203
+ def self.apply
204
+ migrate
205
+ adapter(connection).dump
206
+ delete_migrations
207
+ end
208
+
209
+ # Prepare database: drop, create, load schema (if any), migrate.
210
+ #
211
+ # This is designed for development machines and testing mode.
212
+ # It works faster if used with <tt>apply</tt>.
213
+ #
214
+ # @raise [Hanami::Model::MigrationError] if an error occurs
215
+ #
216
+ # @since 0.4.0
217
+ #
218
+ # @see Hanami::Model::Migrator.apply
219
+ #
220
+ # @example Prepare Database
221
+ # require 'hanami/model'
222
+ # require 'hanami/model/migrator'
223
+ #
224
+ # Hanami::Model.configure do
225
+ # # ...
226
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
227
+ # migrations 'db/migrations'
228
+ # end
229
+ #
230
+ # Hanami::Model::Migrator.prepare # => creates `foo' and run migrations
231
+ #
232
+ # @example Prepare Database (with schema dump)
233
+ # require 'hanami/model'
234
+ # require 'hanami/model/migrator'
235
+ #
236
+ # Hanami::Model.configure do
237
+ # # ...
238
+ # adapter type: :sql, uri: 'postgres://localhost/foo'
239
+ # migrations 'db/migrations'
240
+ # schema 'db/schema.sql'
241
+ # end
242
+ #
243
+ # Hanami::Model::Migrator.apply # => updates schema dump
244
+ # Hanami::Model::Migrator.prepare # => creates `foo', load schema and run pending migrations (if any)
245
+ def self.prepare
246
+ drop rescue nil
247
+ create
248
+ adapter(connection).load
249
+ migrate
250
+ end
251
+
252
+ # Return current database version timestamp
253
+ #
254
+ # If no migrations were ran, it returns <tt>nil</tt>.
255
+ #
256
+ # @return [String,NilClass] current version, if previously migrated
257
+ #
258
+ # @since 0.4.0
259
+ #
260
+ # @example
261
+ # # Given last migrations is:
262
+ # # 20150610133853_create_books.rb
263
+ #
264
+ # Hanami::Model::Migrator.version # => "20150610133853"
265
+ def self.version
266
+ adapter(connection).version
267
+ end
268
+
269
+ private
270
+
271
+ # Loads an adapter for the given connection
272
+ #
273
+ # @since 0.4.0
274
+ # @api private
275
+ def self.adapter(connection)
276
+ Adapter.for(connection)
277
+ end
278
+
279
+ # Delete all the migrations
280
+ #
281
+ # @since 0.4.0
282
+ # @api private
283
+ def self.delete_migrations
284
+ migrations.each_child(&:delete)
285
+ end
286
+
287
+ # Database connection
288
+ #
289
+ # @since 0.4.0
290
+ # @api private
291
+ def self.connection
292
+ Sequel.connect(
293
+ configuration.adapter.uri
294
+ )
295
+ rescue Sequel::AdapterNotFound
296
+ raise MigrationError.new("Current adapter (#{ configuration.adapter.type }) doesn't support SQL database operations.")
297
+ end
298
+
299
+ # Hanami::Model configuration
300
+ #
301
+ # @since 0.4.0
302
+ # @api private
303
+ def self.configuration
304
+ Model.configuration
305
+ end
306
+
307
+ # Migrations directory
308
+ #
309
+ # @since 0.4.0
310
+ # @api private
311
+ def self.migrations
312
+ configuration.migrations
313
+ end
314
+
315
+ # Check if there are migrations
316
+ #
317
+ # @since 0.4.0
318
+ # @api private
319
+ def self.migrations?
320
+ Dir["#{ migrations }/*.rb"].any?
321
+ end
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,170 @@
1
+ require 'uri'
2
+ require 'shellwords'
3
+
4
+ module Hanami
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 'hanami/model/migrator/sqlite_adapter'
32
+ SQLiteAdapter
33
+ when :postgres
34
+ require 'hanami/model/migrator/postgres_adapter'
35
+ PostgresAdapter
36
+ when :mysql
37
+ require 'hanami/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.new(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 Hanami::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 Hanami::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 Hanami::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.adapter_connection.tables.include?(MIGRATIONS_TABLE)
91
+
92
+ if record = connection.adapter_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.5.0
100
+ # @api private
101
+ attr_reader :connection
102
+
103
+ # Returns a database connection
104
+ #
105
+ # Given a DB connection URI we can connect to a specific database or not, we need this when creating
106
+ # or droping a database. Important to notice that we can't always open a _global_ DB connection,
107
+ # because most of the times application's DB user has no rights to do so.
108
+ #
109
+ # @param global [Boolean] determine whether or not a connection should specify an database.
110
+ #
111
+ # @since 0.5.0
112
+ # @api private
113
+ #
114
+ def new_connection(global: false)
115
+ uri = global ? connection.global_uri : connection.uri
116
+
117
+ Sequel.connect(uri)
118
+ end
119
+
120
+ # @since 0.4.0
121
+ # @api private
122
+ def database
123
+ escape connection.database
124
+ end
125
+
126
+ # @since 0.4.0
127
+ # @api private
128
+ def port
129
+ escape connection.port
130
+ end
131
+
132
+ # @since 0.4.0
133
+ # @api private
134
+ def host
135
+ escape connection.host
136
+ end
137
+
138
+ # @since 0.4.0
139
+ # @api private
140
+ def username
141
+ escape connection.user
142
+ end
143
+
144
+ # @since 0.4.0
145
+ # @api private
146
+ def password
147
+ escape connection.password
148
+ end
149
+
150
+ # @since 0.4.0
151
+ # @api private
152
+ def schema
153
+ Model.configuration.schema
154
+ end
155
+
156
+ # @since 0.4.0
157
+ # @api private
158
+ def migrations_table
159
+ escape MIGRATIONS_TABLE
160
+ end
161
+
162
+ # @since 0.4.0
163
+ # @api private
164
+ def escape(string)
165
+ Shellwords.escape(string) unless string.nil?
166
+ end
167
+ end
168
+ end
169
+ end
170
+ end