db_schema 0.1.1 → 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 654c088e5c985a78e896af87cd0b4ea07a90767d
4
- data.tar.gz: 559f43436988fb15091f53f6dd1aa95c8ee495f4
3
+ metadata.gz: 7a6dfe3738279a4199344a79ce5bfbbb7e504d50
4
+ data.tar.gz: 035b90071d29f3447ac99d79cd324b403e4838d2
5
5
  SHA512:
6
- metadata.gz: cf94f686cf6d2659ab01f5dcce429255becf74cf2038979b283f41d2f1ea9fa486704e35d378abad9b26fdd8264f46d8fb9523923a0972ce2ba1647a17304e12
7
- data.tar.gz: a1fb24c2ae4f4fd0a663cb438c7418cb991f9291e1e855572ab3dacf02ebcf000f6809de5bfabe18faeda768675858466c1544fd0c61624b2e53c84a0cc98ef7
6
+ metadata.gz: a19fa80ca01c4a3fd95983a73e58cc09628a5f453ca49fc290cfa7238e9eb136bf8de25e8ae7f8f179d9c65ff24a3e18418c6d380e9db7f351f8903cfe8d839f
7
+ data.tar.gz: 1fe00c3cada2070f96d397920aa785b4ef70cc77856e0ac6818eae46f92281cfdf92d9153c3a7db978a2ad1b13919edf1f2b734331501379c133812437ba8242
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- # DbSchema [![Build Status](https://travis-ci.org/7even/db_schema.svg?branch=master)](https://travis-ci.org/7even/db_schema)
1
+ # DbSchema [![Build Status](https://travis-ci.org/7even/db_schema.svg?branch=master)](https://travis-ci.org/7even/db_schema) [![Gem Version](https://badge.fury.io/rb/db_schema.svg)](https://badge.fury.io/rb/db_schema)
2
2
 
3
3
  DbSchema is an opinionated database schema management tool that lets you maintain your DB schema with a single ruby file.
4
4
 
@@ -53,7 +53,7 @@ But you would lose it even with manual migrations.
53
53
  Add this line to your application's Gemfile:
54
54
 
55
55
  ``` ruby
56
- gem 'db_schema', '~> 0.1.1'
56
+ gem 'db_schema', '~> 0.1.2'
57
57
  ```
58
58
 
59
59
  And then execute:
@@ -356,6 +356,19 @@ end
356
356
 
357
357
  Be warned though that you have to specify the condition exactly as PostgreSQL outputs it in `psql` with `\d table_name` command; otherwise your index will be recreated on each DbSchema run. This will be fixed in a later DbSchema version.
358
358
 
359
+ If you need an index on expression you can use the same syntax replacing column names with SQL strings containing the expressions:
360
+
361
+ ``` ruby
362
+ db.table :users do |t|
363
+ t.timestamp :created_at
364
+ t.index 'date(created_at)'
365
+ end
366
+ ```
367
+
368
+ Expression indexes syntax allows specifying an order exactly like in a common index on table fields - just use a hash form like `t.index 'date(created_at)' => :desc`. You can also use an expression in a multiple index.
369
+
370
+ As with partial index condition (and all other SQL segments in `db_schema`), you must write the expression in a way `psql` outputs it, so instead of `lower(email)` you should use `lower(email::text)` (assuming that `email` is a varchar field).
371
+
359
372
  #### Foreign keys
360
373
 
361
374
  The `#foreign_key` method defines a foreign key. In it's minimal form it takes a referencing field name and referenced table name:
@@ -519,7 +532,7 @@ All configuration options are described in the following table:
519
532
 
520
533
  By default DbSchema logs the changes it applies to your database; you can disable that by setting `log_changes` to false.
521
534
 
522
- DbSchema provides an opt-out post-run schema check; it ensures that there are no remaining differences between your `schema.rb` and the actual database schema. If DbSchema still sees any differences it will keep applying them on each run - usually this is harmless (because it does not really change your schema) but in the case of a partial index with a complex condition it may rebuild the index which is an expensive operation on a large table. You can set `post_check` to false if you are 100% sure that your persistent changes are not a problem for you but I strongly recommend that you turn it on from time to time just to make sure nothing dangerous appears in these persistent changes.
535
+ DbSchema provides an opt-out post-run schema check; it ensures that there are no remaining differences between your `schema.rb` and the actual database schema. If DbSchema still sees any differences it will keep applying them on each run - usually this is harmless (because it does not really change your schema) but in the case of a partial index with a complex condition or an index on some expression it may rebuild the index which is an expensive operation on a large table. You can set `post_check` to false if you are 100% sure that your persistent changes are not a problem for you but I strongly recommend that you turn it on from time to time just to make sure nothing dangerous appears in these persistent changes.
523
536
 
524
537
  The `post_check` option is likely to become off by default when DbSchema becomes more stable and battle-tested, and when the partial index problem will be solved.
525
538
 
@@ -21,8 +21,8 @@ if defined?(AwesomePrint)
21
21
  :dbschema_field
22
22
  when ::DbSchema::Definitions::Index
23
23
  :dbschema_index
24
- when ::DbSchema::Definitions::Index::Field
25
- :dbschema_index_field
24
+ when ::DbSchema::Definitions::Index::Column
25
+ :dbschema_index_column
26
26
  when ::DbSchema::Definitions::CheckConstraint
27
27
  :dbschema_check_constraint
28
28
  when ::DbSchema::Definitions::ForeignKey
@@ -120,17 +120,17 @@ if defined?(AwesomePrint)
120
120
  end
121
121
 
122
122
  def awesome_dbschema_index(object)
123
- fields = format_dbschema_fields(object.fields)
123
+ columns = format_dbschema_fields(object.columns)
124
124
  using = ' using ' + colorize(object.type.to_s, :symbol) unless object.btree?
125
125
 
126
126
  data = [nil]
127
127
  data << colorize('unique', :nilclass) if object.unique?
128
128
  data << colorize('condition: ', :symbol) + object.condition.ai unless object.condition.nil?
129
129
 
130
- "#<#{object.class} #{object.name.ai} on #{fields}#{using}#{data.join(', ')}>"
130
+ "#<#{object.class} #{object.name.ai} on #{columns}#{using}#{data.join(', ')}>"
131
131
  end
132
132
 
133
- def awesome_dbschema_index_field(object)
133
+ def awesome_dbschema_index_column(object)
134
134
  data = [object.name.ai]
135
135
 
136
136
  if object.desc?
@@ -155,7 +155,7 @@ module DbSchema
155
155
  if desired && !actual
156
156
  table_changes << CreateIndex.new(
157
157
  name: index_name,
158
- fields: desired.fields,
158
+ columns: desired.columns,
159
159
  unique: desired.unique?,
160
160
  type: desired.type,
161
161
  condition: desired.condition
@@ -166,7 +166,7 @@ module DbSchema
166
166
  table_changes << DropIndex.new(index_name)
167
167
  table_changes << CreateIndex.new(
168
168
  name: index_name,
169
- fields: desired.fields,
169
+ columns: desired.columns,
170
170
  unique: desired.unique?,
171
171
  type: desired.type,
172
172
  condition: desired.condition
@@ -3,12 +3,12 @@ require 'dry/equalizer'
3
3
  module DbSchema
4
4
  module Definitions
5
5
  class Index
6
- include Dry::Equalizer(:name, :fields, :unique?, :type, :condition)
7
- attr_reader :name, :fields, :type, :condition
6
+ include Dry::Equalizer(:name, :columns, :unique?, :type, :condition)
7
+ attr_reader :name, :columns, :type, :condition
8
8
 
9
- def initialize(name:, fields:, unique: false, type: :btree, condition: nil)
9
+ def initialize(name:, columns:, unique: false, type: :btree, condition: nil)
10
10
  @name = name.to_sym
11
- @fields = fields
11
+ @columns = columns
12
12
  @unique = unique
13
13
  @type = type
14
14
  @condition = condition
@@ -22,7 +22,15 @@ module DbSchema
22
22
  type == :btree
23
23
  end
24
24
 
25
- class Field
25
+ def columns_to_sequel
26
+ if btree?
27
+ columns.map(&:ordered_expression)
28
+ else
29
+ columns.map(&:to_sequel)
30
+ end
31
+ end
32
+
33
+ class Column
26
34
  include Dry::Equalizer(:name, :order, :nulls)
27
35
  attr_reader :name, :order, :nulls
28
36
 
@@ -40,14 +48,42 @@ module DbSchema
40
48
  @order == :desc
41
49
  end
42
50
 
43
- def to_sequel
51
+ def ordered_expression
44
52
  if asc?
45
- Sequel.asc(name, nulls: nulls)
53
+ Sequel.asc(to_sequel, nulls: nulls)
46
54
  else
47
- Sequel.desc(name, nulls: nulls)
55
+ Sequel.desc(to_sequel, nulls: nulls)
48
56
  end
49
57
  end
50
58
  end
59
+
60
+ class TableField < Column
61
+ def expression?
62
+ false
63
+ end
64
+
65
+ def index_name_segment
66
+ name
67
+ end
68
+
69
+ def to_sequel
70
+ name
71
+ end
72
+ end
73
+
74
+ class Expression < Column
75
+ def expression?
76
+ true
77
+ end
78
+
79
+ def index_name_segment
80
+ name.scan(/\b[A-Za-z0-9_]+\b/).join('_')
81
+ end
82
+
83
+ def to_sequel
84
+ Sequel.lit("(#{name})")
85
+ end
86
+ end
51
87
  end
52
88
 
53
89
  class ForeignKey
data/lib/db_schema/dsl.rb CHANGED
@@ -55,11 +55,20 @@ module DbSchema
55
55
  fields << Definitions::Field.build(name, type, options)
56
56
  end
57
57
 
58
- def index(*fields, name: nil, unique: false, using: :btree, where: nil, **ordered_fields)
59
- index_fields = fields.map do |field_name|
60
- Definitions::Index::Field.new(field_name.to_sym)
61
- end + ordered_fields.map do |field_name, field_order_options|
62
- options = case field_order_options
58
+ def index(*columns, name: nil, unique: false, using: :btree, where: nil, **ordered_fields)
59
+ if columns.last.is_a?(Hash)
60
+ *ascending_columns, ordered_expressions = columns
61
+ else
62
+ ascending_columns = columns
63
+ ordered_expressions = {}
64
+ end
65
+
66
+ columns_data = ascending_columns.each_with_object({}) do |column_name, columns|
67
+ columns[column_name] = :asc
68
+ end.merge(ordered_fields).merge(ordered_expressions)
69
+
70
+ index_columns = columns_data.map do |column_name, column_order_options|
71
+ options = case column_order_options
63
72
  when :asc
64
73
  {}
65
74
  when :desc
@@ -72,14 +81,18 @@ module DbSchema
72
81
  raise ArgumentError, 'Only :asc, :desc, :asc_nulls_first and :desc_nulls_last options are supported.'
73
82
  end
74
83
 
75
- Definitions::Index::Field.new(field_name.to_sym, **options)
84
+ if column_name.is_a?(String)
85
+ Definitions::Index::Expression.new(column_name, **options)
86
+ else
87
+ Definitions::Index::TableField.new(column_name, **options)
88
+ end
76
89
  end
77
90
 
78
- index_name = name || "#{table_name}_#{index_fields.map(&:name).join('_')}_index"
91
+ index_name = name || "#{table_name}_#{index_columns.map(&:index_name_segment).join('_')}_index"
79
92
 
80
93
  indices << Definitions::Index.new(
81
94
  name: index_name,
82
- fields: index_fields,
95
+ columns: index_columns,
83
96
  unique: unique,
84
97
  type: using,
85
98
  condition: where
@@ -60,8 +60,8 @@ SELECT conname AS name,
60
60
  indisunique AS unique,
61
61
  indoption AS index_options,
62
62
  pg_get_expr(indpred, indrelid, true) AS condition,
63
- pg_get_expr(indexprs, indrelid, true) AS expression,
64
- amname AS index_type
63
+ amname AS index_type,
64
+ indexrelid AS index_oid
65
65
  FROM pg_class, pg_index
66
66
  LEFT JOIN pg_opclass
67
67
  ON pg_opclass.oid = ANY(pg_index.indclass::int[])
@@ -75,7 +75,16 @@ LEFT JOIN pg_am
75
75
  AND pg_class.oid = pg_index.indrelid
76
76
  AND indisprimary != 't'
77
77
  )
78
- GROUP BY name, column_positions, indisunique, index_options, condition, expression, index_type
78
+ GROUP BY name, column_positions, indisunique, index_options, condition, index_type, index_oid
79
+ SQL
80
+
81
+ EXPRESSION_INDICES_QUERY = <<-SQL.freeze
82
+ WITH index_ids AS (SELECT unnest(?) AS index_id),
83
+ elements AS (SELECT unnest(?) AS element)
84
+ SELECT index_id,
85
+ array_agg(pg_get_indexdef(index_id, element, 't')) AS definitions
86
+ FROM index_ids, elements
87
+ GROUP BY index_id;
79
88
  SQL
80
89
 
81
90
  ENUMS_QUERY = <<-SQL.freeze
@@ -139,14 +148,17 @@ SELECT extname
139
148
 
140
149
  def indices_data_for(table_name)
141
150
  column_names = DbSchema.connection[COLUMN_NAMES_QUERY, table_name.to_s].reduce({}) do |names, column|
142
- names.merge(column[:pos] => column[:name])
151
+ names.merge(column[:pos] => column[:name].to_sym)
143
152
  end
144
153
 
145
- DbSchema.connection[INDICES_QUERY, table_name.to_s].map do |index|
154
+ indices_data = DbSchema.connection[INDICES_QUERY, table_name.to_s].to_a
155
+ expressions_data = index_expressions_data(indices_data)
156
+
157
+ indices_data.map do |index|
146
158
  positions = index[:column_positions].split(' ').map(&:to_i)
147
159
  options = index[:index_options].split(' ').map(&:to_i)
148
160
 
149
- fields = column_names.values_at(*positions).zip(options).map do |column_name, column_order_options|
161
+ columns = positions.zip(options).map do |column_position, column_order_options|
150
162
  options = case column_order_options
151
163
  when 0
152
164
  {}
@@ -158,12 +170,17 @@ SELECT extname
158
170
  { order: :desc, nulls: :last }
159
171
  end
160
172
 
161
- DbSchema::Definitions::Index::Field.new(column_name.to_sym, **options)
173
+ if column_position.zero?
174
+ expression = expressions_data.fetch(index[:index_oid]).shift
175
+ DbSchema::Definitions::Index::Expression.new(expression, **options)
176
+ else
177
+ DbSchema::Definitions::Index::TableField.new(column_names.fetch(column_position), **options)
178
+ end
162
179
  end
163
180
 
164
181
  {
165
182
  name: index[:name].to_sym,
166
- fields: fields,
183
+ columns: columns,
167
184
  unique: index[:unique],
168
185
  type: index[:index_type].to_sym,
169
186
  condition: index[:condition]
@@ -172,6 +189,29 @@ SELECT extname
172
189
  end
173
190
 
174
191
  private
192
+ def index_expressions_data(indices_data)
193
+ expressions_stats = indices_data.each_with_object(ids: [], max: 0) do |index_data, stats|
194
+ expressions_count = index_data[:column_positions].split(' ').count('0')
195
+
196
+ if expressions_count > 0
197
+ stats[:ids] << index_data[:index_oid]
198
+ stats[:max] = [stats[:max], expressions_count].max
199
+ end
200
+ end
201
+
202
+ if expressions_stats[:max] > 0
203
+ DbSchema.connection[
204
+ EXPRESSION_INDICES_QUERY,
205
+ Sequel.pg_array(expressions_stats[:ids]),
206
+ Sequel.pg_array((1..expressions_stats[:max]).to_a)
207
+ ].each_with_object({}) do |index_data, indices_data|
208
+ indices_data[index_data[:index_id]] = index_data[:definitions]
209
+ end
210
+ else
211
+ {}
212
+ end
213
+ end
214
+
175
215
  def build_field(data, primary_key: false)
176
216
  type = data[:type].to_sym.downcase
177
217
  if type == :'user-defined'
@@ -70,14 +70,8 @@ module DbSchema
70
70
  end
71
71
 
72
72
  change.indices.each do |index|
73
- fields = if index.btree?
74
- index.fields.map(&:to_sequel)
75
- else
76
- index.fields.map(&:name)
77
- end
78
-
79
73
  index(
80
- fields,
74
+ index.columns_to_sequel,
81
75
  name: index.name,
82
76
  unique: index.unique?,
83
77
  type: index.type,
@@ -141,14 +135,8 @@ module DbSchema
141
135
  when Changes::AlterColumnDefault
142
136
  set_column_default(element.name, element.new_default)
143
137
  when Changes::CreateIndex
144
- fields = if element.btree?
145
- element.fields.map(&:to_sequel)
146
- else
147
- element.fields.map(&:name)
148
- end
149
-
150
138
  add_index(
151
- fields,
139
+ element.columns_to_sequel,
152
140
  name: element.name,
153
141
  unique: element.unique?,
154
142
  type: element.type,
@@ -24,7 +24,7 @@ module DbSchema
24
24
  field_names = table.fields.map(&:name)
25
25
 
26
26
  table.indices.each do |index|
27
- index.fields.map(&:name).each do |field_name|
27
+ index.columns.reject(&:expression?).map(&:name).each do |field_name|
28
28
  unless field_names.include?(field_name)
29
29
  error_message = %(Index "#{index.name}" refers to a missing field "#{table.name}.#{field_name}")
30
30
  errors << error_message
@@ -1,3 +1,3 @@
1
1
  module DbSchema
2
- VERSION = '0.1.1'
2
+ VERSION = '0.1.2'
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: db_schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vsevolod Romashov
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-09-24 00:00:00.000000000 Z
11
+ date: 2016-10-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sequel