Module: Rhubarb::PersistingClassMixins

Includes:
FindFreshest
Defined in:
lib/rhubarb/classmixins.rb

Overview

Methods mixed in to the class object of a persisting class

Constant Summary collapse

PCM_INPUT_TRANSFORMERS =
{:blob=>Util.blobify_proc, :zblob=>Util.zblobify_proc, :object=>Util.swizzle_object_proc}
PCM_OUTPUT_TRANSFORMERS =
{:object=>Util.deswizzle_object_proc, :zblob=>Util.dezblobify_proc}

Instance Method Summary collapse

Methods included from FindFreshest

#find_freshest

Instance Method Details

#check(condition) ⇒ Object

Models a CHECK constraint.



56
57
58
# File 'lib/rhubarb/classmixins.rb', line 56

def check(condition)
  "check (#{condition})"
end

#countObject

Returns the number of rows in the table backing this class



368
369
370
# File 'lib/rhubarb/classmixins.rb', line 368

def count
  self.db.get_first_value("select count(row_id) from #{quoted_table_name}").to_i
end

#create(*args) ⇒ Object

Creates a new row in the table with the supplied column values. May throw a SQLite3::SQLException.



296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
# File 'lib/rhubarb/classmixins.rb', line 296

def create(*args)
  new_row = args[0]
  new_row[:created] = new_row[:updated] = Util::timestamp

  colspec, valspec = cvspecs[new_row.keys]

  res = nil

  # resolve any references in the args
  new_row.each do |column,value|
    xform = PCM_INPUT_TRANSFORMERS[colkinds[column]]
    new_row[column] = xform ? xform.call(value) : Util::rhubarb_fk_identity(value)
  end

  create_text = "insert into #{quoted_table_name} (#{colspec}) values (#{valspec})"
  db.do_query(create_text, new_row)
  
  res = find(db.last_insert_row_id)
  
  res
end

#create_table(dbkey = :default) ⇒ Object

Creates a table in the database corresponding to this class.



326
327
328
329
330
331
# File 'lib/rhubarb/classmixins.rb', line 326

def create_table(dbkey=:default)
  ensure_accessors
  self.db ||= Persistence::dbs[dbkey] unless @explicitdb
  self.db.execute(table_decl)
  @creation_callbacks.each {|func| func.call}
end

#cvspecsObject



379
380
381
382
383
384
# File 'lib/rhubarb/classmixins.rb', line 379

def cvspecs
  @cvspecs ||= Hash.new do |h,new_row_keys|
    cols = colnames.intersection new_row_keys
    h[new_row_keys] = [Proc.new {|f| f.to_s.inspect}, Proc.new {|f| f.inspect}].map {|p| (cols.map {|col| p.call(col)}).join(", ")}
  end
end

#dbObject



333
334
335
# File 'lib/rhubarb/classmixins.rb', line 333

def db
  @db || Persistence::db
end

#db=(dbo) ⇒ Object



337
338
339
340
# File 'lib/rhubarb/classmixins.rb', line 337

def db=(dbo)
  @explicitdb = true
  @db = dbo
end

#declare_column(cname, kind, *quals) ⇒ Object

Adds a column named cname to this table declaration, and adds the following methods to the class:

  • accessors for cname, called cname and cname=

  • find_by_cname and find_first_by_cname methods, which return a list of rows and the first row that have the given value for cname, respectively

If the column references a column in another table (given via a references(…) argument to quals), then add triggers to the database to ensure referential integrity and cascade-on-delete (if specified)



162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
# File 'lib/rhubarb/classmixins.rb', line 162

def declare_column(cname, kind, *quals)
  ensure_accessors

  find_method_name = "find_by_#{cname}".to_sym
  find_first_method_name = "find_first_by_#{cname}".to_sym
  find_query = "select * from #{quoted_table_name} where \"#{cname}\" = ? order by row_id"

  get_method_name = "#{cname}".to_sym
  set_method_name = "#{cname}=".to_sym

  # does this column reference another table?
  rf = quals.find {|qual| qual.class == Reference}
  if rf
    self.refs[cname] = rf
  end

  # add a find for this column (a class method)
  klass = (class << self; self end)
  klass.class_eval do 
    if ::Rhubarb::Persistence::sqlite_13
      define_method find_method_name do |arg|
        results = []
        arg = Util.rhubarb_fk_identity(arg)
        db.do_query(find_query, arg) {|row| results << self.new(row)}
        results
      end
      
      define_method find_first_method_name do |arg|
        result = nil
        arg = Util.rhubarb_fk_identity(arg)
        db.do_query(find_query, arg) {|row| result = self.new(row) ; break }
        result
      end
    else
      define_method find_method_name do |arg|
        results = []
        db.do_query(find_query, arg) {|row| results << self.new(row)}
        results
      end
      
      define_method find_first_method_name do |arg|
        result = nil
        db.do_query(find_query, arg) {|row| result = self.new(row) ; break }
        result
      end
    end
  end

  self.colnames.merge([cname])
  self.colkinds[cname] = kind
  self.columns << Column.new(cname, kind, quals)

  out_xform = PCM_OUTPUT_TRANSFORMERS[kind]
  # add accessors

  getp = Proc.new do |cn, ox|
    define_method get_method_name do
      freshen
      return nil unless @tuple
      lookup_result = (@tuple[cn.to_s])
      (ox != nil) ? ox.call(lookup_result) : lookup_result
    end
  end

  getp.call(cname, out_xform)

  if not rf
    xform = nil
    
    xform = PCM_INPUT_TRANSFORMERS[kind]
    setp = Proc.new do |cn,xf|
      define_method set_method_name do |arg|
        new_val = xform ? xform.call(arg) : arg
        @tuple["#{cn}"] = new_val
        update cn, new_val
      end     
    end

    setp.call(cname,xform)
  else
    # this column references another table; create a set 
    # method that can handle either row objects or row IDs
    setp = Proc.new do |cn, rf|
      define_method set_method_name do |arg|
        freshen
        
        arg_id = nil
        
        if arg.class == Fixnum
          arg_id = arg
          arg = rf.referent.find arg_id
        else
          arg_id = arg.row_id
        end
        @tuple["#{cn}"] = arg
        
        update cn, arg_id
      end
    end

    setp.call(cname, rf)

    # Finally, add appropriate triggers to ensure referential integrity.
    # If rf has an on_delete trigger, also add the necessary
    # triggers to cascade deletes. 
    # Note that we do not support update triggers, since the API does 
    # not expose the capacity to change row IDs.
    
    rtable = rf.referent.table_name
    qrtable = rf.referent.quoted_table_name

    self.creation_callbacks << Proc.new do   
      @ccount ||= 0

      insert_trigger_name, delete_trigger_name = %w{insert delete}.map {|op| "ri_#{op}_#{self.table_name}_#{@ccount}_#{rtable}" } 

      self.db.execute_batch("CREATE TRIGGER #{insert_trigger_name} BEFORE INSERT ON #{quoted_table_name} WHEN new.\"#{cname}\" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM #{qrtable} WHERE new.\"#{cname}\" == \"#{rf.column}\") BEGIN SELECT RAISE(ABORT, 'constraint #{insert_trigger_name} (#{rtable} missing foreign key row) failed'); END;")

      self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} BEFORE DELETE ON #{qrtable} WHEN EXISTS (SELECT 1 FROM #{quoted_table_name} WHERE old.\"#{rf.column}\" == \"#{cname}\") BEGIN DELETE FROM #{quoted_table_name} WHERE \"#{cname}\" = old.\"#{rf.column}\"; END;") if rf.options[:on_delete] == :cascade

      @ccount = @ccount + 1
    end
  end
end

#declare_constraint(cname, kind, *details) ⇒ Object

Declares a constraint. Only check constraints are supported; see the check method.



289
290
291
292
# File 'lib/rhubarb/classmixins.rb', line 289

def declare_constraint(cname, kind, *details)
  ensure_accessors
  @constraints << "constraint #{cname} #{kind} #{details.join(" ")}"
end

#declare_custom_query(name, query) ⇒ Object

Declares a custom query method named name, and adds it to this class. The custom query method returns a list of objects corresponding to the rows returned by executing query on the database. query should select all fields (with SELECT *). If query includes the string _TABLE_, it will be expanded to the table name. Typically, you will want to use declare_query instead; this method is most useful for self-joins.



125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
# File 'lib/rhubarb/classmixins.rb', line 125

def declare_custom_query(name, query)
  klass = (class << self; self end)
  processed_query = query.gsub("__TABLE__", "#{self.quoted_table_name}")
  
  klass.class_eval do
    define_method name.to_s do |*args|
      # handle reference parameters
      if args.size == 1 && args[0].is_a?(Hash)
        args[0].each do |k,v|
          args[0][k] = Util::rhubarb_fk_identity(v)
        end
      else
        args = args.map do |arg| 
          raise RuntimeError.new("Hash-valued positional parameters may only appear as named positional parameters.") if arg.is_a?(Hash)
          Util::rhubarb_fk_identity(arg)
        end
      end
      
      results = []
      db.do_query(processed_query, args) {|tup| results << self.new(tup)}
      results
    end
  end
end

#declare_index_on(*fields) ⇒ Object



150
151
152
153
154
155
156
# File 'lib/rhubarb/classmixins.rb', line 150

def declare_index_on(*fields)
  @creation_callbacks << Proc.new do
    idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
    creation_cmd = "create index #{idx_name} on #{self.quoted_table_name} (#{fields.join(', ')})"
    self.db.execute(creation_cmd)
  end if fields.size > 0
end

#declare_query(name, query) ⇒ Object

Declares a query method named name and adds it to this class. The query method returns a list of objects corresponding to the rows returned by executing “+SELECT * FROM+ table WHERE query” on the database.



120
121
122
# File 'lib/rhubarb/classmixins.rb', line 120

def declare_query(name, query)
  declare_custom_query(name, "select * from __TABLE__ where #{query}")
end

#declare_table_name(nm) ⇒ Object Also known as: table_name=

Enables setting the table name to a custom name



42
43
44
# File 'lib/rhubarb/classmixins.rb', line 42

def declare_table_name(nm)
  @table_name = nm
end

#delete_allObject



107
108
109
# File 'lib/rhubarb/classmixins.rb', line 107

def delete_all
  db.do_query("DELETE from #{quoted_table_name}")
end

#delete_where(arg_hash) ⇒ Object



111
112
113
114
115
116
117
# File 'lib/rhubarb/classmixins.rb', line 111

def delete_where(arg_hash)
  arg_hash = arg_hash.dup
  valid_cols = self.colnames.intersection arg_hash.keys
  select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
  arg_hash.each {|key,val| arg_hash[key] = val.row_id if val.respond_to? :row_id}
  db.do_query("DELETE FROM #{quoted_table_name} WHERE #{select_criteria}", arg_hash)
end

#ensure_accessorsObject

Ensure that all the necessary accessors on our class instance are defined and that all metaclass fields have the appropriate values



344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
# File 'lib/rhubarb/classmixins.rb', line 344

def ensure_accessors
  # Define singleton accessors
  if not self.respond_to? :columns
    class << self
      # Arrays of columns, column names, and column constraints.
      # Note that colnames does not contain id, created, or updated.
      # The API purposefully does not expose the ability to create a
      # row with a given id, and created and updated values are
      # maintained automatically by the API.
      attr_accessor :columns, :colnames, :colkinds, :constraints, :dirtied, :refs, :creation_callbacks
    end
  end

  # Ensure singleton fields are initialized
  self.columns ||= [Column.new(:row_id, :integer, [:primary_key]), Column.new(:created, :integer, []), Column.new(:updated, :integer, [])]
  self.colnames ||= Set.new [:created, :updated]
  self.constraints ||= []
  self.colkinds ||= {}
  self.dirtied ||= {}
  self.refs ||= {}
  self.creation_callbacks ||= []
end

#find(id) ⇒ Object Also known as: find_by_id

Returns an object corresponding to the row with the given ID, or nil if no such row exists.



61
62
63
64
# File 'lib/rhubarb/classmixins.rb', line 61

def find(id)
  tup = self.find_tuple(id)
  tup ? self.new(tup) : nil
end

#find_allObject

Does what it says on the tin. Since this will allocate an object for each row, it isn’t recomended for huge tables.



101
102
103
104
105
# File 'lib/rhubarb/classmixins.rb', line 101

def find_all
  results = []
  db.do_query("SELECT * from #{quoted_table_name}") {|tup| results << self.new(tup)}
  results
end

#find_by_sqlite12(arg_hash) ⇒ Object Also known as: find_by



82
83
84
85
86
87
88
89
90
91
92
# File 'lib/rhubarb/classmixins.rb', line 82

def find_by_sqlite12(arg_hash)
  results = []
  arg_hash = arg_hash.dup
  valid_cols = self.colnames.intersection arg_hash.keys
  select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
  arg_hash.each do |key,val| 
    arg_hash[key] = val.row_id if val.respond_to? :row_id
  end
  db.do_query("select * from #{quoted_table_name} where #{select_criteria} order by row_id", arg_hash) {|tup| results << self.new(tup) }
  results
end

#find_by_sqlite13(arg_hash) ⇒ Object



68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/rhubarb/classmixins.rb', line 68

def find_by_sqlite13(arg_hash)
  results = []
  arg_hash = arg_hash.dup
  valid_cols = self.colnames.intersection arg_hash.keys
  select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
  arg_hash.each do |key,val| 
    arg_hash[key] = val.row_id if val.respond_to? :row_id
    xform = PCM_INPUT_TRANSFORMERS[colkinds[key]]
    arg_hash[key] = xform.call(val) if xform
  end
  db.do_query("select * from #{quoted_table_name} where #{select_criteria} order by row_id", arg_hash) {|tup| results << self.new(tup) }
  results
end

#find_tuple(tid) ⇒ Object



372
373
374
375
376
377
# File 'lib/rhubarb/classmixins.rb', line 372

def find_tuple(tid)
  ft_text = "select * from #{quoted_table_name} where row_id = ?"
  result = nil
  db.do_query(ft_text, tid) {|tup| result = tup; break}
  result
end

#quoted_table_nameObject



37
38
39
# File 'lib/rhubarb/classmixins.rb', line 37

def quoted_table_name
  table_name(true)
end

#references(table, options = {}) ⇒ Object

Models a foreign-key relationship. options is a hash of options, which include

:column => name

specifies the name of the column to reference in klass (defaults to row id)

:on_delete => :cascade

specifies that deleting the referenced row in klass will delete all rows referencing that row through this reference



51
52
53
# File 'lib/rhubarb/classmixins.rb', line 51

def references(table, options={})
  Reference.new(table, options)
end

#table_declObject

Returns a string consisting of the DDL statement to create a table corresponding to this class.



320
321
322
323
# File 'lib/rhubarb/classmixins.rb', line 320

def table_decl
  ddlspecs = [columns.join(", "), constraints.join(", ")].reject {|str| str.size==0}.join(", ")
  "create table #{quoted_table_name} (#{ddlspecs});"
end

#table_name(quoted = false) ⇒ Object

Returns the name of the database table modeled by this class. Defaults to the name of the class (sans module names)



31
32
33
34
35
# File 'lib/rhubarb/classmixins.rb', line 31

def table_name(quoted=false)
  @table_name ||= self.name.split("::").pop.downcase  
  @quoted_table_name ||= "'#{@table_name}'"
  quoted ? @quoted_table_name : @table_name
end