db_schema 0.1.1 → 0.1.2

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
  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