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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a1d8f26a5f4ae7a05dde0ab5385b893c2271d95127b74363e9fe9f55ed513b86
|
|
4
|
+
data.tar.gz: b1db489a98150c49b6446ddaa79d15cff8a4f4f8eb68759384013d73b9b02fbb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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]
|
|
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
|
-
|
|
21
|
-
|
|
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
|
|
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(
|
|
96
|
-
|
|
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
|
|
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
|
data/lib/schema_ferry/version.rb
CHANGED