declare_schema 0.6.2 → 0.8.0.pre.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 +4 -4
- data/CHANGELOG.md +35 -0
- data/Gemfile.lock +55 -53
- data/lib/declare_schema/field_declaration_dsl.rb +3 -4
- data/lib/declare_schema/model.rb +1 -2
- data/lib/declare_schema/model/column.rb +158 -0
- data/lib/declare_schema/model/field_spec.rb +59 -144
- data/lib/declare_schema/model/foreign_key_definition.rb +36 -25
- data/lib/declare_schema/version.rb +1 -1
- data/lib/generators/declare_schema/migration/migration_generator.rb +1 -1
- data/lib/generators/declare_schema/migration/migrator.rb +113 -135
- data/spec/lib/declare_schema/field_declaration_dsl_spec.rb +1 -1
- data/spec/lib/declare_schema/field_spec_spec.rb +142 -38
- data/spec/lib/declare_schema/migration_generator_spec.rb +73 -69
- data/spec/lib/declare_schema/model/column_spec.rb +122 -0
- data/spec/lib/declare_schema/model/foreign_key_definition_spec.rb +93 -0
- data/spec/lib/generators/declare_schema/migration/migrator_spec.rb +2 -11
- metadata +8 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 63c52c12c292be619b08901eb088c9957e8bfbc9708d087faa9d418704524175
|
4
|
+
data.tar.gz: 5a12e3d6cc67914e6b8c8739c17d89e2ea7886212f2abd90bd372f57fab808af
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e33ae65cdfcb5fc56c24b4d62804ba28c77b29efc0a7659b45660c953f6310ae90b8b42a8d51f9f5331592e511ec33d56ff0f7b691dcb7aa8f299a5f7d523f22
|
7
|
+
data.tar.gz: 688ca01165e655ab6ff309f82ba336530bb44d695ac90de049cabc730850c7059c8789ef95bafbc02059b6329b4212d0e2ab726034b9c71d814a51fdaca02501
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,36 @@ Inspired by [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
4
4
|
|
5
5
|
Note: this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
6
6
|
|
7
|
+
## [0.8.0] - UNRELEASED
|
8
|
+
### Removed
|
9
|
+
- Removed `sql_type` that was confusing because it was actually the same as `type` (ex: :string) and not
|
10
|
+
in fact the SQL type (ex: ``varchar(255)'`).
|
11
|
+
|
12
|
+
## [0.7.1] - 2021-02-17
|
13
|
+
### Fixed
|
14
|
+
- Exclude unknown options from FieldSpec#sql_options and #schema_attributes.
|
15
|
+
- Fixed a bug where fk_field_options were getting merged into spec_attrs after checking for equivalence,
|
16
|
+
leading to phantom migrations with no changes, or missing migrations when just the fk_field_options changed.
|
17
|
+
|
18
|
+
## [0.7.0] - 2021-02-14
|
19
|
+
### Changed
|
20
|
+
- Use `schema_attributes` for generating both up and down change migrations, so they are guaranteed to be symmetrical.
|
21
|
+
Note: Rails schema dumper is still used for the down migration to replace a model that has been dropped.
|
22
|
+
|
23
|
+
## [0.6.4] - 2021-02-08
|
24
|
+
- Fixed a bug where the generated call to add_foreign_key() was not setting `column:`,
|
25
|
+
so it only worked in cases where Rails could infer the foreign key by convention.
|
26
|
+
|
27
|
+
## [0.6.3] - 2021-01-21
|
28
|
+
### Added
|
29
|
+
- Added `add_foreign_key` native rails call in `DeclareSchema::Model::ForeignKeyDefinition#to_add_statement`.
|
30
|
+
|
31
|
+
### Fixed
|
32
|
+
- Fixed a bug in migration generation caused by `DeclareSchema::Migration#create_constraints`
|
33
|
+
calling `DeclareSchema::Model::ForeignKeyDefinition#to_add_statement` with unused parameters.
|
34
|
+
|
35
|
+
- Fixed a bug in `DeclareSchema::Migration#remove_foreign_key` where special characters would not be quoted properly.
|
36
|
+
|
7
37
|
## [0.6.2] - 2021-01-06
|
8
38
|
### Added
|
9
39
|
- Added `sqlite3` as dev dependency for local development
|
@@ -100,6 +130,11 @@ using the appropriate Rails configuration attributes.
|
|
100
130
|
### Added
|
101
131
|
- Initial version from https://github.com/Invoca/hobo_fields v4.1.0.
|
102
132
|
|
133
|
+
[0.8.0]: https://github.com/Invoca/declare_schema/compare/v0.7.1...v0.8.0
|
134
|
+
[0.7.1]: https://github.com/Invoca/declare_schema/compare/v0.7.0...v0.7.1
|
135
|
+
[0.7.0]: https://github.com/Invoca/declare_schema/compare/v0.6.3...v0.7.0
|
136
|
+
[0.6.4]: https://github.com/Invoca/declare_schema/compare/v0.6.3...v0.6.4
|
137
|
+
[0.6.3]: https://github.com/Invoca/declare_schema/compare/v0.6.2...v0.6.3
|
103
138
|
[0.6.2]: https://github.com/Invoca/declare_schema/compare/v0.6.1...v0.6.2
|
104
139
|
[0.6.1]: https://github.com/Invoca/declare_schema/compare/v0.6.0...v0.6.1
|
105
140
|
[0.6.0]: https://github.com/Invoca/declare_schema/compare/v0.5.0...v0.6.0
|
data/Gemfile.lock
CHANGED
@@ -1,49 +1,49 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
declare_schema (0.
|
4
|
+
declare_schema (0.8.0.pre.1)
|
5
5
|
rails (>= 4.2)
|
6
6
|
|
7
7
|
GEM
|
8
8
|
remote: https://rubygems.org/
|
9
9
|
specs:
|
10
|
-
actioncable (5.2.4.
|
11
|
-
actionpack (= 5.2.4.
|
10
|
+
actioncable (5.2.4.5)
|
11
|
+
actionpack (= 5.2.4.5)
|
12
12
|
nio4r (~> 2.0)
|
13
13
|
websocket-driver (>= 0.6.1)
|
14
|
-
actionmailer (5.2.4.
|
15
|
-
actionpack (= 5.2.4.
|
16
|
-
actionview (= 5.2.4.
|
17
|
-
activejob (= 5.2.4.
|
14
|
+
actionmailer (5.2.4.5)
|
15
|
+
actionpack (= 5.2.4.5)
|
16
|
+
actionview (= 5.2.4.5)
|
17
|
+
activejob (= 5.2.4.5)
|
18
18
|
mail (~> 2.5, >= 2.5.4)
|
19
19
|
rails-dom-testing (~> 2.0)
|
20
|
-
actionpack (5.2.4.
|
21
|
-
actionview (= 5.2.4.
|
22
|
-
activesupport (= 5.2.4.
|
20
|
+
actionpack (5.2.4.5)
|
21
|
+
actionview (= 5.2.4.5)
|
22
|
+
activesupport (= 5.2.4.5)
|
23
23
|
rack (~> 2.0, >= 2.0.8)
|
24
24
|
rack-test (>= 0.6.3)
|
25
25
|
rails-dom-testing (~> 2.0)
|
26
26
|
rails-html-sanitizer (~> 1.0, >= 1.0.2)
|
27
|
-
actionview (5.2.4.
|
28
|
-
activesupport (= 5.2.4.
|
27
|
+
actionview (5.2.4.5)
|
28
|
+
activesupport (= 5.2.4.5)
|
29
29
|
builder (~> 3.1)
|
30
30
|
erubi (~> 1.4)
|
31
31
|
rails-dom-testing (~> 2.0)
|
32
32
|
rails-html-sanitizer (~> 1.0, >= 1.0.3)
|
33
|
-
activejob (5.2.4.
|
34
|
-
activesupport (= 5.2.4.
|
33
|
+
activejob (5.2.4.5)
|
34
|
+
activesupport (= 5.2.4.5)
|
35
35
|
globalid (>= 0.3.6)
|
36
|
-
activemodel (5.2.4.
|
37
|
-
activesupport (= 5.2.4.
|
38
|
-
activerecord (5.2.4.
|
39
|
-
activemodel (= 5.2.4.
|
40
|
-
activesupport (= 5.2.4.
|
36
|
+
activemodel (5.2.4.5)
|
37
|
+
activesupport (= 5.2.4.5)
|
38
|
+
activerecord (5.2.4.5)
|
39
|
+
activemodel (= 5.2.4.5)
|
40
|
+
activesupport (= 5.2.4.5)
|
41
41
|
arel (>= 9.0)
|
42
|
-
activestorage (5.2.4.
|
43
|
-
actionpack (= 5.2.4.
|
44
|
-
activerecord (= 5.2.4.
|
42
|
+
activestorage (5.2.4.5)
|
43
|
+
actionpack (= 5.2.4.5)
|
44
|
+
activerecord (= 5.2.4.5)
|
45
45
|
marcel (~> 0.3.1)
|
46
|
-
activesupport (5.2.4.
|
46
|
+
activesupport (5.2.4.5)
|
47
47
|
concurrent-ruby (~> 1.0, >= 1.0.2)
|
48
48
|
i18n (>= 0.7, < 2)
|
49
49
|
minitest (~> 5.1)
|
@@ -54,25 +54,25 @@ GEM
|
|
54
54
|
thor (>= 0.14.0)
|
55
55
|
arel (9.0.0)
|
56
56
|
ast (2.4.1)
|
57
|
-
bootsnap (1.
|
57
|
+
bootsnap (1.7.2)
|
58
58
|
msgpack (~> 1.0)
|
59
59
|
builder (3.2.4)
|
60
60
|
byebug (11.1.3)
|
61
61
|
climate_control (0.2.0)
|
62
62
|
coderay (1.1.3)
|
63
|
-
concurrent-ruby (1.1.
|
63
|
+
concurrent-ruby (1.1.8)
|
64
64
|
crass (1.0.6)
|
65
65
|
diff-lcs (1.4.4)
|
66
|
-
erubi (1.
|
66
|
+
erubi (1.10.0)
|
67
67
|
ffi (1.14.2)
|
68
68
|
globalid (0.4.2)
|
69
69
|
activesupport (>= 4.2.0)
|
70
|
-
i18n (1.8.
|
70
|
+
i18n (1.8.9)
|
71
71
|
concurrent-ruby (~> 1.0)
|
72
|
-
listen (3.4.
|
72
|
+
listen (3.4.1)
|
73
73
|
rb-fsevent (~> 0.10, >= 0.10.3)
|
74
74
|
rb-inotify (~> 0.9, >= 0.9.10)
|
75
|
-
loofah (2.
|
75
|
+
loofah (2.9.0)
|
76
76
|
crass (~> 1.0.2)
|
77
77
|
nokogiri (>= 1.5.9)
|
78
78
|
mail (2.7.1)
|
@@ -82,12 +82,13 @@ GEM
|
|
82
82
|
method_source (1.0.0)
|
83
83
|
mimemagic (0.3.5)
|
84
84
|
mini_mime (1.0.2)
|
85
|
-
mini_portile2 (2.
|
86
|
-
minitest (5.14.
|
87
|
-
msgpack (1.
|
88
|
-
nio4r (2.5.
|
89
|
-
nokogiri (1.
|
90
|
-
mini_portile2 (~> 2.
|
85
|
+
mini_portile2 (2.5.0)
|
86
|
+
minitest (5.14.3)
|
87
|
+
msgpack (1.4.2)
|
88
|
+
nio4r (2.5.5)
|
89
|
+
nokogiri (1.11.1)
|
90
|
+
mini_portile2 (~> 2.5.0)
|
91
|
+
racc (~> 1.4)
|
91
92
|
parallel (1.19.2)
|
92
93
|
parser (2.7.1.4)
|
93
94
|
ast (~> 2.4.1)
|
@@ -97,35 +98,36 @@ GEM
|
|
97
98
|
pry-byebug (3.9.0)
|
98
99
|
byebug (~> 11.0)
|
99
100
|
pry (~> 0.13.0)
|
101
|
+
racc (1.5.2)
|
100
102
|
rack (2.2.3)
|
101
103
|
rack-test (1.1.0)
|
102
104
|
rack (>= 1.0, < 3)
|
103
|
-
rails (5.2.4.
|
104
|
-
actioncable (= 5.2.4.
|
105
|
-
actionmailer (= 5.2.4.
|
106
|
-
actionpack (= 5.2.4.
|
107
|
-
actionview (= 5.2.4.
|
108
|
-
activejob (= 5.2.4.
|
109
|
-
activemodel (= 5.2.4.
|
110
|
-
activerecord (= 5.2.4.
|
111
|
-
activestorage (= 5.2.4.
|
112
|
-
activesupport (= 5.2.4.
|
105
|
+
rails (5.2.4.5)
|
106
|
+
actioncable (= 5.2.4.5)
|
107
|
+
actionmailer (= 5.2.4.5)
|
108
|
+
actionpack (= 5.2.4.5)
|
109
|
+
actionview (= 5.2.4.5)
|
110
|
+
activejob (= 5.2.4.5)
|
111
|
+
activemodel (= 5.2.4.5)
|
112
|
+
activerecord (= 5.2.4.5)
|
113
|
+
activestorage (= 5.2.4.5)
|
114
|
+
activesupport (= 5.2.4.5)
|
113
115
|
bundler (>= 1.3.0)
|
114
|
-
railties (= 5.2.4.
|
116
|
+
railties (= 5.2.4.5)
|
115
117
|
sprockets-rails (>= 2.0.0)
|
116
118
|
rails-dom-testing (2.0.3)
|
117
119
|
activesupport (>= 4.2.0)
|
118
120
|
nokogiri (>= 1.6)
|
119
121
|
rails-html-sanitizer (1.3.0)
|
120
122
|
loofah (~> 2.3)
|
121
|
-
railties (5.2.4.
|
122
|
-
actionpack (= 5.2.4.
|
123
|
-
activesupport (= 5.2.4.
|
123
|
+
railties (5.2.4.5)
|
124
|
+
actionpack (= 5.2.4.5)
|
125
|
+
activesupport (= 5.2.4.5)
|
124
126
|
method_source
|
125
127
|
rake (>= 0.8.7)
|
126
128
|
thor (>= 0.19.0, < 2.0)
|
127
129
|
rainbow (3.0.0)
|
128
|
-
rake (13.0.
|
130
|
+
rake (13.0.3)
|
129
131
|
rb-fsevent (0.10.4)
|
130
132
|
rb-inotify (0.10.1)
|
131
133
|
ffi (~> 1.0)
|
@@ -167,15 +169,15 @@ GEM
|
|
167
169
|
activesupport (>= 4.0)
|
168
170
|
sprockets (>= 3.0.0)
|
169
171
|
sqlite3 (1.4.2)
|
170
|
-
thor (1.0
|
172
|
+
thor (1.1.0)
|
171
173
|
thread_safe (0.3.6)
|
172
|
-
tzinfo (1.2.
|
174
|
+
tzinfo (1.2.9)
|
173
175
|
thread_safe (~> 0.1)
|
174
176
|
unicode-display_width (1.7.0)
|
175
177
|
websocket-driver (0.7.3)
|
176
178
|
websocket-extensions (>= 0.1.0)
|
177
179
|
websocket-extensions (0.1.5)
|
178
|
-
yard (0.9.
|
180
|
+
yard (0.9.26)
|
179
181
|
|
180
182
|
PLATFORMS
|
181
183
|
ruby
|
@@ -28,13 +28,12 @@ module DeclareSchema
|
|
28
28
|
field(:lock_version, :integer, default: 1, null: false)
|
29
29
|
end
|
30
30
|
|
31
|
-
def field(name, type, *args)
|
32
|
-
|
33
|
-
@model.declare_field(name, type, *(args + [@options.merge(options)]))
|
31
|
+
def field(name, type, *args, **options)
|
32
|
+
@model.declare_field(name, type, *[*args, @options.merge(options)])
|
34
33
|
end
|
35
34
|
|
36
35
|
def method_missing(name, *args)
|
37
|
-
field(name,
|
36
|
+
field(name, *args)
|
38
37
|
end
|
39
38
|
end
|
40
39
|
end
|
data/lib/declare_schema/model.rb
CHANGED
@@ -81,8 +81,7 @@ module DeclareSchema
|
|
81
81
|
# arguments. The arguments are forwarded to the #field_added
|
82
82
|
# callback, allowing custom metadata to be added to field
|
83
83
|
# declarations.
|
84
|
-
def declare_field(name, type, *args)
|
85
|
-
options = args.extract_options!
|
84
|
+
def declare_field(name, type, *args, **options)
|
86
85
|
try(:field_added, name, type, args, options)
|
87
86
|
add_serialize_for_field(name, type, options)
|
88
87
|
add_formatting_for_field(name, type)
|
@@ -0,0 +1,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module DeclareSchema
|
4
|
+
class UnknownTypeError < RuntimeError; end
|
5
|
+
|
6
|
+
module Model
|
7
|
+
# This class is a wrapper for the ActiveRecord::...::Column class
|
8
|
+
class Column
|
9
|
+
class << self
|
10
|
+
def native_type?(type)
|
11
|
+
type != :primary_key && native_types[type]
|
12
|
+
end
|
13
|
+
|
14
|
+
# MySQL example:
|
15
|
+
# { primary_key: "bigint auto_increment PRIMARY KEY",
|
16
|
+
# string: { name: "varchar", limit: 255 },
|
17
|
+
# text: { name: "text", limit: 65535},
|
18
|
+
# integer: {name: "int", limit: 4 },
|
19
|
+
# float: {name: "float", limit: 24 },
|
20
|
+
# decimal: { name: "decimal" },
|
21
|
+
# datetime: { name: "datetime" },
|
22
|
+
# timestamp: { name: "timestamp" },
|
23
|
+
# time: { name: "time" },
|
24
|
+
# date: { name: "date" },
|
25
|
+
# binary: { name>: "blob", limit: 65535 },
|
26
|
+
# boolean: { name: "tinyint", limit: 1 },
|
27
|
+
# json: { name: "json" } }
|
28
|
+
#
|
29
|
+
# SQLite example:
|
30
|
+
# { primary_key: "integer PRIMARY KEY AUTOINCREMENT NOT NULL",
|
31
|
+
# string: { name: "varchar" },
|
32
|
+
# text: { name: "text"},
|
33
|
+
# integer: { name: "integer" },
|
34
|
+
# float: { name: "float" },
|
35
|
+
# decimal: { name: "decimal" },
|
36
|
+
# datetime: { name: "datetime" },
|
37
|
+
# time: { name: "time" },
|
38
|
+
# date: { name: "date" },
|
39
|
+
# binary: { name: "blob" },
|
40
|
+
# boolean: { name: "boolean" },
|
41
|
+
# json: { name: "json" } }
|
42
|
+
def native_types
|
43
|
+
@native_types ||= ActiveRecord::Base.connection.native_database_types.tap do |types|
|
44
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
45
|
+
types[:text][:limit] ||= 0xffff
|
46
|
+
types[:binary][:limit] ||= 0xffff
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def deserialize_default_value(column, type, default_value)
|
52
|
+
type or raise ArgumentError, "must pass type; got #{type.inspect}"
|
53
|
+
|
54
|
+
case Rails::VERSION::MAJOR
|
55
|
+
when 4
|
56
|
+
# TODO: Delete this Rails 4 support ASAP! This could be wrong, since it's using the type of the old column...which
|
57
|
+
# might be getting migrated to a new type. We should be using just type as below. -Colin
|
58
|
+
column.type_cast_from_database(default_value)
|
59
|
+
else
|
60
|
+
cast_type = ActiveRecord::Base.connection.send(:lookup_cast_type, type) or
|
61
|
+
raise "cast_type not found for #{type}"
|
62
|
+
cast_type.deserialize(default_value)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Normalizes schema attributes for the given database adapter name.
|
67
|
+
# Note that the un-normalized attributes are still useful for generating migrations because those
|
68
|
+
# may be run with a different adapter.
|
69
|
+
# This method never mutates its argument.
|
70
|
+
def normalize_schema_attributes(schema_attributes, db_adapter_name)
|
71
|
+
case schema_attributes[:type]
|
72
|
+
when :boolean
|
73
|
+
schema_attributes.reverse_merge(limit: 1)
|
74
|
+
when :integer
|
75
|
+
schema_attributes.reverse_merge(limit: 8) if db_adapter_name.match?(/sqlite/i)
|
76
|
+
when :float
|
77
|
+
schema_attributes.except(:limit)
|
78
|
+
when :text
|
79
|
+
schema_attributes.except(:limit) if db_adapter_name.match?(/sqlite/i)
|
80
|
+
when :datetime
|
81
|
+
schema_attributes.reverse_merge(precision: 0)
|
82
|
+
when NilClass
|
83
|
+
raise ArgumentError, ":type key not found; keys: #{schema_attributes.keys.inspect}"
|
84
|
+
end || schema_attributes
|
85
|
+
end
|
86
|
+
|
87
|
+
def equivalent_schema_attributes?(schema_attributes_lhs, schema_attributes_rhs)
|
88
|
+
db_adapter_name = ActiveRecord::Base.connection.class.name
|
89
|
+
normalized_lhs = normalize_schema_attributes(schema_attributes_lhs, db_adapter_name)
|
90
|
+
normalized_rhs = normalize_schema_attributes(schema_attributes_rhs, db_adapter_name)
|
91
|
+
|
92
|
+
normalized_lhs == normalized_rhs
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
attr_reader :type
|
97
|
+
|
98
|
+
def initialize(model, current_table_name, column)
|
99
|
+
@model = model or raise ArgumentError, "must pass model"
|
100
|
+
@current_table_name = current_table_name or raise ArgumentError, "must pass current_table_name"
|
101
|
+
@column = column or raise ArgumentError, "must pass column"
|
102
|
+
@type = @column.type
|
103
|
+
self.class.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect}"
|
104
|
+
end
|
105
|
+
|
106
|
+
SCHEMA_KEYS = [:type, :limit, :precision, :scale, :null, :default].freeze
|
107
|
+
|
108
|
+
# omits keys with nil values
|
109
|
+
def schema_attributes
|
110
|
+
SCHEMA_KEYS.each_with_object({}) do |key, result|
|
111
|
+
value =
|
112
|
+
case key
|
113
|
+
when :default
|
114
|
+
self.class.deserialize_default_value(@column, @type, @column.default)
|
115
|
+
else
|
116
|
+
col_value = @column.send(key)
|
117
|
+
if col_value.nil? && (native_type = self.class.native_types[@column.type])
|
118
|
+
native_type[key]
|
119
|
+
else
|
120
|
+
col_value
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
result[key] = value unless value.nil?
|
125
|
+
end.tap do |result|
|
126
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i) && @column.type.in?([:string, :text])
|
127
|
+
result.merge!(collation_and_charset_for_column(@current_table_name, @column.name))
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
private
|
133
|
+
|
134
|
+
def collation_and_charset_for_column(current_table_name, column_name)
|
135
|
+
connection = ActiveRecord::Base.connection
|
136
|
+
connection.class.name.match?(/mysql/i) or raise ArgumentError, "only supported for MySQL"
|
137
|
+
|
138
|
+
database_name = connection.current_database
|
139
|
+
|
140
|
+
defaults = connection.select_one(<<~EOS)
|
141
|
+
SELECT C.character_set_name, C.collation_name
|
142
|
+
FROM information_schema.`COLUMNS` C
|
143
|
+
WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
|
144
|
+
C.table_name = '#{connection.quote_string(current_table_name)}' AND
|
145
|
+
C.column_name = '#{connection.quote_string(column_name)}';
|
146
|
+
EOS
|
147
|
+
|
148
|
+
defaults && defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect} from #{database_name}.#{current_table_name}.#{column_name}"
|
149
|
+
defaults && defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
|
150
|
+
|
151
|
+
{
|
152
|
+
charset: defaults["character_set_name"],
|
153
|
+
collation: defaults["collation_name"]
|
154
|
+
}
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -1,9 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require_relative 'column'
|
4
|
+
|
3
5
|
module DeclareSchema
|
6
|
+
class MysqlTextMayNotHaveDefault < RuntimeError; end
|
7
|
+
|
4
8
|
module Model
|
5
9
|
class FieldSpec
|
6
|
-
class UnknownSqlTypeError < RuntimeError; end
|
7
10
|
|
8
11
|
MYSQL_TINYTEXT_LIMIT = 0xff
|
9
12
|
MYSQL_TEXT_LIMIT = 0xffff
|
@@ -30,7 +33,18 @@ module DeclareSchema
|
|
30
33
|
end
|
31
34
|
end
|
32
35
|
|
33
|
-
attr_reader :model, :name, :type, :position, :options
|
36
|
+
attr_reader :model, :name, :type, :position, :options, :sql_options
|
37
|
+
|
38
|
+
TYPE_SYNONYMS = { timestamp: :datetime }.freeze # TODO: drop this synonym. -Colin
|
39
|
+
|
40
|
+
SQL_OPTIONS = [:limit, :precision, :scale, :null, :default, :charset, :collation].freeze
|
41
|
+
NON_SQL_OPTIONS = [:ruby_default, :validates].freeze
|
42
|
+
VALID_OPTIONS = (SQL_OPTIONS + NON_SQL_OPTIONS).freeze
|
43
|
+
OPTION_INDEXES = Hash[VALID_OPTIONS.each_with_index.to_a].freeze
|
44
|
+
|
45
|
+
VALID_OPTIONS.each do |option|
|
46
|
+
define_method(option) { @options[option] }
|
47
|
+
end
|
34
48
|
|
35
49
|
def initialize(model, name, type, position: 0, **options)
|
36
50
|
# TODO: TECH-5116
|
@@ -42,170 +56,71 @@ module DeclareSchema
|
|
42
56
|
@model = model
|
43
57
|
@name = name.to_sym
|
44
58
|
type.is_a?(Symbol) or raise ArgumentError, "type must be a Symbol; got #{type.inspect}"
|
45
|
-
@type = type
|
59
|
+
@type = TYPE_SYNONYMS[type] || type
|
46
60
|
@position = position
|
47
|
-
@options = options
|
48
|
-
|
61
|
+
@options = options.dup
|
62
|
+
|
63
|
+
@options.has_key?(:null) or @options[:null] = false
|
64
|
+
|
65
|
+
case @type
|
49
66
|
when :text
|
50
|
-
@options[:default] and raise "default may not be given for :text field #{model}##{@name}"
|
51
67
|
if self.class.mysql_text_limits?
|
68
|
+
@options[:default].nil? or raise MysqlTextMayNotHaveDefault, "when using MySQL, non-nil default may not be given for :text field #{model}##{@name}"
|
52
69
|
@options[:limit] = self.class.round_up_mysql_text_limit(@options[:limit] || MYSQL_LONGTEXT_LIMIT)
|
70
|
+
else
|
71
|
+
@options.delete(:limit)
|
53
72
|
end
|
54
73
|
when :string
|
55
|
-
@options[:limit] or raise "limit must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
|
74
|
+
@options[:limit] or raise "limit: must be given for :string field #{model}##{@name}: #{@options.inspect}; do you want `limit: 255`?"
|
56
75
|
when :bigint
|
57
76
|
@type = :integer
|
58
|
-
@options =
|
77
|
+
@options[:limit] = 8
|
59
78
|
end
|
60
79
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
end
|
80
|
+
Column.native_type?(@type) or raise UnknownTypeError, "#{@type.inspect}"
|
81
|
+
|
82
|
+
if @type.in?([:string, :text, :binary, :varbinary, :integer, :enum])
|
83
|
+
@options[:limit] ||= Column.native_types[@type][:limit]
|
66
84
|
else
|
67
|
-
@options
|
68
|
-
@options
|
85
|
+
@type != :decimal && @options.has_key?(:limit) and warn("unsupported limit: for SQL type #{@type} in field #{model}##{@name}")
|
86
|
+
@options.delete(:limit)
|
69
87
|
end
|
70
|
-
end
|
71
88
|
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
89
|
+
if @type == :decimal
|
90
|
+
@options[:precision] or warn("precision: required for :decimal type in field #{model}##{@name}")
|
91
|
+
@options[:scale] or warn("scale: required for :decimal type in field #{model}##{@name}")
|
92
|
+
else
|
93
|
+
if @type != :datetime
|
94
|
+
@options.has_key?(:precision) and warn("precision: only allowed for :decimal type or :datetime for SQL type #{@type} in field #{model}##{@name}")
|
95
|
+
end
|
96
|
+
@options.has_key?(:scale) and warn("scale: only allowed for :decimal type for SQL type #{@type} in field #{model}##{@name}")
|
79
97
|
end
|
80
98
|
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
else
|
86
|
-
field_class = DeclareSchema.to_class(type)
|
87
|
-
field_class && field_class::COLUMN_TYPE or raise UnknownSqlTypeError, "#{type.inspect} for #{model}##{@name}"
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
def sql_options
|
93
|
-
@options.except(:ruby_default, :validates)
|
94
|
-
end
|
95
|
-
|
96
|
-
def limit
|
97
|
-
@options[:limit] || native_types[sql_type][:limit]
|
98
|
-
end
|
99
|
-
|
100
|
-
def precision
|
101
|
-
@options[:precision]
|
102
|
-
end
|
103
|
-
|
104
|
-
def scale
|
105
|
-
@options[:scale]
|
106
|
-
end
|
107
|
-
|
108
|
-
def null
|
109
|
-
!:null.in?(@options) || @options[:null]
|
110
|
-
end
|
111
|
-
|
112
|
-
def default
|
113
|
-
@options[:default]
|
114
|
-
end
|
115
|
-
|
116
|
-
def charset
|
117
|
-
@options[:charset]
|
118
|
-
end
|
119
|
-
|
120
|
-
def collation
|
121
|
-
@options[:collation]
|
122
|
-
end
|
123
|
-
|
124
|
-
def same_type?(col_spec)
|
125
|
-
type = sql_type
|
126
|
-
normalized_type = TYPE_SYNONYMS[type] || type
|
127
|
-
normalized_col_spec_type = TYPE_SYNONYMS[col_spec.type] || col_spec.type
|
128
|
-
normalized_type == normalized_col_spec_type
|
129
|
-
end
|
130
|
-
|
131
|
-
def different_to?(table_name, col_spec)
|
132
|
-
!same_as(table_name, col_spec)
|
133
|
-
end
|
134
|
-
|
135
|
-
def same_as(table_name, col_spec)
|
136
|
-
same_type?(col_spec) &&
|
137
|
-
same_attributes?(col_spec) &&
|
138
|
-
(!type.in?([:text, :string]) || same_charset_and_collation?(table_name, col_spec))
|
139
|
-
end
|
140
|
-
|
141
|
-
private
|
142
|
-
|
143
|
-
def same_attributes?(col_spec)
|
144
|
-
native_type = native_types[type]
|
145
|
-
check_attributes = [:null, :default]
|
146
|
-
check_attributes += [:precision, :scale] if sql_type == :decimal && !col_spec.is_a?(SQLITE_COLUMN_CLASS) # remove when rails fixes https://rails.lighthouseapp.com/projects/8994-ruby-on-rails/tickets/2872
|
147
|
-
check_attributes -= [:default] if sql_type == :text && col_spec.class.name =~ /mysql/i
|
148
|
-
check_attributes << :limit if sql_type.in?([:string, :binary, :varbinary, :integer, :enum]) ||
|
149
|
-
(sql_type == :text && self.class.mysql_text_limits?)
|
150
|
-
check_attributes.all? do |k|
|
151
|
-
if k == :default
|
152
|
-
case Rails::VERSION::MAJOR
|
153
|
-
when 4
|
154
|
-
col_spec.type_cast_from_database(col_spec.default) == col_spec.type_cast_from_database(default)
|
155
|
-
else
|
156
|
-
cast_type = ActiveRecord::Base.connection.lookup_cast_type_from_column(col_spec) or raise "cast_type not found for #{col_spec.inspect}"
|
157
|
-
cast_type.deserialize(col_spec.default) == cast_type.deserialize(default)
|
158
|
-
end
|
99
|
+
if @type.in?([:text, :string])
|
100
|
+
if ActiveRecord::Base.connection.class.name.match?(/mysql/i)
|
101
|
+
@options[:charset] ||= model.table_options[:charset] || Generators::DeclareSchema::Migration::Migrator.default_charset
|
102
|
+
@options[:collation] ||= model.table_options[:collation] || Generators::DeclareSchema::Migration::Migrator.default_collation
|
159
103
|
else
|
160
|
-
|
161
|
-
|
162
|
-
col_value = native_type[k]
|
163
|
-
end
|
164
|
-
col_value == send(k)
|
104
|
+
@options.delete(:charset)
|
105
|
+
@options.delete(:collation)
|
165
106
|
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
|
169
|
-
def same_charset_and_collation?(table_name, col_spec)
|
170
|
-
current_collation_and_charset = collation_and_charset_for_column(table_name, col_spec)
|
171
|
-
|
172
|
-
collation == current_collation_and_charset[:collation] &&
|
173
|
-
charset == current_collation_and_charset[:charset]
|
174
|
-
end
|
175
|
-
|
176
|
-
def collation_and_charset_for_column(table_name, col_spec)
|
177
|
-
column_name = col_spec.name
|
178
|
-
connection = ActiveRecord::Base.connection
|
179
|
-
|
180
|
-
if connection.class.name.match?(/mysql/i)
|
181
|
-
database_name = connection.current_database
|
182
|
-
|
183
|
-
defaults = connection.select_one(<<~EOS)
|
184
|
-
SELECT C.character_set_name, C.collation_name
|
185
|
-
FROM information_schema.`COLUMNS` C
|
186
|
-
WHERE C.table_schema = '#{connection.quote_string(database_name)}' AND
|
187
|
-
C.table_name = '#{connection.quote_string(table_name)}' AND
|
188
|
-
C.column_name = '#{connection.quote_string(column_name)}';
|
189
|
-
EOS
|
190
|
-
|
191
|
-
defaults["character_set_name"] or raise "character_set_name missing from #{defaults.inspect}"
|
192
|
-
defaults["collation_name"] or raise "collation_name missing from #{defaults.inspect}"
|
193
|
-
|
194
|
-
{
|
195
|
-
charset: defaults["character_set_name"],
|
196
|
-
collation: defaults["collation_name"]
|
197
|
-
}
|
198
107
|
else
|
199
|
-
{}
|
108
|
+
@options[:charset] and warn("charset may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
|
109
|
+
@options[:collation] and warne("collation may only given for :string and :text fields for SQL type #{@type} in field #{model}##{@name}")
|
200
110
|
end
|
201
|
-
end
|
202
111
|
|
203
|
-
|
204
|
-
|
112
|
+
@options = Hash[@options.sort_by { |k, _v| OPTION_INDEXES[k] || 9999 }]
|
113
|
+
|
114
|
+
@sql_options = @options.slice(*SQL_OPTIONS)
|
205
115
|
end
|
206
116
|
|
207
|
-
|
208
|
-
|
117
|
+
# returns the attributes for schema migrations as a Hash
|
118
|
+
# omits name and position since those are meta-data above the schema
|
119
|
+
# omits keys with nil values
|
120
|
+
def schema_attributes(col_spec)
|
121
|
+
@sql_options.merge(type: @type).tap do |attrs|
|
122
|
+
attrs[:default] = Column.deserialize_default_value(col_spec, @type, attrs[:default])
|
123
|
+
end.compact
|
209
124
|
end
|
210
125
|
end
|
211
126
|
end
|