schema_ferry 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: aacec6bc39b272cc96454401749eea5b880bb7c3a3d08e4666927f00f92d0379
4
- data.tar.gz: eb407968fdbb8b848a1c4f942fb824cef3a80a063a1effb81bf02389342a69ce
3
+ metadata.gz: a1d8f26a5f4ae7a05dde0ab5385b893c2271d95127b74363e9fe9f55ed513b86
4
+ data.tar.gz: b1db489a98150c49b6446ddaa79d15cff8a4f4f8eb68759384013d73b9b02fbb
5
5
  SHA512:
6
- metadata.gz: 2e4663eb185b31ab7724e574c01c9b23fb49b8844a3e20bcebcba2ebc467d1abe8b8e7e7272fa73b0cf122f4a74cc6433811b4598a328b8847edad41b73963ae
7
- data.tar.gz: db642c4079f769cc430a79bbe7174a4dd1b13b89000bfcdc0ae9b4ce3e47c29e57db2c756820c543ab06531b3ce6a430f90861d8ed70a46c91bc7bc2f1d82b82
6
+ metadata.gz: a4f9341de8e149c2e281c6a278a55d178bdf00f56a18d8362c23266d37e4d4ef54ae36daa97399e3de7db46b4e7ff3552bc98215f564b1cc0698c4db5daa5cb0
7
+ data.tar.gz: cdf15361234635907e9aacb90075ed6edb6ddc681360650ad3fc7a38d5a8c75057492ec84405bd884d639397addc6abc47369297181c5ae426293634ed54e043
data/CHANGELOG.md CHANGED
@@ -6,3 +6,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
8
  ## [Unreleased]
9
+
10
+ ## [0.1.1] - 2026-07-06
11
+
12
+ ### Fixed
13
+
14
+ - `apply!` no longer fails with `PG::DatatypeMismatch` when a `BIGINT UNSIGNED` column takes part in a foreign key — the standard Rails-on-MySQL primary key layout. Such columns are now mapped to signed `bigint` (matching the referenced primary key) instead of `numeric(20)`, with a warning.
15
+ - Foreign keys whose column follows the Rails naming convention (`<table>_id`) are no longer dropped and re-created on every run: the generated `add_foreign_key` now omits `column:` for conventional names, matching ridgepole's export format.
16
+
17
+ ## [0.1.0] - 2026-07-06
18
+
19
+ ### Added
20
+
21
+ - Initial release
22
+ - `SchemaFerry.define` DSL — `source` / `target`, `map_type`, `enum_as :check`, `ignore_table`, and per-table rules (`map_column`, `ignore_column`, `ignore_index`, `add_index`)
23
+ - `Pipeline#dry_run` / `#apply!` — reads the MySQL schema via ActiveRecord, converts it with the default type mappings plus your rules, and applies the diff to PostgreSQL via ridgepole
24
+ - `schema_ferry` CLI (`apply` / `dry-run`) driven by a `Ferryfile`, with a one-line summary and cron-friendly exit status
25
+ - Automatic adjustments with warnings: unsigned integer widening, FULLTEXT/SPATIAL index skipping, 63-byte identifier shortening, zero-date default removal
data/README.md CHANGED
@@ -128,7 +128,7 @@ Ignoring a column also drops indexes and foreign keys that reference it. Renamin
128
128
  | `TINYINT(1)` | `boolean` | see the caveat above if a column holds more than 0/1 |
129
129
  | `TINYINT`…`BIGINT` (signed) | `smallint` / `integer` / `bigint` | widths normalized to PostgreSQL's three integer sizes |
130
130
  | `TINYINT`…`INT` `UNSIGNED` | one size larger | e.g. `INT UNSIGNED` → `bigint` |
131
- | `BIGINT UNSIGNED` | `numeric(20)` | PostgreSQL has no unsigned 8-byte integer; emitted with a warning |
131
+ | `BIGINT UNSIGNED` | `numeric(20)` | PostgreSQL has no unsigned 8-byte integer; emitted with a warning. Columns on a foreign key become signed `bigint` instead — see below |
132
132
  | `FLOAT` / `DOUBLE` | `double precision` | |
133
133
  | `DECIMAL(p,s)` | `numeric(p,s)` | |
134
134
  | `DATETIME` / `TIMESTAMP` | `timestamp` | use `map_type :datetime, to: :timestamptz` for `timestamptz` |
@@ -147,6 +147,7 @@ Some MySQL constructs have no PostgreSQL equivalent. schema_ferry handles them a
147
147
  - **Index prefix lengths** (`KEY (col(10))`) are dropped silently — PostgreSQL indexes the full column.
148
148
  - **Identifiers over 63 bytes** (MySQL allows 64): index and foreign key names are shortened deterministically (`first 54 bytes + _ + 8-char digest`), so repeated runs stay stable. Overlong table names are only warned about — rename those yourself.
149
149
  - **Zero-date defaults** (`'0000-00-00 00:00:00'`) are invalid in PostgreSQL and are dropped.
150
+ - **BIGINT UNSIGNED columns on a foreign key** (either side) become signed `bigint` instead of `numeric(20)` — a numeric column cannot reference a bigint primary key. Values above 2⁶³−1 will not fit, the same trade-off as for `BIGINT UNSIGNED` primary keys.
150
151
 
151
152
  ## How it works
152
153
 
@@ -13,8 +13,8 @@ module SchemaFerry
13
13
  @type_mapper = type_mapper
14
14
  end
15
15
 
16
- def call(raw, table_name, rule)
17
- raw = bump_unsigned_integer(raw)
16
+ def call(raw, table_name, rule, fk_columns = [])
17
+ raw = bump_unsigned_integer(raw, table_name, fk_columns.include?(raw[:name]))
18
18
  raw = drop_zero_date_default(raw, table_name)
19
19
  override = rule&.column_type_overrides&.[](raw[:name])
20
20
  col_opts = raw.slice(:limit, :precision, :scale, :null, :default, :default_function, :comment)
@@ -30,15 +30,22 @@ module SchemaFerry
30
30
 
31
31
  private
32
32
 
33
- def bump_unsigned_integer(raw)
33
+ def bump_unsigned_integer(raw, table_name, fk_column)
34
34
  return raw unless raw[:type] == :integer && raw[:sql_type].to_s.include?("unsigned")
35
35
 
36
- if raw[:limit] == 8
36
+ if raw[:limit] != 8
37
+ raw.merge(limit: UNSIGNED_LIMIT_BUMP.fetch(raw[:limit], raw[:limit]))
38
+ elsif fk_column
39
+ # decimal(20, 0) cannot be a foreign key to a bigint primary key, so
40
+ # FK columns follow the primary-key conversion (signed bigint) instead.
41
+ emit_warning "#{table_name}.#{raw[:name]}: BIGINT UNSIGNED takes part in a foreign key; " \
42
+ "mapped to signed bigint to match the referenced key " \
43
+ "(values above 2^63-1 will not fit)."
44
+ raw
45
+ else
37
46
  emit_warning "column #{raw[:name].inspect}: BIGINT UNSIGNED has no PostgreSQL " \
38
47
  "integer equivalent; mapped to decimal(20, 0)."
39
48
  raw.merge(type: :decimal, limit: nil, precision: 20, scale: 0)
40
- else
41
- raw.merge(limit: UNSIGNED_LIMIT_BUMP.fetch(raw[:limit], raw[:limit]))
42
49
  end
43
50
  end
44
51
 
@@ -16,14 +16,14 @@ module SchemaFerry
16
16
  end
17
17
 
18
18
  def convert(raw_tables)
19
- raw_tables
20
- .reject { |t| @ignored_tables.include?(t[:name]) }
21
- .map { |t| convert_table(t) }
19
+ kept_tables = raw_tables.reject { |t| @ignored_tables.include?(t[:name]) }
20
+ fk_columns = collect_fk_columns(kept_tables)
21
+ kept_tables.map { |t| convert_table(t, fk_columns.fetch(t[:name], [])) }
22
22
  end
23
23
 
24
24
  private
25
25
 
26
- def convert_table(raw)
26
+ def convert_table(raw, fk_columns)
27
27
  rule = @table_rules[raw[:name]]
28
28
  ignored = rule&.ignored_columns || []
29
29
  IdentifierShortener.warn_long_table_name(raw[:name])
@@ -34,13 +34,32 @@ module SchemaFerry
34
34
  pk_type: convert_pk_type(raw),
35
35
  pk_limit: raw[:pk_limit],
36
36
  comment: raw[:comment],
37
- columns: convert_columns(raw[:columns], raw[:name], rule, ignored),
37
+ columns: convert_columns(raw[:columns], raw[:name], rule, ignored, fk_columns),
38
38
  indexes: convert_indexes(raw[:indexes], raw[:name], rule, ignored),
39
- foreign_keys: convert_foreign_keys(raw[:foreign_keys], ignored),
39
+ foreign_keys: convert_foreign_keys(raw),
40
40
  check_constraints: build_check_constraints(raw, rule, ignored)
41
41
  )
42
42
  end
43
43
 
44
+ # Columns on either side of a surviving foreign key must land in
45
+ # PostgreSQL's integer type family: a numeric(20, 0) column cannot
46
+ # reference a bigint key.
47
+ def collect_fk_columns(raw_tables)
48
+ raw_tables.each_with_object({}) do |raw, acc|
49
+ surviving_foreign_keys(raw).each do |fk|
50
+ (acc[raw[:name]] ||= []) << fk[:column]
51
+ (acc[fk[:to_table]] ||= []) << fk[:primary_key]
52
+ end
53
+ end
54
+ end
55
+
56
+ def surviving_foreign_keys(raw)
57
+ ignored = @table_rules[raw[:name]]&.ignored_columns || []
58
+ raw[:foreign_keys]
59
+ .reject { |fk| @ignored_tables.include?(fk[:to_table]) }
60
+ .reject { |fk| ignored.include?(fk[:column]) }
61
+ end
62
+
44
63
  # MySQL BIGINT comes through AR as :integer with limit 8, so the sql_type
45
64
  # is the only reliable source for the id: option.
46
65
  def convert_pk_type(raw)
@@ -60,10 +79,10 @@ module SchemaFerry
60
79
  end
61
80
  end
62
81
 
63
- def convert_columns(raw_columns, table_name, rule, ignored)
82
+ def convert_columns(raw_columns, table_name, rule, ignored, fk_columns)
64
83
  raw_columns
65
84
  .reject { |c| ignored.include?(c[:name]) }
66
- .map { |c| @column_converter.call(c, table_name, rule) }
85
+ .map { |c| @column_converter.call(c, table_name, rule, fk_columns) }
67
86
  end
68
87
 
69
88
  def convert_indexes(raw_indexes, table_name, rule, ignored)
@@ -92,11 +111,8 @@ module SchemaFerry
92
111
  end
93
112
  end
94
113
 
95
- def convert_foreign_keys(raw_fkeys, ignored)
96
- raw_fkeys
97
- .reject { |fk| @ignored_tables.include?(fk[:to_table]) }
98
- .reject { |fk| ignored.include?(fk[:column]) }
99
- .map { |fk| build_fk_schema(fk) }
114
+ def convert_foreign_keys(raw)
115
+ surviving_foreign_keys(raw).map { |fk| build_fk_schema(fk) }
100
116
  end
101
117
 
102
118
  def skip_unsupported_index?(idx, table_name)
@@ -125,7 +125,7 @@ module SchemaFerry
125
125
 
126
126
  def render_foreign_key(foreign_key)
127
127
  parts = [foreign_key.from_table.inspect, foreign_key.to_table.inspect]
128
- parts << "column: #{foreign_key.column.inspect}" if foreign_key.column
128
+ parts << "column: #{foreign_key.column.inspect}" if custom_fk_column?(foreign_key)
129
129
  non_default_pk = foreign_key.primary_key && foreign_key.primary_key != "id"
130
130
  parts << "primary_key: #{foreign_key.primary_key.inspect}" if non_default_pk
131
131
  parts << "name: #{foreign_key.name.inspect}" if foreign_key.name
@@ -133,6 +133,13 @@ module SchemaFerry
133
133
  parts << "on_delete: #{foreign_key.on_delete.inspect}" if foreign_key.on_delete
134
134
  "add_foreign_key #{parts.join(", ")}"
135
135
  end
136
+
137
+ # ridgepole's PostgreSQL export omits column: when it follows the Rails
138
+ # convention, and it compares foreign keys by their literal options —
139
+ # emitting column: for conventional names re-creates the FK every run.
140
+ def custom_fk_column?(foreign_key)
141
+ foreign_key.column && foreign_key.column != "#{foreign_key.to_table.singularize}_id"
142
+ end
136
143
  end
137
144
  end
138
145
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SchemaFerry
4
- VERSION = "0.1.0"
4
+ VERSION = "0.1.1"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: schema_ferry
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - kyuuri1791