Module: ActiveRecord::SnapshotView::ClassMethods

Defined in:
lib/activerecord_snapshot_view/snapshot_view.rb

Constant Summary collapse

ALPHABET =
"abcdefghijklmnopqrstuvwxyz"

Class Method Summary collapse

Instance Method Summary collapse

Class Method Details

.included(mod) ⇒ Object



89
90
91
92
93
94
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 89

def ClassMethods.included(mod)
  mod.instance_eval do
    alias_method :org_table_name, :table_name
    alias_method :table_name, :active_working_table_or_active_table_name
  end
end

Instance Method Details

#active_table_nameObject

name of the active table read direct from db



199
200
201
202
203
204
205
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 199

def active_table_name
  st = switch_table_name
  begin
    connection.select_value( "select current from #{st}" )
  rescue
  end || default_active_table_name
end

#active_working_table_nameObject



254
255
256
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 254

def active_working_table_name
  Thread.current[thread_local_key_name]
end

#active_working_table_name=(name) ⇒ Object



258
259
260
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 258

def active_working_table_name=(name)
  Thread.current[thread_local_key_name] = name
end

#active_working_table_or_active_table_nameObject

name of the active table, or the working table if inside a new_version block



263
264
265
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 263

def active_working_table_or_active_table_name
  active_working_table_name || active_table_name
end

#advance_versionObject

make working table active, then recreate new working table from base table schema



233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 233

def advance_version
  switch_to(working_table_name)

  # ensure the presence of the new active and working tables. 
  # happens after the switch table update, since this may commit a surrounding 
  # transaction in dbs with retarded non-transactional ddl like, oh i dunno, MyFuckingSQL
  ensure_version_table(active_table_name)

  # recreate the new working table from the base schema. 
  new_wtn = working_table_name
  if new_wtn != base_table_name
    dup_table_schema(base_table_name, new_wtn)
  else
    connection.execute( "truncate table #{new_wtn}" )
  end
end

#base_table_nameObject



117
118
119
120
121
122
123
124
125
126
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 117

def base_table_name
  if !@base_table_name
    @base_table_name = org_table_name
    # the original table_name method re-aliases itself !
    class << self
      alias_method :table_name, :active_working_table_or_active_table_name
    end
  end
  @base_table_name
end

#create_switch_table(name) ⇒ Object

create a switch table of given name, if it doesn’t already exist



160
161
162
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 160

def create_switch_table(name)
  connection.execute( "create table if not exists #{name} (`current` varchar(255))" )
end

#default_active_table_nameObject



192
193
194
195
196
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 192

def default_active_table_name
  # no longer use a different table name for test environments...
  # it makes a mess with named scopes
  base_table_name
end

#dup_table_schema(from, to) ⇒ Object

use schema of from table to recreate to table



129
130
131
132
133
134
135
136
137
138
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 129

def dup_table_schema(from, to)
  connection.execute( "drop table if exists #{to}")
  ct = connection.select_one( "show create table #{from}")["Create Table"]
  ct_no_constraint_names = ct.gsub(/CONSTRAINT `[^`]*`/, "CONSTRAINT ``")
  i = 0
  ct_uniq_constraint_names = ct_no_constraint_names.gsub(/CONSTRAINT ``/) { |s| i+=1 ; "CONSTRAINT `#{to}_#{i}`" }

  new_ct = ct_uniq_constraint_names.gsub( /CREATE TABLE `#{from}`/, "CREATE TABLE `#{to}`")
  connection.execute(new_ct)
end

#ensure_all_tablesObject



214
215
216
217
218
219
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 214

def ensure_all_tables
  suffixed_table_names.each do |table_name|
    ensure_version_table(table_name)
  end
  ensure_switch_table
end

#ensure_switch_tableObject

create the switch table if it doesn’t already exist. return the switch table name



165
166
167
168
169
170
171
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 165

def ensure_switch_table
  stn = switch_table_name
  if !connection.table_exists?(stn) # don't execute any ddl code if we don't need to
    create_switch_table(stn)
  end
  stn
end

#ensure_version_table(name) ⇒ Object



152
153
154
155
156
157
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 152

def ensure_version_table(name)
  if !connection.table_exists?(name) &&
      base_table_name!=name # don't execute ddl unless necessary
    dup_table_schema(base_table_name, name)
  end
end

#historical_version_countObject

number of historical tables to keep around for posterity, or more likely to ensure running transactions aren’t taken down by advance_version recreating a table. default 2



99
100
101
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 99

def historical_version_count
  @historical_version_count || 2
end

#new_version(&block) ⇒ Object

make the working table temporarily active [ for this thread only ], execute the block, and if completed without exception then make the working table permanently active



285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 285

def new_version(&block)
  begin
    self.active_working_table_name = working_table_name
    ensure_version_table(working_table_name)
    connection.execute("truncate table #{working_table_name}")
    r = block.call
    advance_version
    r
  rescue SaveWork => e
    advance_version
    raise e # raise the SaveWork again, in case we are inside nested new_versions
  ensure
    self.active_working_table_name = nil
  end
end

#prepare_to_migrateObject

copy all data to base table, reset switch table and drop suffixed tables… for migration support



141
142
143
144
145
146
147
148
149
150
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 141

def prepare_to_migrate
  if active_table_name != base_table_name
    connection.execute( "truncate table #{base_table_name}" )
    connection.execute( "insert into #{base_table_name} select * from #{active_table_name}" )
  end
  suffixed_table_names.each do |stn|
    connection.execute( "drop table if exists #{stn}" )
  end
  connection.execute( "drop table if exists #{switch_table_name}" )
end

#set_historical_version_count(count) ⇒ Object Also known as: historical_version_count=

set the number of historical tables to keep around to ensure running transactions aren’t interrupted by truncating working tables. 2 is default



105
106
107
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 105

def set_historical_version_count(count)
  @historical_version_count = count
end

#set_table_name(name) ⇒ Object Also known as: table_name=

hide the ActiveRecord::Base method, which redefines a table_name method, and instead capture the given name as the base_table_name



112
113
114
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 112

def set_table_name(name)
  @base_table_name = name
end

#suffixed_table_namesObject

list of suffixed table names



179
180
181
182
183
184
185
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 179

def suffixed_table_names
  suffixes = []
  (0...historical_version_count).each{ |i| suffixes << ALPHABET[i...i+1] }
  suffixes.map do |suffix|
    base_table_name + "_" + suffix
  end
end

#switch_table_nameObject

name of the table with a row holding the active table name



174
175
176
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 174

def switch_table_name
  base_table_name + "_switch"
end

#switch_to(table) ⇒ Object

update switch table to point to a different table



222
223
224
225
226
227
228
229
230
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 222

def switch_to(table)
  st = ensure_switch_table
  # want a transaction at least here [surround is ok too] so 
  # there is never an empty switch table
  ActiveRecord::Base.transaction do
    connection.execute( "delete from #{st}")
    connection.execute( "insert into #{st} values (\'#{table}\')")
  end
end

#table_version_namesObject

ordered vector of table version names, starting with base name



188
189
190
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 188

def table_version_names
  [base_table_name] + suffixed_table_names
end

#thread_local_key_nameObject



250
251
252
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 250

def thread_local_key_name
  "ActiveRecord::SnapshotView::" + self.to_s
end

#updated_version(update = true, &block) ⇒ Object

like new_version, but instead of an empty table you start with a copy of the previous version (if update=true)



269
270
271
272
273
274
275
276
277
278
279
280
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 269

def updated_version(update=true, &block)
  new_version do
    if update
      sql = <<-EOF
        insert into #{working_table_name}
        select * from #{active_table_name}
      EOF
      connection.execute(sql)
    end
    block.call
  end
end

#working_table_nameObject

name of the working table



208
209
210
211
212
# File 'lib/activerecord_snapshot_view/snapshot_view.rb', line 208

def working_table_name
  atn = active_table_name
  tvn = table_version_names
  tvn[ (tvn.index(atn) + 1) % tvn.size ]
end