schema_ferry 0.2.0 → 0.3.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 (30) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -1
  3. data/README.md +34 -32
  4. data/lib/schema_ferry/cli.rb +19 -10
  5. data/lib/schema_ferry/config.rb +109 -0
  6. data/lib/schema_ferry/{converter → core}/column_converter.rb +4 -4
  7. data/lib/schema_ferry/{converter → core}/enum_check_builder.rb +8 -6
  8. data/lib/schema_ferry/{converter/identifier_shortener.rb → core/identifier_shortenable.rb} +4 -14
  9. data/lib/schema_ferry/core/schema_model.rb +60 -0
  10. data/lib/schema_ferry/core/schemafile_renderer.rb +109 -0
  11. data/lib/schema_ferry/core/table_converter.rb +159 -0
  12. data/lib/schema_ferry/core/translator.rb +53 -0
  13. data/lib/schema_ferry/{converter → core}/type_mapper.rb +2 -2
  14. data/lib/schema_ferry/errors.rb +2 -0
  15. data/lib/schema_ferry/io/mysql_reader.rb +80 -0
  16. data/lib/schema_ferry/{target/ridgepole_runner.rb → io/postgres_writer.rb} +11 -6
  17. data/lib/schema_ferry/pipeline.rb +24 -7
  18. data/lib/schema_ferry/support/drop_detectable.rb +17 -0
  19. data/lib/schema_ferry/support/warnings.rb +13 -0
  20. data/lib/schema_ferry/version.rb +1 -1
  21. data/lib/schema_ferry.rb +14 -14
  22. metadata +20 -14
  23. data/lib/schema_ferry/converter/schema_converter.rb +0 -172
  24. data/lib/schema_ferry/dsl/config.rb +0 -77
  25. data/lib/schema_ferry/dsl/table_rule.rb +0 -34
  26. data/lib/schema_ferry/schema_model.rb +0 -56
  27. data/lib/schema_ferry/source/connection_registry.rb +0 -29
  28. data/lib/schema_ferry/source/mysql_reader.rb +0 -88
  29. data/lib/schema_ferry/target/schemafile_renderer.rb +0 -143
  30. data/lib/schema_ferry/warnings.rb +0 -13
@@ -1,172 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module Converter
5
- class SchemaConverter
6
- include Warnings
7
-
8
- # PostgreSQL has no FULLTEXT/SPATIAL equivalent that ridgepole can express.
9
- UNSUPPORTED_INDEX_TYPES = %i[fulltext spatial].freeze
10
-
11
- # MySQL spatial column types have no PostgreSQL equivalent (that would
12
- # require PostGIS, which schema_ferry does not manage). Most of these
13
- # already raise TypeMapper's ConversionError, since ActiveRecord's
14
- # mysql2 adapter reports them as an unrecognized type (nil). POINT is
15
- # the sole exception: AR matches sql_type against an unanchored /int/i
16
- # regex (see register_class_with_limit in
17
- # ActiveRecord::ConnectionAdapters::AbstractAdapter#initialize_type_map),
18
- # which matches "point" as a substring — so it's misreported as plain
19
- # :integer instead. Without this check it would sail through as a
20
- # meaningless integer column instead of raising. It must be caught
21
- # here, by sql_type, and raised explicitly — the same failure mode as
22
- # any other unsupported type, not a warning that's easy to miss in a
23
- # cron log.
24
- MISDETECTED_SPATIAL_SQL_TYPE = /\Apoint\b/i
25
-
26
- def initialize(config)
27
- @column_converter = ColumnConverter.new(TypeMapper.new(config.global_type_overrides))
28
- @table_rules = config.table_rules
29
- @ignored_tables = config.ignored_tables
30
- @enum_check = (EnumCheckBuilder.new if config.enum_mode == :check)
31
- end
32
-
33
- def convert(raw_tables)
34
- kept_tables = raw_tables.reject { |t| @ignored_tables.include?(t[:name]) }
35
- fk_columns = collect_fk_columns(kept_tables)
36
- kept_tables.map { |t| convert_table(t, fk_columns.fetch(t[:name], [])) }
37
- end
38
-
39
- private
40
-
41
- def convert_table(raw, fk_columns)
42
- rule = @table_rules[raw[:name]]
43
- ignored = rule&.ignored_columns || []
44
- IdentifierShortener.warn_long_table_name(raw[:name])
45
-
46
- TableSchema.new(
47
- name: raw[:name],
48
- primary_key: raw[:primary_key],
49
- pk_type: convert_pk_type(raw),
50
- pk_limit: raw[:pk_limit],
51
- comment: raw[:comment],
52
- columns: convert_columns(raw[:columns], raw[:name], rule, ignored, fk_columns),
53
- indexes: convert_indexes(raw[:indexes], raw[:name], rule, ignored),
54
- foreign_keys: convert_foreign_keys(raw),
55
- check_constraints: build_check_constraints(raw, rule, ignored)
56
- )
57
- end
58
-
59
- # Columns on either side of a surviving foreign key must land in
60
- # PostgreSQL's integer type family: a numeric(20, 0) column cannot
61
- # reference a bigint key.
62
- def collect_fk_columns(raw_tables)
63
- raw_tables.each_with_object({}) do |raw, acc|
64
- surviving_foreign_keys(raw).each do |fk|
65
- (acc[raw[:name]] ||= []) << fk[:column]
66
- (acc[fk[:to_table]] ||= []) << fk[:primary_key]
67
- end
68
- end
69
- end
70
-
71
- def surviving_foreign_keys(raw)
72
- ignored = @table_rules[raw[:name]]&.ignored_columns || []
73
- raw[:foreign_keys]
74
- .reject { |fk| @ignored_tables.include?(fk[:to_table]) }
75
- .reject { |fk| ignored.include?(fk[:column]) }
76
- end
77
-
78
- # MySQL BIGINT comes through AR as :integer with limit 8, so the sql_type
79
- # is the only reliable source for the id: option.
80
- def convert_pk_type(raw)
81
- return raw[:pk_type] unless raw[:pk_type] == :integer
82
-
83
- sql_type = raw[:pk_sql_type].to_s
84
- if sql_type.include?("unsigned")
85
- if sql_type.start_with?("bigint")
86
- emit_warning "table #{raw[:name]}: BIGINT UNSIGNED primary key has no PostgreSQL " \
87
- "equivalent; using signed bigint (values above 2^63-1 will not fit)."
88
- end
89
- :bigint
90
- elsif sql_type.start_with?("bigint")
91
- :bigint
92
- else
93
- :integer
94
- end
95
- end
96
-
97
- def convert_columns(raw_columns, table_name, rule, ignored, fk_columns)
98
- raw_columns
99
- .reject { |c| ignored.include?(c[:name]) }
100
- .map do |c|
101
- check_misdetected_spatial_type!(c, table_name, rule)
102
- @column_converter.call(c, table_name, rule, fk_columns)
103
- end
104
- end
105
-
106
- # map_column already lets a rule override any column's type before it
107
- # ever reaches this check (ColumnConverter#call skips TypeMapper
108
- # entirely when an override is present), same as for any other type.
109
- def check_misdetected_spatial_type!(raw, table_name, rule)
110
- return unless raw[:sql_type].to_s.match?(MISDETECTED_SPATIAL_SQL_TYPE)
111
- return if rule&.column_type_overrides&.key?(raw[:name])
112
-
113
- raise ConversionError, "#{table_name}.#{raw[:name]}: MySQL #{raw[:sql_type]} columns have no " \
114
- "PostgreSQL equivalent without PostGIS, which schema_ferry does not " \
115
- "manage. Exclude it with ignore_column :#{raw[:name]}."
116
- end
117
-
118
- def convert_indexes(raw_indexes, table_name, rule, ignored)
119
- raw_indexes
120
- .reject { |idx| rule&.ignored_indexes&.include?(idx[:name]) }
121
- .reject { |idx| idx[:columns].intersect?(ignored) }
122
- .map do |idx|
123
- check_unsupported_index_type!(idx, table_name)
124
- build_index_schema(idx, table_name)
125
- end
126
- end
127
-
128
- def convert_foreign_keys(raw)
129
- surviving_foreign_keys(raw).map { |fk| build_fk_schema(fk) }
130
- end
131
-
132
- # A warning here would be easy to miss in an unattended cron run.
133
- # Raising makes this failure show up the same way as any other error:
134
- # a non-zero exit status a monitor can catch.
135
- def check_unsupported_index_type!(idx, table_name)
136
- return unless UNSUPPORTED_INDEX_TYPES.include?(idx[:type])
137
-
138
- raise ConversionError, "#{table_name}: #{idx[:type].to_s.upcase} index #{idx[:name].inspect} " \
139
- "has no PostgreSQL equivalent. Exclude it with ignore_index :#{idx[:name]}."
140
- end
141
-
142
- def build_index_schema(raw, table_name)
143
- IndexSchema.new(
144
- name: IdentifierShortener.shorten(raw[:name], kind: "index", table: table_name),
145
- columns: raw[:columns],
146
- unique: raw[:unique],
147
- using: raw[:using],
148
- lengths: raw[:lengths],
149
- orders: raw[:orders]
150
- )
151
- end
152
-
153
- def build_fk_schema(raw)
154
- ForeignKeySchema.new(
155
- from_table: raw[:from_table],
156
- to_table: raw[:to_table],
157
- column: raw[:column],
158
- primary_key: raw[:primary_key],
159
- on_update: raw[:on_update],
160
- on_delete: raw[:on_delete],
161
- name: IdentifierShortener.shorten(raw[:name], kind: "foreign key", table: raw[:from_table])
162
- )
163
- end
164
-
165
- def build_check_constraints(raw, rule, ignored)
166
- return [] unless @enum_check
167
-
168
- @enum_check.call(raw, rule, ignored)
169
- end
170
- end
171
- end
172
- end
@@ -1,77 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module DSL
5
- class Config
6
- ENUM_MODES = %i[string check].freeze
7
-
8
- attr_reader :source_url, :target_url,
9
- :global_type_overrides,
10
- :table_rules,
11
- :ignored_tables,
12
- :enum_mode
13
-
14
- # Evaluates a definition block and returns a validated Config.
15
- def self.build(&block)
16
- new.tap do |config|
17
- config.instance_eval(&block)
18
- config.validate!
19
- end
20
- end
21
-
22
- # Evaluates a definition file (the DSL body, without the
23
- # SchemaFerry.define wrapper) and returns a validated Config.
24
- def self.load_file(path)
25
- new.tap do |config|
26
- config.instance_eval(File.read(path), path.to_s, 1)
27
- config.validate!
28
- end
29
- end
30
-
31
- def initialize
32
- @global_type_overrides = {}
33
- @table_rules = {}
34
- @ignored_tables = []
35
- @enum_mode = :string
36
- end
37
-
38
- def source(url)
39
- @source_url = url
40
- end
41
-
42
- def target(url)
43
- @target_url = url
44
- end
45
-
46
- def map_type(mysql_type, to:)
47
- @global_type_overrides[mysql_type.to_sym] = to.to_sym
48
- end
49
-
50
- def table(table_name, &)
51
- rule = TableRule.new(table_name)
52
- rule.instance_eval(&)
53
- @table_rules[table_name.to_s] = rule
54
- end
55
-
56
- def ignore_table(table_name)
57
- @ignored_tables << table_name.to_s
58
- end
59
-
60
- # How to convert MySQL enum columns:
61
- # :string — plain varchar, values not enforced (default)
62
- # :check — varchar plus a CHECK constraint restricting the values
63
- def enum_as(mode)
64
- unless ENUM_MODES.include?(mode.to_sym)
65
- raise ConfigError, "enum_as accepts #{ENUM_MODES.map(&:inspect).join(" or ")}, got #{mode.inspect}"
66
- end
67
-
68
- @enum_mode = mode.to_sym
69
- end
70
-
71
- def validate!
72
- raise ConfigError, 'source is not configured. Add: source "mysql2://..."' unless @source_url
73
- raise ConfigError, 'target is not configured. Add: target "postgresql://..."' unless @target_url
74
- end
75
- end
76
- end
77
- end
@@ -1,34 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module DSL
5
- class TableRule
6
- UNSET = Object.new.freeze
7
- private_constant :UNSET
8
-
9
- attr_reader :table_name, :column_type_overrides,
10
- :column_default_overrides, :ignored_columns, :ignored_indexes
11
-
12
- def initialize(table_name)
13
- @table_name = table_name.to_s
14
- @column_type_overrides = {}
15
- @column_default_overrides = {}
16
- @ignored_columns = []
17
- @ignored_indexes = []
18
- end
19
-
20
- def map_column(column_name, type:, default: UNSET)
21
- @column_type_overrides[column_name.to_s] = type.to_sym
22
- @column_default_overrides[column_name.to_s] = default unless default.equal?(UNSET)
23
- end
24
-
25
- def ignore_column(column_name)
26
- @ignored_columns << column_name.to_s
27
- end
28
-
29
- def ignore_index(index_name)
30
- @ignored_indexes << index_name.to_s
31
- end
32
- end
33
- end
34
- end
@@ -1,56 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- TableSchema = Struct.new(
5
- :name, # String
6
- :primary_key, # String | Array<String> | nil
7
- :pk_type, # Symbol: :bigint, :integer, :string, etc. (single-column PK only)
8
- :pk_limit, # Integer | nil (limit of a string PK column)
9
- :comment, # String | nil
10
- :columns, # Array<ColumnSchema>
11
- :indexes, # Array<IndexSchema>
12
- :foreign_keys, # Array<ForeignKeySchema>
13
- :check_constraints, # Array<CheckConstraintSchema>
14
- keyword_init: true
15
- )
16
-
17
- ColumnSchema = Struct.new(
18
- :name, # String
19
- :type, # Symbol (:string, :integer, :jsonb, …)
20
- :limit, # Integer | nil
21
- :precision, # Integer | nil
22
- :scale, # Integer | nil
23
- :null, # Boolean
24
- :default, # Object | nil
25
- :default_function, # String | nil (e.g. "CURRENT_TIMESTAMP")
26
- :comment, # String | nil
27
- keyword_init: true
28
- )
29
-
30
- IndexSchema = Struct.new(
31
- :name, # String
32
- :columns, # Array<String>
33
- :unique, # Boolean
34
- :using, # Symbol | nil
35
- :lengths, # Integer | Hash | nil
36
- :orders, # Symbol | Hash | nil
37
- keyword_init: true
38
- )
39
-
40
- CheckConstraintSchema = Struct.new(
41
- :expression, # String (e.g. "kind IN ('a', 'b')")
42
- :name, # String
43
- keyword_init: true
44
- )
45
-
46
- ForeignKeySchema = Struct.new(
47
- :from_table, # String
48
- :to_table, # String
49
- :column, # String
50
- :primary_key, # String
51
- :on_update, # Symbol | nil
52
- :on_delete, # Symbol | nil
53
- :name, # String | nil
54
- keyword_init: true
55
- )
56
- end
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module Source
5
- module ConnectionRegistry
6
- # Uses a fresh ConnectionHandler per call so the pool is fully isolated
7
- # from ActiveRecord::Base and any host Rails app connections.
8
- # AR 7.2+ banned anonymous AR subclasses, so we bypass that path entirely.
9
- def self.with_connection(url)
10
- handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
11
- begin
12
- pool = handler.establish_connection(url, owner_name: "schema_ferry")
13
- conn = pool.checkout
14
- conn.verify!
15
- rescue ActiveRecord::ActiveRecordError => e
16
- raise ConnectionError, e.message
17
- end
18
-
19
- begin
20
- yield conn
21
- ensure
22
- pool.checkin(conn)
23
- end
24
- ensure
25
- handler&.clear_all_connections!
26
- end
27
- end
28
- end
29
- end
@@ -1,88 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module Source
5
- class MysqlReader
6
- def initialize(url)
7
- @url = url
8
- end
9
-
10
- def read_all
11
- ConnectionRegistry.with_connection(@url) do |conn|
12
- conn.tables.sort.map { |table_name| read_table(conn, table_name) }
13
- end
14
- rescue ConnectionError
15
- raise
16
- rescue StandardError => e
17
- raise ReadError, e.message
18
- end
19
-
20
- private
21
-
22
- def read_table(conn, name)
23
- columns = conn.columns(name)
24
- pk = conn.primary_key(name)
25
- pk_col = pk.is_a?(String) ? columns.find { |c| c.name == pk } : nil
26
- {
27
- name: name,
28
- primary_key: pk,
29
- pk_type: pk_col&.type,
30
- pk_limit: pk_col&.type == :string ? pk_col.limit : nil,
31
- pk_sql_type: pk_col&.sql_type,
32
- comment: conn.table_comment(name).presence,
33
- columns: serialize_columns(columns, pk),
34
- indexes: serialize_indexes(conn.indexes(name)),
35
- foreign_keys: serialize_foreign_keys(conn.foreign_keys(name), name)
36
- }
37
- end
38
-
39
- # A single-column PK is rendered via create_table's id: option, so it is
40
- # excluded here. Composite PK columns must stay: primary_key: [...] does
41
- # not create them.
42
- def serialize_columns(columns, primary_key)
43
- columns.reject { |c| c.name == primary_key }.map do |c|
44
- {
45
- name: c.name,
46
- type: c.type,
47
- sql_type: c.sql_type,
48
- limit: c.limit,
49
- precision: c.precision,
50
- scale: c.scale,
51
- null: c.null,
52
- default: c.default,
53
- default_function: c.default_function,
54
- comment: c.comment
55
- }
56
- end
57
- end
58
-
59
- def serialize_indexes(indexes)
60
- indexes.map do |idx|
61
- {
62
- name: idx.name,
63
- columns: Array(idx.columns),
64
- unique: idx.unique,
65
- using: idx.using,
66
- type: idx.type, # :fulltext | :spatial | nil
67
- lengths: idx.lengths.presence,
68
- orders: idx.orders.presence
69
- }
70
- end
71
- end
72
-
73
- def serialize_foreign_keys(fkeys, from_table)
74
- fkeys.map do |fk|
75
- {
76
- from_table: from_table,
77
- to_table: fk.to_table,
78
- column: fk.column,
79
- primary_key: fk.primary_key,
80
- on_update: fk.on_update,
81
- on_delete: fk.on_delete,
82
- name: fk.name
83
- }
84
- end
85
- end
86
- end
87
- end
88
- end
@@ -1,143 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- module Target
5
- class SchemafileRenderer
6
- TIMESTAMP_COLS = %w[created_at updated_at].freeze
7
-
8
- def render(tables)
9
- table_blocks = tables.map { |t| render_table(t) }
10
- fkey_lines = tables.flat_map { |t| t.foreign_keys.map { |fk| render_foreign_key(fk) } }
11
-
12
- parts = table_blocks
13
- parts += fkey_lines unless fkey_lines.empty?
14
- parts.join("\n\n")
15
- end
16
-
17
- private
18
-
19
- def render_table(table)
20
- lines = []
21
- lines << "create_table #{table.name.inspect}#{pk_options(table)} do |t|"
22
- lines.concat(render_columns(table.columns))
23
- table.indexes.each { |idx| lines << render_index(idx) }
24
- Array(table.check_constraints).each { |chk| lines << render_check_constraint(chk) }
25
- lines << "end"
26
- lines.join("\n")
27
- end
28
-
29
- def pk_options(table)
30
- opts = []
31
- case table.primary_key
32
- when nil
33
- opts << "id: false"
34
- when String
35
- opts << "primary_key: #{table.primary_key.inspect}" if table.primary_key != "id"
36
- if table.pk_type && table.pk_type != :bigint
37
- opts << "id: #{table.pk_type.inspect}"
38
- opts << "limit: #{table.pk_limit.inspect}" if table.pk_limit
39
- end
40
- when Array
41
- opts << "primary_key: #{table.primary_key.inspect}"
42
- end
43
- opts.unshift("force: :cascade")
44
- opts << "comment: #{table.comment.inspect}" if table.comment
45
- ", #{opts.join(", ")}"
46
- end
47
-
48
- def render_columns(columns)
49
- ts_created = columns.find { |c| c.name == "created_at" }
50
- ts_updated = columns.find { |c| c.name == "updated_at" }
51
- use_timestamps = collapsible_timestamps?(ts_created, ts_updated)
52
-
53
- lines = []
54
- emitted_ts = false
55
-
56
- columns.each do |col|
57
- if TIMESTAMP_COLS.include?(col.name) && use_timestamps
58
- unless emitted_ts
59
- lines << render_timestamps(ts_created)
60
- emitted_ts = true
61
- end
62
- next
63
- end
64
- lines << render_column(col)
65
- end
66
-
67
- lines
68
- end
69
-
70
- def collapsible_timestamps?(created_at, updated_at)
71
- return false unless created_at && updated_at
72
- return false unless created_at.type == :datetime && updated_at.type == :datetime
73
-
74
- created_at.null == updated_at.null &&
75
- created_at.precision == updated_at.precision &&
76
- created_at.default.nil? && updated_at.default.nil? &&
77
- created_at.default_function.nil? && updated_at.default_function.nil?
78
- end
79
-
80
- def render_timestamps(col)
81
- opts = []
82
- opts << "null: false" if col.null == false
83
- opts << "precision: #{col.precision}" if col.precision
84
- opts.empty? ? " t.timestamps" : " t.timestamps #{opts.join(", ")}"
85
- end
86
-
87
- def render_column(col)
88
- parts = [col.name.inspect]
89
- opts = column_options(col)
90
- parts << opts unless opts.empty?
91
- " t.#{col.type} #{parts.join(", ")}"
92
- end
93
-
94
- def column_options(col)
95
- pairs = []
96
- pairs << "limit: #{col.limit.inspect}" if col.limit
97
- pairs << "precision: #{col.precision.inspect}" if col.precision
98
- pairs << "scale: #{col.scale.inspect}" if col.scale
99
- pairs << render_default(col) if render_default(col)
100
- pairs << "null: false" if col.null == false
101
- pairs << "comment: #{col.comment.inspect}" if col.comment
102
- pairs.join(", ")
103
- end
104
-
105
- def render_default(col)
106
- return "default: #{col.default.inspect}" unless col.default.nil?
107
-
108
- "default: -> { #{col.default_function.inspect} }" if col.default_function
109
- end
110
-
111
- def render_index(idx)
112
- cols = idx.columns.inspect
113
- parts = ["name: #{idx.name.inspect}"]
114
- parts << "unique: true" if idx.unique
115
- parts << "using: #{idx.using.inspect}" if idx.using
116
- parts << "order: #{idx.orders.inspect}" if idx.orders
117
- " t.index #{cols}, #{parts.join(", ")}"
118
- end
119
-
120
- def render_check_constraint(chk)
121
- " t.check_constraint #{chk.expression.inspect}, name: #{chk.name.inspect}"
122
- end
123
-
124
- def render_foreign_key(foreign_key)
125
- parts = [foreign_key.from_table.inspect, foreign_key.to_table.inspect]
126
- parts << "column: #{foreign_key.column.inspect}" if custom_fk_column?(foreign_key)
127
- non_default_pk = foreign_key.primary_key && foreign_key.primary_key != "id"
128
- parts << "primary_key: #{foreign_key.primary_key.inspect}" if non_default_pk
129
- parts << "name: #{foreign_key.name.inspect}" if foreign_key.name
130
- parts << "on_update: #{foreign_key.on_update.inspect}" if foreign_key.on_update
131
- parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
132
- "add_foreign_key #{parts.join(", ")}"
133
- end
134
-
135
- # ridgepole's PostgreSQL export omits column: when it follows the Rails
136
- # convention, and it compares foreign keys by their literal options —
137
- # emitting column: for conventional names re-creates the FK every run.
138
- def custom_fk_column?(foreign_key)
139
- foreign_key.column && foreign_key.column != "#{foreign_key.to_table.singularize}_id"
140
- end
141
- end
142
- end
143
- end
@@ -1,13 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SchemaFerry
4
- # Mix in (include for classes, extend for module_function modules) to emit
5
- # "[schema_ferry]"-prefixed warnings to stderr.
6
- module Warnings
7
- private
8
-
9
- def emit_warning(message)
10
- Kernel.warn("[schema_ferry] #{message}")
11
- end
12
- end
13
- end