activerecord-fb-adapter 0.9.1 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # ActiveRecord Firebird Adapter
2
2
  [![Gem Version](https://badge.fury.io/rb/activerecord-fb-adapter.svg)](http://badge.fury.io/rb/activerecord-fb-adapter)
3
+ [![Build Status](https://travis-ci.org/rowland/activerecord-fb-adapter.png?branch=master)](https://travis-ci.org/rowland/activerecord-fb-adapter)
3
4
 
4
5
  <img src="/project_logo.png" align="left" hspace="10">
5
6
  This is the ActiveRecord adapter for working with the [Firebird SQL Server](http://firebirdsql.org/). It currently supports Rails 3.2.x and 4.x. Although this adapter may not yet have feature parity with the 1st tier databases supported by Rails, it has been used in production by different people for several months without issues and may be considered stable. It uses under the hood the [Ruby Firebird Extension Library](https://github.com/rowland/fb).
@@ -30,10 +31,10 @@ gem 'activerecord-fb-adapter'
30
31
  Then run:
31
32
 
32
33
  ```
33
- bundle update
34
+ bundle install
34
35
  ```
35
36
 
36
- which will make bundler to get the gem with it's only dependency: the Fb gem which is "native" (has C code) and will be compiled the first time. Be sure you have a Firebird installation with access to the "ibase.h" file for this to succeed.
37
+ Bundler will install the gem and it's dependency, Fb, which is "native" (has C code) and will be compiled the first time. Be sure you have a Firebird installation with access to the "ibase.h" file for this to succeed.
37
38
 
38
39
  4) Edit the **database.yml** for configuring your database connection:
39
40
 
@@ -50,7 +51,7 @@ development:
50
51
 
51
52
  The default Firebird administrator username and password are **SYSDBA** and **masterkey**, you may have to adjust this to your installation.
52
53
 
53
- Currently the adapter does not supports the "rake db:create" task, so in order to create the database you must add the "create: true" option; with this switch the first time the adapter tries to connect to the database it will be created if it doesn't exists.
54
+ With the "create: true" option, a database will be created the first time the adapter tries to connect if it doesn't exist. Alternatively, if you're using Rails 4 or greater, you can use the "rake db:create" task.
54
55
 
55
56
  5) Start the rails server in development mode
56
57
 
@@ -8,16 +8,9 @@ module ActiveRecord
8
8
  exec_query(sql, name, binds).to_a.map(&:values)
9
9
  end
10
10
 
11
- # Executes the SQL statement in the context of this connection.
12
- def execute(sql, name = nil, skip_logging = false)
13
- translate(sql) do |translated, args|
14
- if (name == :skip_logging) || skip_logging
15
- @connection.execute(translated, *args)
16
- else
17
- log(sql, args, name) do
18
- @connection.execute(translated, *args)
19
- end
20
- end
11
+ def execute(sql, name = nil)
12
+ translate_and_log(sql, [], name) do |args|
13
+ @connection.execute(*args)
21
14
  end
22
15
  end
23
16
 
@@ -25,15 +18,13 @@ module ActiveRecord
25
18
  # +binds+ as the bind substitutes. +name+ is logged along with
26
19
  # the executed +sql+ statement.
27
20
  def exec_query(sql, name = 'SQL', binds = [])
28
- translate(sql, binds) do |translated, args|
29
- log(expand(translated, args), name) do
30
- result, rows = @connection.execute(translated, *args) do |cursor|
31
- [cursor.fields, cursor.fetchall]
32
- end
33
- next result unless result.respond_to?(:map)
34
- cols = result.map { |col| col.name }
35
- ActiveRecord::Result.new(cols, rows)
21
+ translate_and_log(sql, binds, name) do |args|
22
+ result, rows = @connection.execute(*args) do |cursor|
23
+ [cursor.fields, cursor.fetchall]
36
24
  end
25
+ next result unless result.respond_to?(:map)
26
+ cols = result.map { |col| col.name }
27
+ ActiveRecord::Result.new(cols, rows)
37
28
  end
38
29
  end
39
30
 
@@ -41,54 +32,38 @@ module ActiveRecord
41
32
  to_sql(arel, binds)
42
33
  end
43
34
 
44
- # Checks whether there is currently no transaction active. This is done
45
- # by querying the database driver, and does not use the transaction
46
- # house-keeping information recorded by #increment_open_transactions and
47
- # friends.
48
- #
49
- # Returns true if there is no transaction active, false if there is a
50
- # transaction active, and nil if this information is unknown.
51
- def outside_transaction?
52
- !@connection.transaction_started
53
- end
54
-
55
35
  # Begins the transaction (and turns off auto-committing).
56
36
  def begin_db_transaction
57
- @connection.transaction('READ COMMITTED')
37
+ log('begin transaction', nil) do
38
+ begin_isolated_db_transaction(default_transaction_isolation)
39
+ end
40
+ end
41
+
42
+ # Default isolation levels for transactions. This method exists
43
+ # in 4.0.2+, so it's here for backward compatibility with AR 3
44
+ def transaction_isolation_levels
45
+ {
46
+ read_committed: "READ COMMITTED",
47
+ repeatable_read: "REPEATABLE READ",
48
+ serializable: "SERIALIZABLE"
49
+ }
50
+ end
51
+
52
+ # Allows providing the :transaction option to ActiveRecord::Base.transaction
53
+ # in 4.0.2+. Can accept verbatim isolation options like 'WAIT READ COMMITTED'
54
+ def begin_isolated_db_transaction(isolation)
55
+ @connection.transaction transaction_isolation_levels.fetch(isolation, isolation)
58
56
  end
59
57
 
60
58
  # Commits the transaction (and turns on auto-committing).
61
59
  def commit_db_transaction
62
- @connection.commit
60
+ log('commit transaction', nil) { @connection.commit }
63
61
  end
64
62
 
65
63
  # Rolls back the transaction (and turns on auto-committing). Must be
66
64
  # done if the transaction block raises an exception or returns false.
67
65
  def rollback_db_transaction
68
- @connection.rollback
69
- end
70
-
71
- # Appends +LIMIT+ and +OFFSET+ options to an SQL statement, or some SQL
72
- # fragment that has the same semantics as LIMIT and OFFSET.
73
- #
74
- # +options+ must be a Hash which contains a +:limit+ option
75
- # and an +:offset+ option.
76
- #
77
- # This method *modifies* the +sql+ parameter.
78
- #
79
- # ===== Examples
80
- # add_limit_offset!('SELECT * FROM suppliers', {:limit => 10, :offset => 50})
81
- # generates
82
- # SELECT * FROM suppliers LIMIT 10 OFFSET 50
83
- def add_limit_offset!(sql, options) # :nodoc:
84
- if limit = options[:limit]
85
- if offset = options[:offset]
86
- sql << " ROWS #{offset.to_i + 1} TO #{offset.to_i + limit.to_i}"
87
- else
88
- sql << " ROWS #{limit.to_i}"
89
- end
90
- end
91
- sql
66
+ log('rollback transaction', nil) { @connection.rollback }
92
67
  end
93
68
 
94
69
  def default_sequence_name(table_name, _column = nil)
@@ -113,7 +88,7 @@ module ActiveRecord
113
88
  # column values as values. ActiveRecord >= 4 returns an ActiveRecord::Result.
114
89
  def select(sql, name = nil, binds = [])
115
90
  result = exec_query(sql, name, binds)
116
- ActiveRecord::VERSION::MAJOR > 3 ? result : result.to_a
91
+ ::ActiveRecord::VERSION::MAJOR > 3 ? result : result.to_a
117
92
  end
118
93
 
119
94
  # Since the ID is prefetched and passed to #insert, this method is useless.
@@ -121,6 +96,30 @@ module ActiveRecord
121
96
  def last_inserted_id(_result)
122
97
  nil
123
98
  end
99
+
100
+ private
101
+
102
+ def translate_and_log(sql, binds = [], name = nil)
103
+ if ActiveRecord::VERSION::STRING < "4.2.0"
104
+ values = binds.map { |b| type_cast(*b.reverse) }
105
+ else
106
+ values = []
107
+ end
108
+
109
+ if sql =~ /(CREATE TABLE|ALTER TABLE)/
110
+ sql.gsub!(/(\@BINDDATE|BINDDATE\@)/m, '\'')
111
+ else
112
+ sql.gsub!(/\@BINDBINARY(.*?)BINDBINARY\@/m) do |extract|
113
+ values << decode(extract[11...-11]) and '?'
114
+ end
115
+
116
+ sql.gsub!(/\@BINDDATE(.*?)BINDDATE\@/m) do |extract|
117
+ values << extract[9...-9] and '?'
118
+ end
119
+ end
120
+
121
+ log(sql, name, binds) { yield [sql, *values] }
122
+ end
124
123
  end
125
124
  end
126
125
  end
@@ -3,7 +3,6 @@ module ActiveRecord
3
3
  module Fb
4
4
  module Quoting
5
5
  def quote(value, column = nil)
6
- # records are quoted as their primary key
7
6
  return value.quoted_id if value.respond_to?(:quoted_id)
8
7
  type = column && column.type
9
8
 
@@ -11,81 +10,66 @@ module ActiveRecord
11
10
  when String, ActiveSupport::Multibyte::Chars
12
11
  value = value.to_s
13
12
  if [:integer, :float].include?(type)
14
- value = type == :integer ? value.to_i : value.to_f
15
- value.to_s
16
- elsif type && type != :binary && value.size < 256 && !value.include?('@')
17
- "'#{quote_string(value)}'"
18
- else
19
- "@#{Base64.encode64(value).chop}@"
20
- end
21
- when nil then "NULL"
22
- when true then quoted_true
23
- when false then quoted_false
24
- when Numeric, ActiveSupport::Duration then value.to_s
25
- # BigDecimals need to be output in a non-normalized form and quoted.
26
- when BigDecimal then value.to_s('F')
27
- when Symbol then "'#{quote_string(value.to_s)}'"
28
- when Class then "'#{value}'"
29
- else
30
- if value.acts_like?(:date)
31
- quote_date(value)
32
- elsif value.acts_like?(:time)
33
- quote_timestamp(value)
13
+ (type == :integer ? value.to_i : value.to_f).to_s
14
+ elsif type && type == :binary
15
+ "@BINDBINARY#{Base64.encode64(value.to_s)}BINDBINARY@"
34
16
  else
35
- quote_object(value)
17
+ "'#{quote_string(value)}'"
36
18
  end
37
- end
38
- end
39
-
40
- def quote_date(value)
41
- "@#{Base64.encode64(value.strftime('%Y-%m-%d')).chop}@"
42
- end
43
-
44
- def quote_timestamp(value)
45
- get = ActiveRecord::Base.default_timezone == :utc ? :getutc : :getlocal
46
- value = value.respond_to?(get) ? value.send(get) : value
47
- "@#{Base64.encode64(value.strftime('%Y-%m-%d %H:%M:%S')).chop}@"
48
- end
49
-
50
- def quote_string(string) # :nodoc:
51
- string.gsub(/'/, "''")
52
- end
53
-
54
- def quote_object(obj)
55
- if obj.respond_to?(:to_str)
56
- "@#{Base64.encode64(obj.to_str).chop}@"
19
+ when Date, Time
20
+ "@BINDDATE#{quoted_date(value)}BINDDATE@"
57
21
  else
58
- "@#{Base64.encode64(obj.to_yaml).chop}@"
22
+ super
59
23
  end
60
- end
24
+ end if ActiveRecord::VERSION::STRING < "4.2.0"
61
25
 
62
26
  def quote_column_name(column_name) # :nodoc:
63
- if @connection.dialect == 1
64
- %Q(#{ar_to_fb_case(column_name.to_s)})
65
- else
66
- %Q("#{ar_to_fb_case(column_name.to_s)}")
67
- end
27
+ name = ar_to_fb_case(column_name.to_s).gsub('"', '')
28
+ @connection.dialect == 1 ? %Q(#{name}) : %Q("#{name}")
68
29
  end
69
30
 
70
31
  def quote_table_name_for_assignment(_table, attr)
71
32
  quote_column_name(attr)
72
33
  end if ::ActiveRecord::VERSION::MAJOR >= 4
73
34
 
35
+ def unquoted_true
36
+ boolean_domain[:true]
37
+ end
38
+
74
39
  def quoted_true # :nodoc:
75
- quote(boolean_domain[:true])
40
+ quote unquoted_true
41
+ end
42
+
43
+ def unquoted_false
44
+ boolean_domain[:false]
76
45
  end
77
46
 
78
47
  def quoted_false # :nodoc:
79
- quote(boolean_domain[:false])
48
+ quote unquoted_false
80
49
  end
81
50
 
82
51
  def type_cast(value, column)
83
- return super unless value == true || value == false
84
- value ? quoted_true : quoted_false
52
+ if [true, false].include?(value)
53
+ value ? quoted_true : quoted_false
54
+ else
55
+ super
56
+ end
85
57
  end
86
58
 
87
59
  private
88
60
 
61
+ # Types that are bind parameters will not be quoted
62
+ def _quote(value)
63
+ case value
64
+ when Type::Binary::Data
65
+ "@BINDBINARY#{Base64.encode64(value.to_s)}BINDBINARY@"
66
+ when Date, Time
67
+ "@BINDDATE#{quoted_date(value)}BINDDATE@"
68
+ else
69
+ super
70
+ end
71
+ end
72
+
89
73
  # Maps uppercase Firebird column names to lowercase for ActiveRecord;
90
74
  # mixed-case columns retain their original case.
91
75
  def fb_to_ar_case(column_name)
@@ -97,6 +81,16 @@ module ActiveRecord
97
81
  def ar_to_fb_case(column_name)
98
82
  column_name =~ /[[:upper:]]/ ? column_name : column_name.upcase
99
83
  end
84
+
85
+ if defined? Encoding
86
+ def decode(s)
87
+ Base64.decode64(s).force_encoding(@connection.encoding)
88
+ end
89
+ else
90
+ def decode(s)
91
+ Base64.decode64(s)
92
+ end
93
+ end
100
94
  end
101
95
  end
102
96
  end
@@ -29,66 +29,69 @@ module ActiveRecord
29
29
  # Returns an array of indexes for the given table.
30
30
  def indexes(table_name, _name = nil)
31
31
  @connection.indexes.values.map { |ix|
32
- if ix.table_name == table_name && ix.index_name !~ /^rdb\$/
32
+ if ix.table_name == table_name.to_s && ix.index_name !~ /^rdb\$/
33
33
  IndexDefinition.new(table_name, ix.index_name, ix.unique, ix.columns)
34
34
  end
35
35
  }.compact
36
36
  end
37
37
 
38
38
  def primary_key(table_name) #:nodoc:
39
- sql = <<-END_SQL
39
+ row = @connection.query(<<-end_sql)
40
40
  SELECT s.rdb$field_name
41
41
  FROM rdb$indices i
42
42
  JOIN rdb$index_segments s ON i.rdb$index_name = s.rdb$index_name
43
43
  LEFT JOIN rdb$relation_constraints c ON i.rdb$index_name = c.rdb$index_name
44
- WHERE i.rdb$relation_name = '#{ar_to_fb_case(table_name)}' and c.rdb$constraint_type = 'PRIMARY KEY';
45
- END_SQL
46
- row = select_one(sql)
47
- row && fb_to_ar_case(row.values.first.rstrip)
44
+ WHERE i.rdb$relation_name = '#{ar_to_fb_case(table_name)}'
45
+ AND c.rdb$constraint_type = 'PRIMARY KEY';
46
+ end_sql
47
+
48
+ row.first && fb_to_ar_case(row.first[0].rstrip)
48
49
  end
49
50
 
50
51
  # Returns an array of Column objects for the table specified by +table_name+.
51
52
  # See the concrete implementation for details on the expected parameter values.
52
- def columns(table_name, name = nil)
53
- sql = <<-END_SQL
54
- SELECT r.rdb$field_name, r.rdb$field_source, f.rdb$field_type, f.rdb$field_sub_type,
55
- f.rdb$field_length, f.rdb$field_precision, f.rdb$field_scale,
56
- COALESCE(r.rdb$default_source, f.rdb$default_source) rdb$default_source,
57
- COALESCE(r.rdb$null_flag, f.rdb$null_flag) rdb$null_flag
58
- FROM rdb$relation_fields r
59
- JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
60
- WHERE r.rdb$relation_name = '#{ar_to_fb_case(table_name)}'
61
- ORDER BY r.rdb$field_position
62
- END_SQL
63
- select_rows(sql, name).map do |field|
64
- FbColumn.new(*field.map { |value|
65
- value.is_a?(String) ? value.rstrip : value
66
- })
53
+ def columns(table_name, _name = nil)
54
+ column_definitions(table_name).map do |field|
55
+ field.symbolize_keys!.each { |k, v| v.rstrip! if v.is_a?(String) }
56
+ properties = field.values_at(:name, :default_source)
57
+ properties += column_type_for(field)
58
+ properties << !field[:null_flag]
59
+ FbColumn.new(*properties, field.slice(:domain, :sub_type))
67
60
  end
68
61
  end
69
62
 
70
63
  def create_table(name, options = {}) # :nodoc:
64
+ if options.key? :temporary
65
+ fail ActiveRecordError, 'Firebird does not support temporary tables'
66
+ end
67
+
68
+ if options.key? :as
69
+ fail ActiveRecordError, 'Firebird does not support creating tables with a select'
70
+ end
71
+
71
72
  needs_sequence = options[:id] != false
72
73
  while_ensuring_boolean_domain do
73
- super(name, options) do |table_def|
74
+ super name, options do |table_def|
74
75
  yield table_def if block_given?
75
76
  needs_sequence ||= table_def.needs_sequence
76
77
  end
77
78
  end
79
+
78
80
  return if options[:sequence] == false || !needs_sequence
79
81
  create_sequence(options[:sequence] || default_sequence_name(name))
80
82
  end
81
83
 
82
- # Unfortunately, this is a limitation of Firebird.
83
84
  def rename_table(name, new_name)
84
- fail 'Firebird does not support renaming tables.'
85
+ fail ActiveRecordError, 'Firebird does not support renaming tables.'
85
86
  end
86
87
 
87
88
  def drop_table(name, options = {}) # :nodoc:
88
- super(name)
89
- return if options[:sequence] == false
90
- sequence_name = options[:sequence] || default_sequence_name(name)
91
- drop_sequence(sequence_name) if sequence_exists?(sequence_name)
89
+ unless options[:sequence] == false
90
+ sequence_name = options[:sequence] || default_sequence_name(name)
91
+ drop_sequence(sequence_name) if sequence_exists?(sequence_name)
92
+ end
93
+
94
+ super
92
95
  end
93
96
 
94
97
  # Creates a sequence
@@ -108,16 +111,29 @@ module ActiveRecord
108
111
  # Adds a new column to the named table.
109
112
  # See TableDefinition#column for details of the options you can use.
110
113
  def add_column(table_name, column_name, type, options = {})
111
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
112
- add_column_options!(add_column_sql, options)
113
- while_ensuring_boolean_domain { execute(add_column_sql) }
114
+ while_ensuring_boolean_domain { super }
115
+
114
116
  if type == :primary_key && options[:sequence] != false
115
117
  create_sequence(options[:sequence] || default_sequence_name(table_name))
116
118
  end
119
+
117
120
  return unless options[:position]
118
121
  # position is 1-based but add 1 to skip id column
119
- alter_position_sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} POSITION #{options[:position] + 1}"
120
- execute(alter_position_sql)
122
+ execute(squish_sql(<<-end_sql))
123
+ ALTER TABLE #{quote_table_name(table_name)}
124
+ ALTER COLUMN #{quote_column_name(column_name)}
125
+ POSITION #{options[:position] + 1}
126
+ end_sql
127
+ end
128
+
129
+ def remove_column(table_name, column_name, type = nil, options = {})
130
+ indexes(table_name).each do |i|
131
+ if i.columns.any? { |c| c == column_name.to_s }
132
+ remove_index! i.table, i.name
133
+ end
134
+ end
135
+
136
+ super
121
137
  end
122
138
 
123
139
  # Changes the column's definition according to the new options.
@@ -126,10 +142,15 @@ module ActiveRecord
126
142
  # change_column(:suppliers, :name, :string, :limit => 80)
127
143
  # change_column(:accounts, :description, :text)
128
144
  def change_column(table_name, column_name, type, options = {})
129
- sql = "ALTER TABLE #{quote_table_name(table_name)} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
130
- execute(sql)
145
+ type_sql = type_to_sql(type, *options.values_at(:limit, :precision, :scale))
146
+
147
+ execute(squish_sql(<<-end_sql))
148
+ ALTER TABLE #{quote_table_name(table_name)}
149
+ ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_sql}
150
+ end_sql
151
+
131
152
  change_column_null(table_name, column_name, !!options[:null]) if options.key?(:null)
132
- change_column_default(table_name, column_name, options[:default]) if options[:default]
153
+ change_column_default(table_name, column_name, options[:default]) if options.key?(:default)
133
154
  end
134
155
 
135
156
  # Sets a new default value for a column. If you want to set the default
@@ -139,23 +160,39 @@ module ActiveRecord
139
160
  # change_column_default(:suppliers, :qualification, 'new')
140
161
  # change_column_default(:accounts, :authorized, 1)
141
162
  def change_column_default(table_name, column_name, default)
142
- execute("ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} SET DEFAULT #{quote(default)}")
163
+ execute(squish_sql(<<-end_sql))
164
+ ALTER TABLE #{quote_table_name(table_name)}
165
+ ALTER #{quote_column_name(column_name)}
166
+ SET DEFAULT #{quote(default)}
167
+ end_sql
143
168
  end
144
169
 
145
170
  def change_column_null(table_name, column_name, null, default = nil)
146
- fail 'Firebird cannot change the nullability of a column using ALTER COLUMN.'
171
+ change_column_default(table_name, column_name, default) if default
172
+
173
+ execute(squish_sql(<<-end_sql))
174
+ UPDATE RDB$RELATION_FIELDS
175
+ SET RDB$NULL_FLAG=#{quote(null ? nil : 1)}
176
+ WHERE RDB$FIELD_NAME='#{ar_to_fb_case(column_name)}'
177
+ AND RDB$RELATION_NAME='#{ar_to_fb_case(table_name)}'
178
+ end_sql
147
179
  end
148
180
 
149
181
  # Renames a column.
150
182
  # ===== Example
151
183
  # rename_column(:suppliers, :description, :name)
152
184
  def rename_column(table_name, column_name, new_column_name)
153
- execute "ALTER TABLE #{quote_table_name(table_name)} ALTER #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
185
+ execute(squish_sql(<<-end_sql))
186
+ ALTER TABLE #{quote_table_name(table_name)}
187
+ ALTER #{quote_column_name(column_name)}
188
+ TO #{quote_column_name(new_column_name)}
189
+ end_sql
190
+
154
191
  rename_column_indexes(table_name, column_name, new_column_name)
155
192
  end
156
193
 
157
194
  def remove_index!(_table_name, index_name) #:nodoc:
158
- execute("DROP INDEX #{quote_column_name(index_name)}")
195
+ execute "DROP INDEX #{quote_column_name(index_name)}"
159
196
  end
160
197
 
161
198
  def index_name(table_name, options) #:nodoc:
@@ -175,23 +212,44 @@ module ActiveRecord
175
212
  def type_to_sql(type, limit = nil, precision = nil, scale = nil)
176
213
  case type
177
214
  when :integer then integer_to_sql(limit)
178
- when :float then float_to_sql(limit)
215
+ when :float then float_to_sql(limit)
179
216
  else super
180
217
  end
181
218
  end
182
219
 
183
- # Deprecated in Rails 4.1. Backports functionality.
184
- def add_column_options!(sql, options)
185
- if options_include_default?(options)
186
- sql << " DEFAULT #{quote(options[:default], options[:column])}"
187
- end
188
- # must explicitly check for :null to allow change_column to work on migrations
189
- sql << ' NOT NULL' if options[:null] == false
190
- end if ActiveRecord::VERSION::MAJOR > 3
191
-
192
220
  private
193
221
 
194
- if ActiveRecord::VERSION::MAJOR > 3
222
+ def column_definitions(table_name)
223
+ exec_query(squish_sql(<<-end_sql), 'SCHEMA')
224
+ SELECT
225
+ r.rdb$field_name name,
226
+ r.rdb$field_source domain,
227
+ f.rdb$field_type type,
228
+ f.rdb$field_sub_type sub_type,
229
+ f.rdb$field_length "limit",
230
+ f.rdb$field_precision "precision",
231
+ f.rdb$field_scale "scale",
232
+ COALESCE(r.rdb$default_source, f.rdb$default_source) default_source,
233
+ COALESCE(r.rdb$null_flag, f.rdb$null_flag) null_flag
234
+ FROM rdb$relation_fields r
235
+ JOIN rdb$fields f ON r.rdb$field_source = f.rdb$field_name
236
+ WHERE r.rdb$relation_name = '#{ar_to_fb_case(table_name)}'
237
+ ORDER BY r.rdb$field_position
238
+ end_sql
239
+ end
240
+
241
+ # We need to be very precise about our sql types.
242
+ def column_type_for(field)
243
+ sql_type = FbColumn.sql_type_for(field)
244
+
245
+ if ActiveRecord::VERSION::STRING < "4.2.0"
246
+ [sql_type]
247
+ else
248
+ [lookup_cast_type(sql_type), sql_type]
249
+ end
250
+ end
251
+
252
+ if ::ActiveRecord::VERSION::MAJOR > 3
195
253
  def create_table_definition(*args)
196
254
  TableDefinition.new(native_database_types, *args)
197
255
  end
@@ -209,33 +267,33 @@ module ActiveRecord
209
267
  when 3..4 then 'integer'
210
268
  when 5..8 then 'bigint'
211
269
  else
212
- fail ActiveRecordError, "No integer type has byte size #{limit}. Use a NUMERIC with PRECISION 0 instead."
270
+ fail ActiveRecordError, "No integer type has byte size #{limit}. "\
271
+ "Use a NUMERIC with PRECISION 0 instead."
213
272
  end
214
273
  end
215
274
 
216
275
  def float_to_sql(limit)
217
- if limit.nil? || limit <= 4
218
- 'float'
219
- else
220
- 'double precision'
221
- end
276
+ (limit.nil? || limit <= 4) ? 'float' : 'double precision'
222
277
  end
223
278
 
224
279
  # Creates a domain for boolean fields as needed
225
280
  def while_ensuring_boolean_domain(&block)
226
281
  block.call
227
- rescue ActiveRecord::StatementInvalid => e
282
+ rescue ActiveRecordError => e
228
283
  raise unless e.message =~ /Specified domain or source column \w+ does not exist/
229
284
  create_boolean_domain
230
285
  block.call
231
286
  end
232
287
 
288
+ def squish_sql(sql)
289
+ sql.strip.gsub(/\s+/, ' ')
290
+ end
291
+
233
292
  def create_boolean_domain
234
- sql = <<-end_sql
293
+ execute(squish_sql(<<-end_sql))
235
294
  CREATE DOMAIN #{boolean_domain[:name]} AS #{boolean_domain[:type]}
236
295
  CHECK (VALUE IN (#{quoted_true}, #{quoted_false}) OR VALUE IS NULL)
237
296
  end_sql
238
- execute(sql)
239
297
  end
240
298
 
241
299
  def sequence_exists?(sequence_name)
@@ -1,10 +1,18 @@
1
+
1
2
  # Rails 3 & 4 specific database adapter for Firebird (http://firebirdsql.org)
2
3
  # Author: Brent Rowland <rowland@rowlandresearch.com>
3
4
  # Based originally on FireRuby extension by Ken Kunz <kennethkunz@gmail.com>
4
5
 
6
+ require 'fb'
5
7
  require 'base64'
6
8
  require 'arel'
7
- require 'arel/visitors/fb'
9
+
10
+ if Arel::VERSION < "6.0.0"
11
+ require 'arel/visitors/fb'
12
+ else
13
+ require 'arel/visitors/fb_collector'
14
+ end
15
+
8
16
  require 'arel/visitors/bind_visitor'
9
17
  require 'active_record'
10
18
  require 'active_record/base'
@@ -125,11 +133,19 @@ module ActiveRecord
125
133
  include Fb::SchemaStatements
126
134
 
127
135
  @@boolean_domain = { :true => 1, :false => 0, :name => 'BOOLEAN', :type => 'integer' }
128
- cattr_accessor :boolean_domain
136
+ cattr_reader :boolean_domain
137
+
138
+ def self.boolean_domain=(domain)
139
+ FbColumn::TRUE_VALUES << domain[:true]
140
+ @@boolean_domain = domain
141
+ end
142
+
143
+ @@default_transaction_isolation = :read_committed
144
+ cattr_accessor :default_transaction_isolation
129
145
 
130
146
  class BindSubstitution < Arel::Visitors::Fb # :nodoc:
131
147
  include Arel::Visitors::BindVisitor
132
- end
148
+ end if ActiveRecord::VERSION::STRING < "4.2.0"
133
149
 
134
150
  def initialize(connection, logger, config=nil)
135
151
  super(connection, logger)
@@ -137,10 +153,6 @@ module ActiveRecord
137
153
  @visitor = Arel::Visitors::Fb.new(self)
138
154
  end
139
155
 
140
- def self.visitor_for(pool) # :nodoc:
141
- Arel::Visitors::Fb.new(pool)
142
- end
143
-
144
156
  # Returns the human-readable name of the adapter. Use mixed case - one
145
157
  # can always use downcase if needed.
146
158
  def adapter_name
@@ -170,7 +182,11 @@ module ActiveRecord
170
182
  # CREATE TABLE or ALTER TABLE get rolled back by a transaction? PostgreSQL,
171
183
  # SQL Server, and others support this. MySQL and others do not.
172
184
  def supports_ddl_transactions?
173
- false
185
+ true
186
+ end
187
+
188
+ def supports_transaction_isolation?
189
+ true
174
190
  end
175
191
 
176
192
  # Does this adapter support savepoints? FirebirdSQL does
@@ -249,31 +265,11 @@ module ActiveRecord
249
265
 
250
266
  protected
251
267
 
252
- if defined?(Encoding)
253
- def decode(s)
254
- Base64.decode64(s).force_encoding(@connection.encoding)
255
- end
256
- else
257
- def decode(s)
258
- Base64.decode64(s)
259
- end
260
- end
261
-
262
- def translate(sql, binds = [])
263
- sql.gsub!(/\sIN\s+\([^\)]*\)/mi) do |m|
264
- m.gsub(/\(([^\)]*)\)/m) do |n|
265
- n.gsub(/\@(.*?)\@/m) do |o|
266
- "'#{quote_string(decode(o[1..-1]))}'"
267
- end
268
- end
269
- end
270
- args = binds.map { |col, val| type_cast(val, col) }
271
- sql.gsub!(/\@(.*?)\@/m) { |m| args << decode(m[1..-1]); '?' }
272
- yield(sql, args) if block_given?
273
- end
274
-
275
- def expand(sql, args)
276
- ([sql] + args) * ', '
268
+ # Maps SQL types to ActiveRecord 4.2+ type objects
269
+ def initialize_type_map(m)
270
+ super
271
+ m.register_type %r(timestamp)i, Type::DateTime.new
272
+ m.alias_type %r(blob sub_type text)i, 'text'
277
273
  end
278
274
 
279
275
  def translate_exception(e, message)
@@ -282,6 +278,8 @@ module ActiveRecord
282
278
  InvalidForeignKey.new(message, e)
283
279
  when /violation of PRIMARY or UNIQUE KEY constraint/, /attempt to store duplicate value/
284
280
  RecordNotUnique.new(message, e)
281
+ when /This operation is not defined for system tables/
282
+ ActiveRecordError.new(message)
285
283
  else
286
284
  super
287
285
  end
@@ -1,72 +1,56 @@
1
1
  module ActiveRecord
2
2
  module ConnectionAdapters
3
3
  class FbColumn < Column # :nodoc:
4
- def initialize(name, domain, type, sub_type, length, precision, scale, default_source, null_flag)
5
- @firebird_type = ::Fb::SqlType.from_code(type, sub_type || 0)
6
- super(name.downcase, nil, @firebird_type, !null_flag)
7
- @default = parse_default(default_source) if default_source
8
- case @firebird_type
9
- when 'VARCHAR', 'CHAR'
10
- @limit = length
11
- when 'DECIMAL', 'NUMERIC'
12
- @precision, @scale = precision, scale.abs
13
- end
14
- @domain, @sub_type = domain, sub_type
15
- end
4
+ class << self
5
+ delegate :boolean_domain, to: 'ActiveRecord::ConnectionAdapters::FbAdapter'
16
6
 
17
- def type
18
- if @domain =~ /BOOLEAN/
19
- :boolean
20
- elsif @type == :binary and @sub_type == 1
21
- :text
22
- else
23
- @type
24
- end
25
- end
7
+ # When detecting types, ActiveRecord expects strings in a certain format.
8
+ # In 4.2, these strings are converted to ActiveRecord::Type::Value objects
9
+ # using the type_map (see #initialize_type_map). Prior to 4.2, the sql_type
10
+ # could be coerced to a certain ActiveRecord type in Column#simplified_type.
11
+ def sql_type_for(field)
12
+ type, sub_type, domain = field.values_at(:type, :sub_type, :domain)
13
+ sql_type = ::Fb::SqlType.from_code(type, sub_type || 0).downcase
26
14
 
27
- # Submits a _CAST_ query to the database, casting the default value to the specified SQL type.
28
- # This enables Firebird to provide an actual value when context variables are used as column
29
- # defaults (such as CURRENT_TIMESTAMP).
30
- def default
31
- if @default
32
- sql = "SELECT CAST(#{@default} AS #{column_def}) FROM RDB$DATABASE"
33
- connection = ActiveRecord::Base.connection
34
- if connection
35
- value = connection.raw_connection.query(:hash, sql)[0]['cast']
36
- return nil if value.acts_like?(:date) || value.acts_like?(:time)
37
- type_cast(value)
38
- else
39
- raise ConnectionNotEstablished, "No Firebird connections established."
15
+ case sql_type
16
+ when /(numeric|decimal)/
17
+ sql_type << "(#{field[:precision]},#{field[:scale].abs})"
18
+ when /(int|float|double|char|varchar)/
19
+ sql_type << "(#{field[:limit]})"
40
20
  end
21
+
22
+ sql_type << ' sub_type text' if sql_type =~ /blob/ && sub_type == 1
23
+ sql_type = 'boolean' if domain =~ %r(#{boolean_domain[:name]})i
24
+ sql_type
41
25
  end
42
26
  end
43
27
 
44
- def self.value_to_boolean(value)
45
- %W(#{FbAdapter.boolean_domain[:true]} true t 1).include? value.to_s.downcase
28
+ attr_reader :sub_type, :domain
29
+
30
+ if ActiveRecord::VERSION::STRING < "4.2.0"
31
+ def initialize(name, default, sql_type = nil, null = true, fb_options = {})
32
+ @domain, @sub_type = fb_options.values_at(:domain, :sub_type)
33
+ super(name.downcase, parse_default(default), sql_type, null)
34
+ end
35
+ else
36
+ def initialize(name, default, cast_type, sql_type = nil, null = true, fb_options = {})
37
+ @domain, @sub_type = fb_options.values_at(:domain, :sub_type)
38
+ super(name.downcase, parse_default(default), cast_type, sql_type, null)
39
+ end
46
40
  end
47
41
 
48
42
  private
49
43
 
50
- def parse_default(default_source)
51
- default_source =~ /^\s*DEFAULT\s+(.*)\s*$/i
52
- return $1 unless $1.upcase == "NULL"
53
- end
54
-
55
- def column_def
56
- case @firebird_type
57
- when 'CHAR', 'VARCHAR' then "#{@firebird_type}(#{@limit})"
58
- when 'NUMERIC', 'DECIMAL' then "#{@firebird_type}(#{@precision},#{@scale.abs})"
59
- #when 'DOUBLE' then "DOUBLE PRECISION"
60
- else @firebird_type
61
- end
44
+ def parse_default(default)
45
+ return if default.nil? || default =~ /null/i
46
+ default.gsub(/^\s*DEFAULT\s+/i, '').gsub(/(^'|'$)/, '')
62
47
  end
63
48
 
49
+ # Type conversion prior to 4.2
64
50
  def simplified_type(field_type)
65
- if field_type == 'TIMESTAMP'
66
- :datetime
67
- else
68
- super
69
- end
51
+ return :datetime if field_type =~ /timestamp/
52
+ return :text if field_type =~ /blob sub_type text/
53
+ super
70
54
  end
71
55
  end
72
56
  end
@@ -18,7 +18,7 @@ module ActiveRecord
18
18
  end
19
19
 
20
20
  def self.fb_connection_config(config)
21
- config = config.symbolize_keys.reverse_merge(:downcase_names => true, :port => 3050)
21
+ config = config.symbolize_keys.dup.reverse_merge(:downcase_names => true, :port => 3050)
22
22
  fail ArgumentError, 'No database specified. Missing argument: database.' if !config[:database]
23
23
  if config[:host].nil? || config[:host] =~ /localhost/i
24
24
  config[:database] = File.expand_path(config[:database], defined?(Rails) && Rails.root)
@@ -0,0 +1,75 @@
1
+ module ActiveRecord
2
+ module Tasks # :nodoc:
3
+ class FbDatabaseTasks # :nodoc:
4
+ delegate :fb_connection_config, :establish_connection, to: ::ActiveRecord::Base
5
+
6
+ def initialize(configuration, root = ::ActiveRecord::Tasks::DatabaseTasks.root)
7
+ @root, @configuration = root, fb_connection_config(configuration)
8
+ end
9
+
10
+ def create
11
+ fb_database.create
12
+ establish_connection configuration
13
+ rescue ::Fb::Error => e
14
+ raise unless e.message.include?('database or file exists')
15
+ raise DatabaseAlreadyExists
16
+ end
17
+
18
+ def drop
19
+ fb_database.drop
20
+ rescue ::Fb::Error => e
21
+ raise ::ActiveRecord::ConnectionNotEstablished, e.message
22
+ end
23
+
24
+ def purge
25
+ drop
26
+ create
27
+ end
28
+
29
+ def structure_dump(filename)
30
+ isql :extract, output: filename
31
+ end
32
+
33
+ def structure_load(filename)
34
+ isql input: filename
35
+ end
36
+
37
+ private
38
+
39
+ def fb_database
40
+ ::Fb::Database.new(configuration)
41
+ end
42
+
43
+ # Executes isql commands to load/dump the schema.
44
+ # The generated command might look like this:
45
+ # isql db/development.fdb -user SYSDBA -password masterkey -extract
46
+ def isql(*args)
47
+ opts = args.extract_options!
48
+ user, pass = configuration.values_at(:username, :password)
49
+ user ||= configuration[:user]
50
+ opts.reverse_merge!(user: user, password: pass)
51
+ cmd = [isql_executable, configuration[:database]]
52
+ cmd += opts.map { |name, val| "-#{name} #{val}" }
53
+ cmd += args.map { |flag| "-#{flag}" }
54
+ cmd = cmd.join(' ')
55
+ raise "Error running: #{cmd}" unless Kernel.system(cmd)
56
+ end
57
+
58
+ # Finds the isql command line utility from the PATH
59
+ # Many linux distros call this program isql-fb, instead of isql
60
+ def isql_executable
61
+ require 'mkmf'
62
+ exe = ['isql-fb', 'isql'].detect { |c| find_executable0(c) }
63
+ exe || abort("Unable to find isql or isql-fb in your $PATH")
64
+ end
65
+
66
+ def configuration
67
+ @configuration
68
+ end
69
+
70
+ def root
71
+ @root
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,14 @@
1
+ require 'active_record/connection_adapters/fb_adapter'
2
+
3
+ module ActiveRecordFbAdapter
4
+
5
+ if defined?(::Rails::Railtie) && ::ActiveRecord::VERSION::MAJOR > 3
6
+ class Railtie < ::Rails::Railtie
7
+ rake_tasks do
8
+ load 'active_record/tasks/fb_database_tasks.rb'
9
+ ActiveRecord::Tasks::DatabaseTasks.register_task /fb/, ActiveRecord::Tasks::FbDatabaseTasks
10
+ end
11
+ end
12
+ end
13
+
14
+ end
@@ -0,0 +1,96 @@
1
+ module Arel
2
+ module Visitors
3
+ class Fb < Arel::Visitors::ToSql
4
+ private
5
+
6
+ def visit_Arel_Nodes_SelectStatement o, collector
7
+ collector << "SELECT "
8
+ collector = visit o.offset, collector if o.offset && !o.limit
9
+
10
+ collector = o.cores.inject(collector) { |c,x|
11
+ visit_Arel_Nodes_SelectCore(x, c)
12
+ }
13
+
14
+ unless o.orders.empty?
15
+ collector << ORDER_BY
16
+ len = o.orders.length - 1
17
+ o.orders.each_with_index { |x, i|
18
+ collector = visit(x, collector)
19
+ collector << COMMA unless len == i
20
+ }
21
+ end
22
+
23
+ if o.limit && o.offset
24
+ collector = limit_with_rows o, collector
25
+ elsif o.limit && !o.offset
26
+ collector = visit o.limit, collector
27
+ end
28
+
29
+ collector = maybe_visit o.lock, collector
30
+ collector
31
+ end
32
+
33
+ def visit_Arel_Nodes_SelectCore o, collector
34
+ if o.set_quantifier
35
+ collector = visit o.set_quantifier, collector
36
+ collector << SPACE
37
+ end
38
+
39
+ unless o.projections.empty?
40
+ len = o.projections.length - 1
41
+ o.projections.each_with_index do |x, i|
42
+ collector = visit(x, collector)
43
+ collector << COMMA unless len == i
44
+ end
45
+ end
46
+
47
+ if o.source && !o.source.empty?
48
+ collector << " FROM "
49
+ collector = visit o.source, collector
50
+ end
51
+
52
+ unless o.wheres.empty?
53
+ collector << WHERE
54
+ len = o.wheres.length - 1
55
+ o.wheres.each_with_index do |x, i|
56
+ collector = visit(x, collector)
57
+ collector << AND unless len == i
58
+ end
59
+ end
60
+
61
+ unless o.groups.empty?
62
+ collector << GROUP_BY
63
+ len = o.groups.length - 1
64
+ o.groups.each_with_index do |x, i|
65
+ collector = visit(x, collector)
66
+ collector << COMMA unless len == i
67
+ end
68
+ end
69
+
70
+ collector = maybe_visit o.having, collector
71
+ collector
72
+ end
73
+
74
+ def visit_Arel_Nodes_Limit o, collector
75
+ collector << " ROWS "
76
+ visit o.expr, collector
77
+ end
78
+
79
+ def visit_Arel_Nodes_Offset o, collector
80
+ collector << " SKIP "
81
+ visit o.expr, collector
82
+ collector << SPACE
83
+ end
84
+
85
+ # Firebird helper
86
+ def limit_with_rows o, collector
87
+ collector << " ROWS "
88
+ visit o.offset.expr + 1, collector
89
+ collector << " TO "
90
+ visit o.offset.expr + o.limit.expr.expr, collector
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ Arel::Visitors::VISITORS['fb'] = Arel::Visitors::Fb
metadata CHANGED
@@ -1,7 +1,8 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: activerecord-fb-adapter
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.9.1
4
+ version: 1.0.0
5
+ prerelease:
5
6
  platform: ruby
6
7
  authors:
7
8
  - Brent Rowland
@@ -13,31 +14,99 @@ dependencies:
13
14
  - !ruby/object:Gem::Dependency
14
15
  name: fb
15
16
  requirement: !ruby/object:Gem::Requirement
17
+ none: false
16
18
  requirements:
17
- - - ">="
19
+ - - ! '>='
18
20
  - !ruby/object:Gem::Version
19
21
  version: 0.7.4
20
22
  type: :runtime
21
23
  prerelease: false
22
24
  version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
23
26
  requirements:
24
- - - ">="
27
+ - - ! '>='
25
28
  - !ruby/object:Gem::Version
26
29
  version: 0.7.4
27
30
  - !ruby/object:Gem::Dependency
28
31
  name: activerecord
29
32
  requirement: !ruby/object:Gem::Requirement
33
+ none: false
30
34
  requirements:
31
- - - ">="
35
+ - - ! '>='
32
36
  - !ruby/object:Gem::Version
33
37
  version: 3.2.0
34
38
  type: :runtime
35
39
  prerelease: false
36
40
  version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
37
42
  requirements:
38
- - - ">="
43
+ - - ! '>='
39
44
  - !ruby/object:Gem::Version
40
45
  version: 3.2.0
46
+ - !ruby/object:Gem::Dependency
47
+ name: mocha
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ! '>='
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ - !ruby/object:Gem::Dependency
63
+ name: pry-byebug
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ! '>='
68
+ - !ruby/object:Gem::Version
69
+ version: '0'
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ! '>='
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ - !ruby/object:Gem::Dependency
79
+ name: minitest-spec-rails
80
+ requirement: !ruby/object:Gem::Requirement
81
+ none: false
82
+ requirements:
83
+ - - ! '>='
84
+ - !ruby/object:Gem::Version
85
+ version: '0'
86
+ type: :development
87
+ prerelease: false
88
+ version_requirements: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: '0'
94
+ - !ruby/object:Gem::Dependency
95
+ name: minitest-reporters
96
+ requirement: !ruby/object:Gem::Requirement
97
+ none: false
98
+ requirements:
99
+ - - ! '>='
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ type: :development
103
+ prerelease: false
104
+ version_requirements: !ruby/object:Gem::Requirement
105
+ none: false
106
+ requirements:
107
+ - - ! '>='
108
+ - !ruby/object:Gem::Version
109
+ version: '0'
41
110
  description:
42
111
  email: rowland@rowlandresearch.com
43
112
  executables: []
@@ -53,30 +122,34 @@ files:
53
122
  - lib/active_record/connection_adapters/fb_adapter.rb
54
123
  - lib/active_record/connection_adapters/fb_column.rb
55
124
  - lib/active_record/fb_base.rb
125
+ - lib/active_record/tasks/fb_database_tasks.rb
126
+ - lib/activerecord-fb-adapter.rb
56
127
  - lib/arel/visitors/fb.rb
128
+ - lib/arel/visitors/fb_collector.rb
57
129
  homepage: http://github.com/rowland/activerecord-fb-adapter
58
130
  licenses:
59
131
  - MIT
60
- metadata: {}
61
132
  post_install_message:
62
133
  rdoc_options: []
63
134
  require_paths:
64
135
  - lib
65
136
  required_ruby_version: !ruby/object:Gem::Requirement
137
+ none: false
66
138
  requirements:
67
- - - ">="
139
+ - - ! '>='
68
140
  - !ruby/object:Gem::Version
69
141
  version: '0'
70
142
  required_rubygems_version: !ruby/object:Gem::Requirement
143
+ none: false
71
144
  requirements:
72
- - - ">="
145
+ - - ! '>='
73
146
  - !ruby/object:Gem::Version
74
147
  version: '0'
75
148
  requirements:
76
149
  - Firebird library fb
77
150
  rubyforge_project:
78
- rubygems_version: 2.2.0
151
+ rubygems_version: 1.8.23
79
152
  signing_key:
80
- specification_version: 4
153
+ specification_version: 3
81
154
  summary: ActiveRecord Firebird Adapter for Rails 3 and 4 with support for migrations.
82
155
  test_files: []
checksums.yaml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- SHA1:
3
- metadata.gz: fd91257c22642692e91cfd7a15de3bc0684d3148
4
- data.tar.gz: 28371fa7615afc66fcbf322ea08b77810390fe05
5
- SHA512:
6
- metadata.gz: 76431db22cf5caf52185ab189ab48295dcbe62e8bba1863ecb21c7213c5ab228d78c87b19a10d3f872741e2247ab40ef3130404db3bb0b8c93acdcdc9083a2ca
7
- data.tar.gz: 732da63410df05ae6416c6f38d7c4e3ff322a0b5a49a66cd61930db5a4ec2835508c26c081477c48b962e96daf2bd76e423d82524aade7d3f404ee012a65250f