hanami-model 0.0.0 → 0.6.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.
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