pakyow-data 1.0.0.rc1

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 (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +0 -0
  3. data/LICENSE +4 -0
  4. data/README.md +29 -0
  5. data/lib/pakyow/data/adapters/abstract.rb +58 -0
  6. data/lib/pakyow/data/adapters/sql/commands.rb +58 -0
  7. data/lib/pakyow/data/adapters/sql/dataset_methods.rb +29 -0
  8. data/lib/pakyow/data/adapters/sql/differ.rb +76 -0
  9. data/lib/pakyow/data/adapters/sql/migrator/adapter_methods.rb +95 -0
  10. data/lib/pakyow/data/adapters/sql/migrator.rb +181 -0
  11. data/lib/pakyow/data/adapters/sql/migrators/automator.rb +49 -0
  12. data/lib/pakyow/data/adapters/sql/migrators/finalizer.rb +96 -0
  13. data/lib/pakyow/data/adapters/sql/runner.rb +49 -0
  14. data/lib/pakyow/data/adapters/sql/source_extension.rb +31 -0
  15. data/lib/pakyow/data/adapters/sql/types.rb +50 -0
  16. data/lib/pakyow/data/adapters/sql.rb +247 -0
  17. data/lib/pakyow/data/behavior/config.rb +28 -0
  18. data/lib/pakyow/data/behavior/lookup.rb +75 -0
  19. data/lib/pakyow/data/behavior/serialization.rb +40 -0
  20. data/lib/pakyow/data/connection.rb +103 -0
  21. data/lib/pakyow/data/container.rb +273 -0
  22. data/lib/pakyow/data/errors.rb +169 -0
  23. data/lib/pakyow/data/framework.rb +42 -0
  24. data/lib/pakyow/data/helpers.rb +11 -0
  25. data/lib/pakyow/data/lookup.rb +85 -0
  26. data/lib/pakyow/data/migrator.rb +182 -0
  27. data/lib/pakyow/data/object.rb +98 -0
  28. data/lib/pakyow/data/proxy.rb +262 -0
  29. data/lib/pakyow/data/result.rb +53 -0
  30. data/lib/pakyow/data/sources/abstract.rb +82 -0
  31. data/lib/pakyow/data/sources/ephemeral.rb +72 -0
  32. data/lib/pakyow/data/sources/relational/association.rb +43 -0
  33. data/lib/pakyow/data/sources/relational/associations/belongs_to.rb +47 -0
  34. data/lib/pakyow/data/sources/relational/associations/has_many.rb +54 -0
  35. data/lib/pakyow/data/sources/relational/associations/has_one.rb +54 -0
  36. data/lib/pakyow/data/sources/relational/associations/through.rb +67 -0
  37. data/lib/pakyow/data/sources/relational/command.rb +531 -0
  38. data/lib/pakyow/data/sources/relational/migrator.rb +101 -0
  39. data/lib/pakyow/data/sources/relational.rb +587 -0
  40. data/lib/pakyow/data/subscribers/adapters/memory.rb +153 -0
  41. data/lib/pakyow/data/subscribers/adapters/redis/pipeliner.rb +45 -0
  42. data/lib/pakyow/data/subscribers/adapters/redis/scripts/_shared.lua +73 -0
  43. data/lib/pakyow/data/subscribers/adapters/redis/scripts/expire.lua +16 -0
  44. data/lib/pakyow/data/subscribers/adapters/redis/scripts/persist.lua +15 -0
  45. data/lib/pakyow/data/subscribers/adapters/redis/scripts/register.lua +37 -0
  46. data/lib/pakyow/data/subscribers/adapters/redis.rb +209 -0
  47. data/lib/pakyow/data/subscribers.rb +148 -0
  48. data/lib/pakyow/data/tasks/bootstrap.rake +18 -0
  49. data/lib/pakyow/data/tasks/create.rake +22 -0
  50. data/lib/pakyow/data/tasks/drop.rake +32 -0
  51. data/lib/pakyow/data/tasks/finalize.rake +56 -0
  52. data/lib/pakyow/data/tasks/migrate.rake +24 -0
  53. data/lib/pakyow/data/tasks/reset.rake +18 -0
  54. data/lib/pakyow/data/types.rb +37 -0
  55. data/lib/pakyow/data.rb +27 -0
  56. data/lib/pakyow/environment/data/auto_migrate.rb +31 -0
  57. data/lib/pakyow/environment/data/config.rb +54 -0
  58. data/lib/pakyow/environment/data/connections.rb +76 -0
  59. data/lib/pakyow/environment/data/memory_db.rb +23 -0
  60. data/lib/pakyow/validations/unique.rb +26 -0
  61. metadata +186 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 48569dc1d1d2f24fdf3a94b7b642ac33ecf1f80ed5fd2ae336300815eccc59ff
4
+ data.tar.gz: b4fae401a84c092cdd0fa5df7564016a2f9209b6c53c81d408b8f5a3ae96900e
5
+ SHA512:
6
+ metadata.gz: 683e102bdc37a1806c78f181cdca846015d82277e3580ade5347f91a5f2182ba1e12f6cd48d1e2bd0184ce3fa7a96b9862a067924e0515f61319a4e2228f1efa
7
+ data.tar.gz: feeafc96cc49182ad4ad8819c628429fae848818fd284ade6906cdd34c32f0f8e1883cdcb72c23f037ebd86a70081a4f4f506ce430a7b5bd086cdab74077e0e7
data/CHANGELOG.md ADDED
File without changes
data/LICENSE ADDED
@@ -0,0 +1,4 @@
1
+ Copyright (c) Metabahn, LLC
2
+
3
+ Pakyow Data is an open-source project licensed under the terms of the LGPLv3 license.
4
+ See <https://choosealicense.com/licenses/lgpl-3.0/> for license text.
data/README.md ADDED
@@ -0,0 +1,29 @@
1
+ # pakyow-data
2
+
3
+ Data layer for Pakyow.
4
+
5
+ # Download
6
+
7
+ The latest version of Pakyow Data can be installed with RubyGems:
8
+
9
+ ```
10
+ gem install pakyow-data
11
+ ```
12
+
13
+ Source code can be downloaded as part of the Pakyow project on Github:
14
+
15
+ - https://github.com/pakyow/pakyow/tree/master/pakyow-data
16
+
17
+ # License
18
+
19
+ Pakyow Data is free and open-source under the [LGPLv3 license](https://choosealicense.com/licenses/lgpl-3.0/).
20
+
21
+ # Support
22
+
23
+ Found a bug? Tell us about it here:
24
+
25
+ - https://github.com/pakyow/pakyow/issues
26
+
27
+ We'd love to have you in the community:
28
+
29
+ - http://pakyow.org/get-involved
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Abstract
7
+ def initialize(opts, logger: nil)
8
+ @opts, @logger = opts, logger
9
+ end
10
+
11
+ def dataset_for_source(_source)
12
+ raise "dataset_for_source is not implemented on #{self}"
13
+ end
14
+
15
+ def result_for_attribute_value(_attribute, _value, _source)
16
+ raise "result_for_attribute_value is not implemented on #{self}"
17
+ end
18
+
19
+ def transaction
20
+ raise "transactions are not supported by #{self}"
21
+ end
22
+
23
+ def connected?
24
+ false
25
+ end
26
+
27
+ def migratable?
28
+ false
29
+ end
30
+
31
+ class << self
32
+ def types_for_adapter(_adapter)
33
+ end
34
+ end
35
+
36
+ module SourceExtension
37
+ end
38
+
39
+ module Commands
40
+ end
41
+
42
+ module DatasetMethods
43
+ def to_a(_dataset)
44
+ raise "to_a is not implemented on #{self}"
45
+ end
46
+
47
+ def one(_dataset)
48
+ raise "one is not implemented on #{self}"
49
+ end
50
+
51
+ def count(_dataset)
52
+ raise "count is not implemented on #{self}"
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ module Commands
8
+ extend Support::Extension
9
+
10
+ apply_extension do
11
+ command :create, performs_create: true do |values|
12
+ begin
13
+ inserted_return_value = insert(values)
14
+ if self.class.primary_key_field
15
+ if Migrator::AUTO_INCREMENTING_TYPES.include?(self.class.primary_key_type)
16
+ where(self.class.primary_key_field => inserted_return_value)
17
+ else
18
+ where(self.class.primary_key_field => values[self.class.primary_key_field])
19
+ end
20
+ else
21
+ where(values)
22
+ end
23
+ rescue Sequel::UniqueConstraintViolation => error
24
+ raise UniqueViolation.build(error)
25
+ rescue Sequel::ForeignKeyConstraintViolation => error
26
+ raise ConstraintViolation.build(error)
27
+ end
28
+ end
29
+
30
+ command :update, performs_update: true do |values|
31
+ __getobj__.select(self.class.primary_key_field).map { |result|
32
+ result[self.class.primary_key_field]
33
+ }.tap do
34
+ begin
35
+ unless values.empty?
36
+ update(values)
37
+ end
38
+ rescue Sequel::UniqueConstraintViolation => error
39
+ raise UniqueViolation.build(error)
40
+ rescue Sequel::ForeignKeyConstraintViolation => error
41
+ raise ConstraintViolation.build(error)
42
+ end
43
+ end
44
+ end
45
+
46
+ command :delete, provides_dataset: false, performs_delete: true do
47
+ begin
48
+ delete
49
+ rescue Sequel::ForeignKeyConstraintViolation => error
50
+ raise ConstraintViolation.build(error)
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ module DatasetMethods
8
+ def to_a(dataset)
9
+ dataset.qualify.all
10
+ rescue Sequel::Error => error
11
+ raise QueryError.build(error)
12
+ end
13
+
14
+ def one(dataset)
15
+ dataset.qualify.first
16
+ rescue Sequel::Error => error
17
+ raise QueryError.build(error)
18
+ end
19
+
20
+ def count(dataset)
21
+ dataset.qualify.count
22
+ rescue Sequel::Error => error
23
+ raise QueryError.build(error)
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ class Differ
8
+ def initialize(connection:, source:, attributes: source.attributes)
9
+ @connection, @source, @attributes = connection, source, attributes
10
+ end
11
+
12
+ def exists?
13
+ raw_connection.table_exists?(table_name)
14
+ end
15
+
16
+ def changes?
17
+ attributes_to_add.any? || columns_to_remove.any? || column_types_to_change.any?
18
+ end
19
+
20
+ def table_name
21
+ @source.dataset_table
22
+ end
23
+
24
+ def attributes
25
+ Hash[@attributes.map { |attribute_name, attribute|
26
+ [attribute_name, @connection.adapter.finalized_attribute(attribute)]
27
+ }]
28
+ end
29
+
30
+ def attributes_to_add
31
+ {}.tap { |attributes|
32
+ self.attributes.each do |attribute_name, attribute_type|
33
+ unless schema.find { |column| column[0] == attribute_name }
34
+ attributes[attribute_name] = attribute_type
35
+ end
36
+ end
37
+ }
38
+ end
39
+
40
+ def columns_to_remove
41
+ {}.tap { |columns|
42
+ schema.each do |column_name, column_info|
43
+ unless @source.attributes.keys.find { |attribute_name| attribute_name == column_name }
44
+ columns[column_name] = column_info
45
+ end
46
+ end
47
+ }
48
+ end
49
+
50
+ def column_types_to_change
51
+ {}.tap { |attributes|
52
+ self.attributes.each do |attribute_name, attribute_type|
53
+ if found_column = schema.find { |column| column[0] == attribute_name }
54
+ column_name, column_info = found_column
55
+ unless column_info[:type] == attribute_type.meta[:column_type] && (!attribute_type.meta.include?(:native_type) || column_info[:db_type] == attribute_type.meta[:native_type])
56
+ attributes[column_name] = attribute_type.meta[:migration_type]
57
+ end
58
+ end
59
+ end
60
+ }
61
+ end
62
+
63
+ private
64
+
65
+ def raw_connection
66
+ @connection.adapter.connection
67
+ end
68
+
69
+ def schema
70
+ raw_connection.schema(table_name)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pakyow
4
+ module Data
5
+ module Adapters
6
+ class Sql
7
+ class Migrator
8
+ module AdapterMethods
9
+ module Mysql
10
+ def create!
11
+ handle_error do
12
+ @connection.adapter.connection.run("CREATE DATABASE `#{database}`")
13
+ end
14
+ end
15
+
16
+ def drop!
17
+ handle_error do
18
+ @connection.adapter.connection.run("DROP DATABASE `#{database}`")
19
+ end
20
+ end
21
+
22
+ def self.globalize_connection_opts!(connection_opts)
23
+ connection_opts[:initial] = Sql.build_opts(path: connection_opts[:path])
24
+ connection_opts[:path] = nil
25
+ end
26
+
27
+ private def database
28
+ if @connection.opts.key?(:initial)
29
+ @connection.opts[:initial][:path]
30
+ else
31
+ @connection.opts[:path]
32
+ end
33
+ end
34
+ end
35
+
36
+ module Postgres
37
+ def create!
38
+ handle_error do
39
+ @connection.adapter.connection.run("CREATE DATABASE \"#{database}\"")
40
+ end
41
+ end
42
+
43
+ def drop!
44
+ handle_error do
45
+ @connection.adapter.connection.run <<~SQL
46
+ SELECT
47
+ pg_terminate_backend(pid)
48
+ FROM
49
+ pg_stat_activity
50
+ WHERE
51
+ -- don't kill my own connection!
52
+ pid <> pg_backend_pid()
53
+ -- don't kill the connections to other databases
54
+ AND datname = '#{@connection.opts[:path]}';
55
+ SQL
56
+
57
+ @connection.adapter.connection.run("DROP DATABASE \"#{database}\"")
58
+ end
59
+ end
60
+
61
+ def self.globalize_connection_opts!(connection_opts)
62
+ connection_opts[:initial] = Sql.build_opts(path: connection_opts[:path])
63
+ connection_opts[:path] = "template1"
64
+ end
65
+
66
+ private def database
67
+ if @connection.opts.key?(:initial)
68
+ @connection.opts[:initial][:path]
69
+ else
70
+ @connection.opts[:path]
71
+ end
72
+ end
73
+ end
74
+
75
+ module Sqlite
76
+ def create!
77
+ # intentionally empty; automatically created on connect
78
+ end
79
+
80
+ def drop!
81
+ if File.exist?(@connection.opts[:path])
82
+ FileUtils.rm(@connection.opts[:path])
83
+ end
84
+ end
85
+
86
+ def self.globalize_connection_opts!(connection_opts)
87
+ # nothing to do here
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/differ"
4
+ require "pakyow/data/sources/relational/migrator"
5
+
6
+ module Pakyow
7
+ module Data
8
+ module Adapters
9
+ class Sql
10
+ class Migrator < Sources::Relational::Migrator
11
+ require "pakyow/data/adapters/sql/migrator/adapter_methods"
12
+
13
+ def initialize(*)
14
+ super
15
+
16
+ extend self.class.adapter_methods_for_adapter(
17
+ @connection.opts[:adapter]
18
+ )
19
+ end
20
+
21
+ def disconnect!
22
+ @connection.disconnect
23
+ end
24
+
25
+ def create_source?(source)
26
+ !differ(source).exists?
27
+ end
28
+
29
+ def change_source?(source, attributes = source.attributes)
30
+ create_source?(source) || differ(source, attributes).changes?
31
+ end
32
+
33
+ def create_source!(source, attributes)
34
+ local_context = self
35
+ differ = differ(source, attributes)
36
+ create_table differ.table_name do
37
+ differ.attributes.each do |attribute_name, attribute|
38
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source)
39
+ end
40
+ end
41
+ end
42
+
43
+ def reassociate_source!(source, foreign_keys)
44
+ foreign_keys.each do |foreign_key_name, foreign_key|
45
+ differ = differ(source, foreign_key_name => foreign_key)
46
+
47
+ if create_source?(source) || differ.changes?
48
+ local_context = self
49
+
50
+ associate_table differ.table_name, with: foreign_key.meta[:foreign_key] do
51
+ attributes_to_add = if local_context.send(:create_source?, source)
52
+ differ.attributes
53
+ else
54
+ differ.attributes_to_add
55
+ end
56
+
57
+ attributes_to_add.each do |attribute_name, attribute|
58
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source, method_prefix: "add_")
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
64
+
65
+ def change_source!(source, attributes)
66
+ differ = differ(source, attributes)
67
+
68
+ if differ.changes?
69
+ local_context = self
70
+
71
+ alter_table differ.table_name do
72
+ differ.attributes_to_add.each do |attribute_name, attribute|
73
+ local_context.send(:add_column_for_attribute, attribute_name, attribute, self, source, method_prefix: "add_")
74
+ end
75
+
76
+ differ.column_types_to_change.each do |column_name, _column_type|
77
+ local_context.send(:change_column_type_for_attribute, column_name, differ.attributes[column_name], self)
78
+ end
79
+
80
+ # TODO: revisit when we're ready to tackle foreign key removal
81
+ #
82
+ # differ.columns_to_remove.keys.each do |column_name|
83
+ # local_context.send(:remove_column_by_name, column_name, self)
84
+ # end
85
+ end
86
+ end
87
+ end
88
+
89
+ private
90
+
91
+ def automator
92
+ require "pakyow/data/adapters/sql/migrators/automator"
93
+ Migrators::Automator.new(@connection, sources: @sources)
94
+ end
95
+
96
+ def finalizer
97
+ require "pakyow/data/adapters/sql/migrators/finalizer"
98
+ Migrators::Finalizer.new(@connection, sources: @sources)
99
+ end
100
+
101
+ def differ(source, attributes = source.attributes)
102
+ Differ.new(connection: @connection, source: source, attributes: attributes)
103
+ end
104
+
105
+ AUTO_INCREMENTING_TYPES = %i(integer bignum).freeze
106
+ def add_column_for_attribute(attribute_name, attribute, context, source, method_prefix: "")
107
+ if attribute.meta[:primary_key]
108
+ if AUTO_INCREMENTING_TYPES.include?(attribute.meta[:migration_type])
109
+ context.send(:"#{method_prefix}primary_key", attribute_name, type: type_for_attribute(attribute))
110
+ else
111
+ context.send(:"#{method_prefix}column", attribute_name, type_for_attribute(attribute), primary_key: true, **column_opts_for_attribute(attribute))
112
+ end
113
+ elsif attribute.meta[:foreign_key] && source.container.sources.any? { |potential_foreign_source| potential_foreign_source.plural_name == attribute.meta[:foreign_key] }
114
+ context.send(:"#{method_prefix}foreign_key", attribute_name, attribute.meta[:foreign_key], type: type_for_attribute(attribute))
115
+ else
116
+ context.send(:"#{method_prefix}column", attribute_name, type_for_attribute(attribute), **column_opts_for_attribute(attribute))
117
+ end
118
+ end
119
+
120
+ def change_column_type_for_attribute(attribute_name, attribute, context)
121
+ context.set_column_type(attribute_name, type_for_attribute(attribute), **column_opts_for_attribute(attribute))
122
+ end
123
+
124
+ def remove_column_by_name(column_name, context)
125
+ context.drop_column(column_name)
126
+ end
127
+
128
+ ALLOWED_COLUMN_OPTS = %i(size text)
129
+ def column_opts_for_attribute(attribute)
130
+ {}.tap do |opts|
131
+ ALLOWED_COLUMN_OPTS.each do |opt|
132
+ if attribute.meta.include?(opt)
133
+ opts[opt] = attribute.meta[opt]
134
+ end
135
+ end
136
+ end
137
+ end
138
+
139
+ def column_opts_string_for_attribute(attribute)
140
+ opts = column_opts_for_attribute(attribute)
141
+
142
+ if opts.any?
143
+ opts.each_with_object(String.new) { |(key, value), opts_string|
144
+ opts_string << ", #{key}: #{value.inspect}"
145
+ }
146
+ else
147
+ ""
148
+ end
149
+ end
150
+
151
+ def handle_error
152
+ yield
153
+ rescue Sequel::Error => error
154
+ Pakyow.logger.warn "#{error}"
155
+ end
156
+
157
+ class << self
158
+ def adapter_methods_for_adapter(adapter)
159
+ case adapter
160
+ when "mysql", "mysql2"
161
+ AdapterMethods::Mysql
162
+ when "postgres"
163
+ AdapterMethods::Postgres
164
+ when "sqlite"
165
+ AdapterMethods::Sqlite
166
+ end
167
+ end
168
+
169
+ def globalize_connection_opts!(connection_opts)
170
+ adapter_methods_for_adapter(
171
+ connection_opts[:adapter]
172
+ ).globalize_connection_opts!(
173
+ connection_opts
174
+ )
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/migrator"
4
+
5
+ module Pakyow
6
+ module Data
7
+ module Adapters
8
+ class Sql
9
+ module Migrators
10
+ class Automator < Migrator
11
+ def associate_table(name, **, &block)
12
+ alter_table(name, &block)
13
+ end
14
+
15
+ def alter_table(name, &block)
16
+ @connection.adapter.connection.alter_table name do
17
+ AlterTable.new(self).instance_exec(&block)
18
+ end
19
+ end
20
+
21
+ def method_missing(name, *args, &block)
22
+ @connection.adapter.connection.public_send(name, *args, &block)
23
+ end
24
+
25
+ private
26
+
27
+ def type_for_attribute(attribute)
28
+ attribute.meta[:database_type]
29
+ end
30
+
31
+ class AlterTable
32
+ def initialize(table)
33
+ @table = table
34
+ end
35
+
36
+ def drop_column(*)
37
+ # Prevent columns from being dropped during auto migrate.
38
+ end
39
+
40
+ def method_missing(name, *args, &block)
41
+ @table.public_send(name, *args, &block)
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pakyow/data/adapters/sql/migrator"
4
+
5
+ module Pakyow
6
+ module Data
7
+ module Adapters
8
+ class Sql
9
+ module Migrators
10
+ class Finalizer < Migrator
11
+ attr_reader :migrations
12
+
13
+ def initialize(*)
14
+ super
15
+ @migrations = []
16
+ end
17
+
18
+ def create_table(name, &block)
19
+ writer = Writer.new(root: true)
20
+ writer.create_table(name, &block)
21
+ @migrations << ["create_#{name}", writer]
22
+ end
23
+
24
+ def associate_table(name, with:, &block)
25
+ writer = Writer.new(root: true)
26
+ writer.alter_table(name, &block)
27
+ @migrations << ["associate_#{name}_with_#{with}", writer]
28
+ end
29
+
30
+ def alter_table(name, &block)
31
+ writer = Writer.new(root: true)
32
+ writer.alter_table(name, &block)
33
+ @migrations << ["change_#{name}", writer]
34
+ end
35
+
36
+ private
37
+
38
+ def type_for_attribute(attribute)
39
+ attribute.meta[:migration_type]
40
+ end
41
+
42
+ class Writer
43
+ def initialize(root: false)
44
+ @root, @content = root, String.new
45
+ end
46
+
47
+ def to_s
48
+ if @root
49
+ <<~CONTENT
50
+ change do
51
+ #{indent(@content.strip)}
52
+ end
53
+ CONTENT
54
+ else
55
+ @content.strip
56
+ end
57
+ end
58
+
59
+ def to_ary
60
+ [to_s]
61
+ end
62
+
63
+ def method_missing(name, *args, **kwargs, &block)
64
+ method_call = "#{name} #{args_to_string(args, kwargs)}"
65
+
66
+ if block_given?
67
+ @content << <<~CONTENT
68
+ #{method_call} do
69
+ #{indent(Writer.new.tap { |writer| writer.instance_exec(&block) }.to_s)}
70
+ end
71
+ CONTENT
72
+ else
73
+ @content << <<~CONTENT
74
+ #{method_call}
75
+ CONTENT
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def args_to_string(args, kwargs)
82
+ kwargs.each_with_object(args.map(&:inspect).join(", ")) { |(key, value), string|
83
+ string << ", #{key}: #{value.inspect}"
84
+ }
85
+ end
86
+
87
+ def indent(content)
88
+ content.split("\n").map { |line| " #{line}" }.join("\n")
89
+ end
90
+ end
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end