convergence 1.0.6 → 1.1.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 (69) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +35 -4
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +46 -0
  5. data/CONTRIBUTING.md +80 -0
  6. data/Gemfile.lock +23 -8
  7. data/README.md +131 -1
  8. data/Rakefile +45 -2
  9. data/convergence.gemspec +3 -1
  10. data/lib/convergence/cli.rb +10 -2
  11. data/lib/convergence/command/apply.rb +38 -9
  12. data/lib/convergence/command/dryrun.rb +5 -5
  13. data/lib/convergence/command/export.rb +19 -1
  14. data/lib/convergence/command.rb +6 -0
  15. data/lib/convergence/database_connector/postgres_connector.rb +25 -0
  16. data/lib/convergence/database_connector/sqlite_connector.rb +22 -0
  17. data/lib/convergence/database_connector.rb +6 -0
  18. data/lib/convergence/default_parameter/mysql_default_parameter.rb +1 -1
  19. data/lib/convergence/default_parameter/postgres_default_parameter.rb +57 -0
  20. data/lib/convergence/default_parameter/sqlite_default_parameter.rb +57 -0
  21. data/lib/convergence/default_parameter.rb +6 -0
  22. data/lib/convergence/diff.rb +9 -2
  23. data/lib/convergence/dsl.rb +18 -3
  24. data/lib/convergence/dumper/mysql_schema_dumper.rb +37 -9
  25. data/lib/convergence/dumper/postgres_schema_dumper.rb +193 -0
  26. data/lib/convergence/dumper/sqlite_schema_dumper.rb +132 -0
  27. data/lib/convergence/dumper.rb +113 -0
  28. data/lib/convergence/sql_generator/mysql_generator.rb +18 -3
  29. data/lib/convergence/sql_generator/postgres_generator.rb +256 -0
  30. data/lib/convergence/sql_generator/sqlite_generator.rb +178 -0
  31. data/lib/convergence/version.rb +1 -1
  32. data/spec/config/spec_database.yml +12 -0
  33. data/spec/convergence/config_spec.rb +75 -0
  34. data/spec/convergence/diff_spec.rb +125 -45
  35. data/spec/convergence/dsl_spec.rb +26 -0
  36. data/spec/convergence/dumper/mysql_schema_dumper_spec.rb +25 -2
  37. data/spec/convergence/dumper/postgres_schema_dumper_spec.rb +103 -0
  38. data/spec/convergence/dumper/sqlite_schema_dumper_spec.rb +85 -0
  39. data/spec/convergence/dumper_spec.rb +110 -16
  40. data/spec/convergence/foreign_key_spec.rb +33 -0
  41. data/spec/convergence/index_spec.rb +52 -0
  42. data/spec/convergence/pretty_diff_spec.rb +85 -0
  43. data/spec/convergence/table_spec.rb +23 -0
  44. data/spec/fixtures/add_table_with_enum_set.schema +5 -0
  45. data/spec/fixtures/change_table_comment_to_paper.schema +2 -0
  46. data/spec/fixtures/execute_raw_sql.schema +28 -0
  47. data/spec/fixtures/postgres/add_columns_to_paper.schema +29 -0
  48. data/spec/fixtures/postgres/add_table.schema +32 -0
  49. data/spec/fixtures/postgres/change_comment_columns_to_paper.schema +28 -0
  50. data/spec/fixtures/postgres/change_table_comment_to_paper.schema +28 -0
  51. data/spec/fixtures/postgres/drop_foreign_key.schema +25 -0
  52. data/spec/fixtures/postgres/drop_table.schema +19 -0
  53. data/spec/fixtures/postgres/remove_columns_to_paper.schema +27 -0
  54. data/spec/fixtures/postgres_test_db.sql +41 -0
  55. data/spec/fixtures/sqlite/add_columns_to_paper.schema +29 -0
  56. data/spec/fixtures/sqlite/add_table.schema +32 -0
  57. data/spec/fixtures/sqlite/change_columns_to_paper.schema +28 -0
  58. data/spec/fixtures/sqlite/drop_foreign_key.schema +25 -0
  59. data/spec/fixtures/sqlite/drop_table.schema +19 -0
  60. data/spec/fixtures/sqlite/remove_columns_to_paper.schema +27 -0
  61. data/spec/fixtures/sqlite_test_db.sql +31 -0
  62. data/spec/fixtures/test_db.sql +10 -0
  63. data/spec/integrations/command_diff.rb +35 -0
  64. data/spec/integrations/command_dryrun.rb +37 -2
  65. data/spec/integrations/command_export.rb +28 -0
  66. data/spec/postgres_integrations/command_dryrun.rb +79 -0
  67. data/spec/spec_helper.rb +13 -0
  68. data/spec/sqlite_integrations/command_dryrun.rb +70 -0
  69. metadata +96 -8
@@ -0,0 +1,132 @@
1
+ require 'convergence/dumper'
2
+ require 'convergence/table'
3
+
4
+ class Convergence::Dumper::SqliteSchemaDumper
5
+ DEFAULT_VALUE_PATTERN = /\A'(?<value>(?:[^']|'')*)'\z/.freeze
6
+ # SQLite has no CONSTRAINT-name introspection pragma; the name is only
7
+ # preserved in the original CREATE TABLE text.
8
+ FOREIGN_KEY_NAME_PATTERN = /CONSTRAINT\s+"?(?<name>\w+)"?\s+FOREIGN KEY\s*\(\s*"?(?<column>\w+)"?\s*\)/i.freeze
9
+
10
+ def initialize(connector)
11
+ @connector = connector
12
+ @tables = {}
13
+ end
14
+
15
+ def dump
16
+ table_names.each do |table_name|
17
+ table = Convergence::Table.new(table_name)
18
+ parse_columns(table, column_definitions(table_name))
19
+ parse_indexes(table, index_definitions(table_name))
20
+ parse_foreign_keys(table, table_name)
21
+ @tables[table_name] = table
22
+ end
23
+ @tables
24
+ end
25
+
26
+ private
27
+
28
+ def db
29
+ @connector.schema_client
30
+ end
31
+
32
+ def table_names
33
+ db.execute("SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%'").map { |r| r['name'] }
34
+ end
35
+
36
+ def table_sql(table_name)
37
+ row = db.execute('SELECT sql FROM sqlite_master WHERE type = ? AND name = ?', ['table', table_name]).first
38
+ row ? row['sql'].to_s : ''
39
+ end
40
+
41
+ def column_definitions(table_name)
42
+ db.execute(%(PRAGMA table_info("#{table_name}")))
43
+ end
44
+
45
+ def index_definitions(table_name)
46
+ # origin 'c' is an explicitly created index (CREATE [UNIQUE] INDEX). 'pk' and
47
+ # 'u' are auto-generated by a PRIMARY KEY/UNIQUE column constraint, already
48
+ # represented on the column itself, so they are excluded here.
49
+ db.execute(%(PRAGMA index_list("#{table_name}"))).select { |r| r['origin'] == 'c' }
50
+ end
51
+
52
+ def index_column_definitions(index_name)
53
+ db.execute(%(PRAGMA index_info("#{index_name}")))
54
+ end
55
+
56
+ def foreign_key_definitions(table_name)
57
+ db.execute(%(PRAGMA foreign_key_list("#{table_name}")))
58
+ end
59
+
60
+ def parse_columns(table, columns)
61
+ pk_columns = columns.select { |c| c['pk'].to_i > 0 }
62
+ single_integer_pk_cid = pk_columns.size == 1 ? pk_columns.first['cid'] : nil
63
+ columns.each do |column|
64
+ data_type, column_name, options = parse_column(column, single_integer_pk_cid)
65
+ table.send(data_type, column_name, options)
66
+ end
67
+ end
68
+
69
+ def parse_column(column, single_integer_pk_cid)
70
+ raw_type = column['type'].to_s
71
+ type_name = raw_type[/\A(\w+)/, 1].to_s
72
+ column_name = column['name']
73
+ is_auto_increment_pk = !single_integer_pk_cid.nil? && column['cid'] == single_integer_pk_cid && type_name.upcase == 'INTEGER'
74
+ data_type = if is_auto_increment_pk
75
+ :int
76
+ elsif type_name.empty?
77
+ :text
78
+ else
79
+ type_name.downcase.to_sym
80
+ end
81
+ options = { null: column['notnull'].to_i.zero? }
82
+ if is_auto_increment_pk
83
+ # SQLite's `notnull` pragma column doesn't reflect the implicit NOT NULL
84
+ # of an INTEGER PRIMARY KEY (rowid alias), so it must be forced here.
85
+ options.merge!(primary_key: true, extra: 'auto_increment', null: false)
86
+ else
87
+ options.merge!(primary_key: true) if column['pk'].to_i > 0
88
+ options.merge!(default: column_default_expression(data_type, column['dflt_value'])) unless column['dflt_value'].nil?
89
+ end
90
+ case data_type
91
+ when :decimal
92
+ precision, scale = raw_type.scan(/\d+/)
93
+ options.merge!(precision: precision, scale: scale) if precision
94
+ else
95
+ limit = raw_type[/\((\d+)\)/, 1]
96
+ options.merge!(limit: limit) unless limit.nil?
97
+ end
98
+ [data_type, column_name, options]
99
+ end
100
+
101
+ def column_default_expression(data_type, value)
102
+ return -> { 'CURRENT_TIMESTAMP' } if data_type == :datetime && value.to_s.upcase == 'CURRENT_TIMESTAMP'
103
+ match = DEFAULT_VALUE_PATTERN.match(value)
104
+ return match[:value].gsub("''", "'") if match
105
+ value
106
+ end
107
+
108
+ def parse_indexes(table, indexes)
109
+ indexes.each do |index|
110
+ columns = index_column_definitions(index['name']).sort_by { |c| c['seqno'].to_i }.map { |c| c['name'] }
111
+ options = { name: index['name'], unique: index['unique'].to_i == 1 }
112
+ table.index(columns, options)
113
+ end
114
+ end
115
+
116
+ def parse_foreign_keys(table, table_name)
117
+ foreign_key_names = table_sql(table_name).scan(FOREIGN_KEY_NAME_PATTERN).to_h { |name, column| [column, name] }
118
+ foreign_key_definitions(table_name).group_by { |r| r['id'] }.each do |_id, rows|
119
+ sorted_rows = rows.sort_by { |r| r['seq'].to_i }
120
+ columns = sorted_rows.map { |r| r['from'] }
121
+ to_table = sorted_rows.first['table']
122
+ to_columns = sorted_rows.map { |r| r['to'] }
123
+ key_name = foreign_key_names[columns.first] || "fk_#{table.table_name}_#{columns.join('_')}"
124
+ options = {
125
+ reference: to_table,
126
+ reference_column: to_columns,
127
+ name: key_name
128
+ }
129
+ table.foreign_key(columns, options)
130
+ end
131
+ end
132
+ end
@@ -1,4 +1,39 @@
1
1
  class Convergence::Dumper
2
+ # Convergence's DSL type names follow MySQL terminology; map them to the
3
+ # closest ActiveRecord migration column type.
4
+ RAILS_TYPE_MAPPING = {
5
+ tinyint: :integer,
6
+ smallint: :integer,
7
+ mediumint: :integer,
8
+ int: :integer,
9
+ bigint: :bigint,
10
+ float: :float,
11
+ double: :float,
12
+ decimal: :decimal,
13
+ char: :string,
14
+ varchar: :string,
15
+ tinyblob: :binary,
16
+ blob: :binary,
17
+ mediumblob: :binary,
18
+ longblob: :binary,
19
+ tinytext: :text,
20
+ text: :text,
21
+ mediumtext: :text,
22
+ longtext: :text,
23
+ date: :date,
24
+ time: :time,
25
+ datetime: :datetime,
26
+ timestamp: :datetime,
27
+ year: :integer,
28
+ json: :json,
29
+ # ActiveRecord has no native enum/set column type; fall back to string.
30
+ enum: :string,
31
+ set: :string
32
+ }.freeze
33
+ # Options that only make sense for convergence's own SQL generators, with no
34
+ # equivalent in an ActiveRecord migration's create_table DSL.
35
+ RAILS_UNSUPPORTED_COLUMN_OPTIONS = %i[character_set collate extra after values].freeze
36
+
2
37
  def dump_dsl(tables)
3
38
  tables.map do |_, table|
4
39
  dump_table_dsl(table)
@@ -24,8 +59,84 @@ class Convergence::Dumper
24
59
  dsl
25
60
  end
26
61
 
62
+ def dump_rails_migration(tables, class_name, rails_version: '7.0')
63
+ dsl = "class #{class_name} < ActiveRecord::Migration[#{rails_version}]\n"
64
+ dsl += " def change\n"
65
+ dsl += tables.map { |_, table| dump_table_rails_migration(table) }.join("\n\n")
66
+ dsl += "\n end\n"
67
+ dsl += 'end'
68
+ dsl
69
+ end
70
+
71
+ def dump_table_rails_migration(table)
72
+ table_argument = [table.table_name.to_sym.inspect]
73
+ dsl = " create_table #{table_argument.join(', ')} do |t|\n"
74
+ body_lines = rails_column_lines(table.columns)
75
+ body_lines += table.indexes.map { |_, index| dump_rails_index(index) }
76
+ body_lines += table.foreign_keys.map { |_, key| dump_rails_foreign_key(key) }
77
+ dsl += body_lines.map { |line| " #{line}" }.join("\n")
78
+ dsl += "\n end"
79
+ dsl
80
+ end
81
+
27
82
  private
28
83
 
84
+ # ActiveRecord's create_table implicitly adds an auto-incrementing `id`
85
+ # primary key, so an explicit column matching that shape is redundant.
86
+ def default_primary_key_column?(column_name, column)
87
+ column_name == 'id' &&
88
+ [:int, :bigint].include?(column.type) &&
89
+ column.options[:primary_key] &&
90
+ column.options[:extra].to_s.upcase.include?('AUTO_INCREMENT')
91
+ end
92
+
93
+ def rails_timestamp_column?(column)
94
+ column.type == :datetime && Convergence::Dumper::RAILS_UNSUPPORTED_COLUMN_OPTIONS.none? { |k| column.options.key?(k) }
95
+ end
96
+
97
+ def rails_column_lines(columns)
98
+ columns = columns.reject { |name, column| default_primary_key_column?(name, column) }
99
+ if %w[created_at updated_at].all? { |name| columns.key?(name) && rails_timestamp_column?(columns[name]) } &&
100
+ columns['created_at'].options[:null] == columns['updated_at'].options[:null]
101
+ remaining = columns.reject { |name, _| %w[created_at updated_at].include?(name) }
102
+ lines = remaining.map { |_, column| dump_rails_column(column) }
103
+ lines << (columns['created_at'].options[:null] ? 't.timestamps' : 't.timestamps null: false')
104
+ lines
105
+ else
106
+ columns.map { |_, column| dump_rails_column(column) }
107
+ end
108
+ end
109
+
110
+ def dump_rails_column(column)
111
+ argument = [column.column_name.to_sym.inspect]
112
+ options = column.options.reject { |k, _v| RAILS_UNSUPPORTED_COLUMN_OPTIONS.include?(k) }
113
+ if column.type == :tinyint && column.options[:limit].to_s == '1'
114
+ column_type = :boolean
115
+ options = options.reject { |k, _v| k == :limit }
116
+ options = options.merge(default: false) if options[:default].to_s == '0'
117
+ options = options.merge(default: true) if options[:default].to_s == '1'
118
+ else
119
+ column_type = RAILS_TYPE_MAPPING[column.type] || column.type
120
+ end
121
+ argument << options.map { |k, v| key_value_text(k, v) }
122
+ "t.#{column_type} #{argument.flatten.join(', ')}"
123
+ end
124
+
125
+ def dump_rails_index(index)
126
+ argument = [single_or_multiple_symbol(index.index_columns)]
127
+ options = index.options.select { |k, _v| %i[name unique].include?(k) }
128
+ argument << options.map { |k, v| key_value_text(k, v) }
129
+ "t.index #{argument.flatten.join(', ')}"
130
+ end
131
+
132
+ def dump_rails_foreign_key(foreign_key)
133
+ argument = [foreign_key.to_table.to_sym.inspect]
134
+ argument << "column: #{single_or_multiple_symbol(foreign_key.from_columns)}"
135
+ argument << "primary_key: #{single_or_multiple_symbol(foreign_key.to_columns)}" unless foreign_key.to_columns == ['id']
136
+ argument << key_value_text('name', foreign_key.key_name)
137
+ "t.foreign_key #{argument.join(', ')}"
138
+ end
139
+
29
140
  def dump_column(column)
30
141
  argument = [column.column_name.to_sym.inspect]
31
142
  case [column.type, column.options[:limit]]
@@ -72,6 +183,8 @@ class Convergence::Dumper
72
183
  def key_value_text(k, v)
73
184
  value = if v.to_s == 'true' || v.to_s == 'false' || v.to_s =~ /^\d+$/
74
185
  v
186
+ elsif v.is_a?(Proc)
187
+ %(-> { #{v.call.inspect} })
75
188
  else
76
189
  %(#{v.inspect})
77
190
  end
@@ -13,11 +13,11 @@ class SQLGenerator::MysqlGenerator < SQLGenerator
13
13
 
14
14
  attr_reader :original_table
15
15
 
16
- def generate(to_table, delta, original_table)
16
+ def generate(to_table, delta, original_table, safe_migration: false)
17
17
  @original_table = original_table
18
18
  sqls = []
19
19
  sqls << change_table_sql(to_table, delta)
20
- sqls << drop_table_sqls(delta)
20
+ sqls << (safe_migration ? [] : drop_table_sqls(delta))
21
21
  sqls << create_table_sqls(delta)
22
22
  sqls.reject!(&:empty?)
23
23
  sqls.join("\n")
@@ -166,6 +166,9 @@ DROP TABLE `#{table_name}`;
166
166
  def create_column_sql(column, output_primary_key: false, output_auto_increment: true)
167
167
  sql = "`#{column.column_name}`"
168
168
  sql += " #{column.type}"
169
+ if [:enum, :set].include?(column.type)
170
+ sql += "(#{quote_enum_or_set_values(column.options[:values])})"
171
+ end
169
172
  sql += "(#{column.options[:limit]})" unless column.options[:limit].nil?
170
173
  if column.options[:precision] && column.options[:scale]
171
174
  sql += "(#{column.options[:precision]}, #{column.options[:scale]})"
@@ -188,7 +191,7 @@ DROP TABLE `#{table_name}`;
188
191
  sql += ' PRIMARY KEY'
189
192
  end
190
193
  if column.options[:default]
191
- sql += " DEFAULT '#{column.options[:default]}'"
194
+ sql += " DEFAULT #{quote_default_expression(column.options[:default])}"
192
195
  end
193
196
  if column.options[:comment]
194
197
  sql += " COMMENT '#{column.options[:comment]}'"
@@ -244,4 +247,16 @@ DROP TABLE `#{table_name}`;
244
247
  end
245
248
  end.join(' ')
246
249
  end
250
+
251
+ def quote_enum_or_set_values(values)
252
+ Array(values).map { |v| "'#{v.to_s.gsub("'", "''")}'" }.join(',')
253
+ end
254
+
255
+ def quote_default_expression(value)
256
+ if value.is_a?(Proc)
257
+ value.call
258
+ else
259
+ %('#{value}')
260
+ end
261
+ end
247
262
  end
@@ -0,0 +1,256 @@
1
+ require 'convergence/sql_generator'
2
+
3
+ class SQLGenerator::PostgresGenerator < SQLGenerator
4
+ # Convergence's DSL type names follow MySQL terminology (kept for cross-adapter DSL
5
+ # compatibility). Map them to their closest native PostgreSQL type.
6
+ TYPE_MAPPING = {
7
+ tinyint: 'smallint',
8
+ smallint: 'smallint',
9
+ mediumint: 'integer',
10
+ int: 'integer',
11
+ bigint: 'bigint',
12
+ float: 'real',
13
+ double: 'double precision',
14
+ decimal: 'decimal',
15
+ char: 'char',
16
+ varchar: 'varchar',
17
+ tinyblob: 'bytea',
18
+ blob: 'bytea',
19
+ mediumblob: 'bytea',
20
+ longblob: 'bytea',
21
+ tinytext: 'text',
22
+ text: 'text',
23
+ mediumtext: 'text',
24
+ longtext: 'text',
25
+ date: 'date',
26
+ time: 'time',
27
+ datetime: 'timestamp',
28
+ timestamp: 'timestamp',
29
+ year: 'integer',
30
+ json: 'jsonb'
31
+ }.freeze
32
+ TYPES_WITH_LIMIT = [:char, :varchar].freeze
33
+ UNSUPPORTED_TYPES = [:enum, :set].freeze
34
+
35
+ attr_reader :original_table
36
+
37
+ def generate(to_table, delta, original_table, safe_migration: false)
38
+ @original_table = original_table
39
+ sqls = []
40
+ sqls << change_table_sql(to_table, delta)
41
+ sqls << (safe_migration ? [] : drop_table_sqls(delta))
42
+ sqls << create_table_sqls(delta)
43
+ sqls.reject!(&:empty?)
44
+ sqls.join("\n")
45
+ end
46
+
47
+ private
48
+
49
+ # FIXME: multiple pk change not supported yet
50
+ def change_table_sql(to_table, delta)
51
+ change_table = delta[:change_table]
52
+ results = []
53
+ change_table.each do |table_name, table_delta|
54
+ unless table_delta[:remove_foreign_key].empty?
55
+ results << alter_remove_foreign_keys_sql(table_name, table_delta[:remove_foreign_key].keys)
56
+ end
57
+ table_delta[:remove_index].each do |index_name, _index|
58
+ results << alter_remove_index_sql(index_name)
59
+ end
60
+ unless table_delta[:remove_column].empty?
61
+ results << alter_remove_columns_sql(table_name, table_delta[:remove_column].values)
62
+ end
63
+ unless table_delta[:add_column].empty?
64
+ results << alter_add_columns_sql(table_name, table_delta[:add_column].values)
65
+ end
66
+ table_delta[:change_column].each do |column_name, column|
67
+ results << alter_change_column_sql(table_name, column_name, column, to_table)
68
+ end
69
+ table_delta[:add_index].each do |_index_name, index|
70
+ results << alter_add_index_sql(table_name, index)
71
+ end
72
+ unless table_delta[:add_foreign_key].empty?
73
+ results << alter_add_foreign_keys_sql(table_name, table_delta[:add_foreign_key].values)
74
+ end
75
+ unless table_delta[:change_table_option].empty?
76
+ results << alter_change_table_sql(table_name, table_delta[:change_table_option])
77
+ end
78
+ end
79
+ results << '' unless results.empty?
80
+ results
81
+ end
82
+
83
+ def alter_add_columns_sql(table_name, columns)
84
+ add_column_sqls = columns.map { |column| %(ALTER TABLE "#{table_name}" ADD COLUMN #{create_column_sql(column, output_primary_key: true)};) }
85
+ comment_sqls = columns.map { |column| comment_on_column_sql(table_name, column) }.compact
86
+ (add_column_sqls + comment_sqls).join("\n")
87
+ end
88
+
89
+ def alter_remove_columns_sql(table_name, columns)
90
+ columns.map { |column| %(ALTER TABLE "#{table_name}" DROP COLUMN "#{column.column_name}";) }.join("\n")
91
+ end
92
+
93
+ def alter_change_column_sql(table_name, column_name, change_column_option, to_table)
94
+ column = to_table[table_name].columns[column_name]
95
+ column.options.merge!(after: change_column_option[:after]) unless change_column_option[:after].nil?
96
+ sqls = []
97
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" TYPE #{postgres_column_type(column)} USING "#{column_name}"::#{postgres_column_type(column)};)
98
+ if column.options[:null]
99
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" DROP NOT NULL;)
100
+ else
101
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" SET NOT NULL;)
102
+ end
103
+ if column.options[:default]
104
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" SET DEFAULT #{quote_default_expression(column.options[:default])};)
105
+ else
106
+ sqls << %(ALTER TABLE "#{table_name}" ALTER COLUMN "#{column_name}" DROP DEFAULT;)
107
+ end
108
+ comment_sql = comment_on_column_sql(table_name, column)
109
+ sqls << comment_sql unless comment_sql.nil?
110
+ sqls.join("\n")
111
+ end
112
+
113
+ def alter_change_table_sql(table_name, change_table_option)
114
+ return '' unless change_table_option.key?(:comment)
115
+ %(COMMENT ON TABLE "#{table_name}" IS '#{escape_quote(change_table_option[:comment])}';)
116
+ end
117
+
118
+ def alter_remove_index_sql(index_name)
119
+ %(DROP INDEX "#{index_name}";)
120
+ end
121
+
122
+ def alter_add_index_sql(table_name, index)
123
+ sql = 'CREATE'
124
+ sql += ' UNIQUE' if index.options[:unique]
125
+ sql += %( INDEX "#{index.index_name}" ON "#{table_name}" (#{quoted_index_columns(index).join(',')});)
126
+ sql
127
+ end
128
+
129
+ def alter_remove_foreign_keys_sql(table_name, index_names)
130
+ index_names.map { |index_name| %(ALTER TABLE "#{table_name}" DROP CONSTRAINT "#{index_name}";) }.join("\n")
131
+ end
132
+
133
+ def alter_add_foreign_keys_sql(table_name, foreign_keys)
134
+ foreign_keys.map { |foreign_key| %(ALTER TABLE "#{table_name}" ADD #{foreign_key_constraint_sql(foreign_key)};) }.join("\n")
135
+ end
136
+
137
+ def foreign_key_constraint_sql(foreign_key)
138
+ sql = %(CONSTRAINT "#{foreign_key.key_name}" FOREIGN KEY )
139
+ sql += "(#{[foreign_key.from_columns].flatten.map { |v| %("#{v}") }.join(',')}) "
140
+ sql += %(REFERENCES "#{foreign_key.to_table}" )
141
+ sql += "(#{[foreign_key.to_columns].flatten.map { |v| %("#{v}") }.join(',')})"
142
+ sql
143
+ end
144
+
145
+ def create_table_sqls(delta)
146
+ delta[:add_table].map do |table_name, table|
147
+ column_sql = (create_table_column_sql(table) << create_table_index_sql(table))
148
+ .flatten
149
+ .reject(&:empty?)
150
+ .join(",\n ")
151
+ sql = <<-SQL
152
+ CREATE TABLE "#{table_name}" (
153
+ #{column_sql}
154
+ );
155
+ SQL
156
+ sql = sql.strip
157
+ index_sqls = table.indexes.values.map { |index| alter_add_index_sql(table_name, index) }
158
+ comment_sqls = create_table_comment_sqls(table_name, table)
159
+ ([sql] + index_sqls + comment_sqls).join("\n")
160
+ end
161
+ end
162
+
163
+ def create_table_comment_sqls(table_name, table)
164
+ sqls = []
165
+ if table.table_options[:comment] && !table.table_options[:comment].to_s.empty?
166
+ sqls << %(COMMENT ON TABLE "#{table_name}" IS '#{escape_quote(table.table_options[:comment])}';)
167
+ end
168
+ table.columns.values.each do |column|
169
+ comment_sql = comment_on_column_sql(table_name, column)
170
+ sqls << comment_sql unless comment_sql.nil?
171
+ end
172
+ sqls
173
+ end
174
+
175
+ def comment_on_column_sql(table_name, column)
176
+ return nil if column.options[:comment].nil? || column.options[:comment].to_s.empty?
177
+ %(COMMENT ON COLUMN "#{table_name}"."#{column.column_name}" IS '#{escape_quote(column.options[:comment])}';)
178
+ end
179
+
180
+ def drop_table_sqls(delta)
181
+ delta[:remove_table].map do |table_name, _|
182
+ %(DROP TABLE "#{table_name}";)
183
+ end
184
+ end
185
+
186
+ def create_table_column_sql(table)
187
+ table.columns.values.map do |column|
188
+ create_column_sql(column)
189
+ end
190
+ end
191
+
192
+ def create_column_sql(column, output_primary_key: false, output_identity: true)
193
+ fail NotImplementedError.new("#{column.type} is not supported on PostgreSQL adapter yet") if UNSUPPORTED_TYPES.include?(column.type)
194
+ sql = %("#{column.column_name}")
195
+ sql += " #{postgres_column_type(column)}"
196
+ if column.options[:null]
197
+ sql += ' DEFAULT NULL' unless column.options[:default]
198
+ else
199
+ sql += ' NOT NULL'
200
+ end
201
+ if column.options[:primary_key] && output_primary_key
202
+ sql += ' PRIMARY KEY'
203
+ end
204
+ if column.options[:default]
205
+ sql += " DEFAULT #{quote_default_expression(column.options[:default])}"
206
+ end
207
+ if auto_increment?(column) && output_identity
208
+ sql += ' GENERATED BY DEFAULT AS IDENTITY'
209
+ end
210
+ sql
211
+ end
212
+
213
+ def postgres_column_type(column)
214
+ return 'boolean' if column.type == :tinyint && column.options[:limit].to_s == '1'
215
+ type = TYPE_MAPPING[column.type] || column.type.to_s
216
+ if TYPES_WITH_LIMIT.include?(column.type) && column.options[:limit]
217
+ "#{type}(#{column.options[:limit]})"
218
+ elsif column.type == :decimal && column.options[:precision] && column.options[:scale]
219
+ "#{type}(#{column.options[:precision]}, #{column.options[:scale]})"
220
+ else
221
+ type
222
+ end
223
+ end
224
+
225
+ def auto_increment?(column)
226
+ extra = column.options[:extra]
227
+ !extra.nil? && extra.to_s.upcase.include?('AUTO_INCREMENT')
228
+ end
229
+
230
+ def create_table_index_sql(table)
231
+ pkeys = table.columns.select { |_k, v| v.options[:primary_key] }
232
+ foreign_keys = table.foreign_keys.values
233
+ results = []
234
+ unless pkeys.empty?
235
+ results << %(PRIMARY KEY (#{pkeys.keys.map { |v| %("#{v}") }.join(',')}))
236
+ end
237
+ results << foreign_keys.map { |fk| foreign_key_constraint_sql(fk) }
238
+ results
239
+ end
240
+
241
+ def quoted_index_columns(index)
242
+ [index.index_columns].flatten.map { |v| %("#{v}") }
243
+ end
244
+
245
+ def escape_quote(value)
246
+ value.to_s.gsub("'", "''")
247
+ end
248
+
249
+ def quote_default_expression(value)
250
+ if value.is_a?(Proc)
251
+ value.call
252
+ else
253
+ %('#{escape_quote(value)}')
254
+ end
255
+ end
256
+ end