Module: Rhubarb::PersistingClassMixins
- Defined in:
- lib/rhubarb/rhubarb.rb
Overview
Methods mixed in to the class object of a persisting class
Instance Method Summary collapse
-
#check(condition) ⇒ Object
Models a CHECK constraint.
-
#count ⇒ Object
Returns the number of rows in the table backing this class.
-
#create(*args) ⇒ Object
Creates a new row in the table with the supplied column values.
-
#create_table(dbkey = :default) ⇒ Object
Creates a table in the database corresponding to this class.
- #db ⇒ Object
- #db=(d) ⇒ Object
-
#declare_column(cname, kind, *quals) ⇒ Object
Adds a column named
cnameto this table declaration, and adds the following methods to the class: * accessors forcname, calledcnameandcname=*find_by_cnameandfind_first_by_cnamemethods, which return a list of rows and the first row that have the given value forcname, respectively If the column references a column in another table (given via a references(…) argument toquals), then add triggers to the database to ensure referential integrity and cascade-on-delete (if specified). -
#declare_constraint(cname, kind, *details) ⇒ Object
Declares a constraint.
-
#declare_custom_query(name, query) ⇒ Object
Declares a custom query method named
name, and adds it to this class. - #declare_index_on(*fields) ⇒ Object
-
#declare_query(name, query) ⇒ Object
Declares a query method named
nameand adds it to this class. -
#declare_table_name(nm) ⇒ Object
Enables setting the table name to a custom name.
- #delete_all ⇒ Object
-
#ensure_accessors ⇒ Object
Ensure that all the necessary accessors on our class instance are defined and that all metaclass fields have the appropriate values.
-
#find(id) ⇒ Object
(also: #find_by_id)
Returns an object corresponding to the row with the given ID, or
nilif no such row exists. -
#find_all ⇒ Object
Does what it says on the tin.
- #find_by(arg_hash) ⇒ Object
-
#find_freshest(args) ⇒ Object
args contains the following keys * :group_by maps to a list of columns to group by (mandatory) * :select_by maps to a hash mapping from column symbols to values (optional) * :version maps to some version to be considered “current” for the purposes of this query; that is, all rows later than the “current” version will be disregarded (optional, defaults to latest version).
- #find_tuple(id) ⇒ Object
-
#references(table, options = {}) ⇒ Object
Models a foreign-key relationship.
-
#table_decl ⇒ Object
Returns a string consisting of the DDL statement to create a table corresponding to this class.
-
#table_name ⇒ Object
Returns the name of the database table modeled by this class.
Instance Method Details
#check(condition) ⇒ Object
Models a CHECK constraint.
133 134 135 |
# File 'lib/rhubarb/rhubarb.rb', line 133 def check(condition) "check (#{condition})" end |
#count ⇒ Object
Returns the number of rows in the table backing this class
422 423 424 425 |
# File 'lib/rhubarb/rhubarb.rb', line 422 def count result = self.db.execute("select count(row_id) from #{table_name}")[0] result[0].to_i end |
#create(*args) ⇒ Object
Creates a new row in the table with the supplied column values. May throw a SQLite3::SQLException.
347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 |
# File 'lib/rhubarb/rhubarb.rb', line 347 def create(*args) new_row = args[0] new_row[:created] = new_row[:updated] = SQLBUtil:: cols = colnames.intersection new_row.keys colspec = (cols.map {|col| col.to_s}).join(", ") valspec = (cols.map {|col| col.inspect}).join(", ") res = nil # resolve any references in the args new_row.each do |k,v| new_row[k] = v.row_id if v.class.ancestors.include? Persisting end self.db.transaction do |db| stmt = "insert into #{table_name} (#{colspec}) values (#{valspec})" # p stmt db.execute(stmt, new_row) res = find(db.last_insert_row_id) end res end |
#create_table(dbkey = :default) ⇒ Object
Creates a table in the database corresponding to this class.
383 384 385 386 387 |
# File 'lib/rhubarb/rhubarb.rb', line 383 def create_table(dbkey=:default) self.db = Persistence::dbs[dbkey] self.db.execute(table_decl) @creation_callbacks.each {|func| func.call} end |
#db ⇒ Object
389 390 391 |
# File 'lib/rhubarb/rhubarb.rb', line 389 def db @db || Persistence::db end |
#db=(d) ⇒ Object
393 394 395 |
# File 'lib/rhubarb/rhubarb.rb', line 393 def db=(d) @db = d 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, calledcnameandcname= -
find_by_cnameandfind_first_by_cnamemethods, which return a list of rows and the first row that have the given value forcname, 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)
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 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 |
# File 'lib/rhubarb/rhubarb.rb', line 252 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 get_method_name = "#{cname}".to_sym set_method_name = "#{cname}=".to_sym # does this column reference another table? rf = quals.find {|q| q.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 define_method find_method_name do |arg| res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg) res.map {|row| self.new(row)} end define_method find_first_method_name do |arg| res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg) return self.new(res[0]) if res.size > 0 nil end end self.colnames.merge([cname]) self.columns << Column.new(cname, kind, quals) # add accessors define_method get_method_name do freshen return @tuple["#{cname}"] if @tuple nil end if not rf define_method set_method_name do |arg| @tuple["#{cname}"] = arg update cname, arg end else # this column references another table; create a set # method that can handle either row objects or row IDs 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["#{cname}"] = arg update cname, arg_id end # 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. self.creation_callbacks << Proc.new do @ccount ||= 0 insert_trigger_name = "ri_insert_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}" delete_trigger_name = "ri_delete_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}" self.db.execute_batch("CREATE TRIGGER #{insert_trigger_name} BEFORE INSERT ON \"#{self.table_name}\" WHEN new.\"#{cname}\" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM \"#{rf.referent.table_name}\" WHERE new.\"#{cname}\" == \"#{rf.column}\") BEGIN SELECT RAISE(ABORT, 'constraint #{insert_trigger_name} (#{rf.referent.table_name} missing foreign key row) failed'); END;") self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} BEFORE DELETE ON \"#{rf.referent.table_name}\" WHEN EXISTS (SELECT 1 FROM \"#{self.table_name}\" WHERE old.\"#{rf.column}\" == \"#{cname}\") BEGIN DELETE FROM \"#{self.table_name}\" WHERE \"#{cname}\" = old.\"#{rf.column}\"; END;") if rf.[: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.
339 340 341 342 343 |
# File 'lib/rhubarb/rhubarb.rb', line 339 def declare_constraint(cname, kind, *details) ensure_accessors info = details.join(" ") @constraints << "constraint #{cname} #{kind} #{info}" 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.
226 227 228 229 230 231 232 233 234 235 236 237 238 |
# File 'lib/rhubarb/rhubarb.rb', line 226 def declare_custom_query(name, query) klass = (class << self; self end) klass.class_eval do define_method name.to_s do |*args| # handle reference parameters args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x} res = self.db.execute(query.gsub("__TABLE__", "#{self.table_name}"), args) # XXX: should freshen each row? res.map {|row| self.new(row) } end end end |
#declare_index_on(*fields) ⇒ Object
240 241 242 243 244 245 246 |
# File 'lib/rhubarb/rhubarb.rb', line 240 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.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.
212 213 214 215 216 217 218 219 220 221 222 223 |
# File 'lib/rhubarb/rhubarb.rb', line 212 def declare_query(name, query) klass = (class << self; self end) klass.class_eval do define_method name.to_s do |*args| # handle reference parameters args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x} res = self.db.execute("select * from #{table_name} where #{query}", args) res.map {|row| self.new(row)} end end end |
#declare_table_name(nm) ⇒ Object
Enables setting the table name to a custom name
121 122 123 |
# File 'lib/rhubarb/rhubarb.rb', line 121 def declare_table_name(nm) @table_name = nm end |
#delete_all ⇒ Object
207 208 209 |
# File 'lib/rhubarb/rhubarb.rb', line 207 def delete_all self.db.execute("DELETE from #{table_name}") end |
#ensure_accessors ⇒ Object
Ensure that all the necessary accessors on our class instance are defined and that all metaclass fields have the appropriate values
399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 |
# File 'lib/rhubarb/rhubarb.rb', line 399 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, :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.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.
138 139 140 141 142 |
# File 'lib/rhubarb/rhubarb.rb', line 138 def find(id) tup = self.find_tuple(id) return self.new(tup) if tup nil end |
#find_all ⇒ Object
Does what it says on the tin. Since this will allocate an object for each row, it isn’t recomended for huge tables.
203 204 205 |
# File 'lib/rhubarb/rhubarb.rb', line 203 def find_all self.db.execute("SELECT * from #{table_name}").map {|tup| self.new(tup)} end |
#find_by(arg_hash) ⇒ Object
146 147 148 149 150 151 152 153 |
# File 'lib/rhubarb/rhubarb.rb', line 146 def find_by(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 {|k,v| arg_hash[k] = v.row_id if v.respond_to? :row_id} self.db.execute("select * from #{table_name} where #{select_criteria} order by row_id", arg_hash).map {|tup| self.new(tup) } end |
#find_freshest(args) ⇒ Object
args contains the following keys
* :group_by maps to a list of columns to group by (mandatory)
* :select_by maps to a hash mapping from column symbols to values (optional)
* :version maps to some version to be considered "current" for the purposes of this query; that is, all rows later than the "current" version will be disregarded (optional, defaults to latest version)
159 160 161 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 |
# File 'lib/rhubarb/rhubarb.rb', line 159 def find_freshest(args) args = args.dup args[:version] ||= SQLBUtil:: args[:select_by] ||= {} query_params = {} query_params[:version] = args[:version] select_clauses = ["created <= :version"] valid_cols = self.colnames.intersection args[:select_by].keys valid_cols.map do |col| select_clauses << "#{col.to_s} = #{col.inspect}" val = args[:select_by][col] val = val.row_id if val.respond_to? :row_id query_params[col] = val end group_by_clause = "GROUP BY " + args[:group_by].join(", ") where_clause = "WHERE " + select_clauses.join(" AND ") projection = self.colnames - [:created] join_clause = projection.map do |column| "__fresh.#{column} = __freshest.#{column}" end projection << "MAX(created) AS __current_version" join_clause << "__fresh.__current_version = __freshest.created" query = " SELECT __freshest.* FROM ( SELECT #{projection.to_a.join(', ')} FROM ( SELECT * from #{table_name} #{where_clause} ) #{group_by_clause} ) as __fresh INNER JOIN #{table_name} as __freshest ON #{join_clause.join(' AND ')} ORDER BY row_id " self.db.execute(query, query_params).map {|tup| self.new(tup) } end |
#find_tuple(id) ⇒ Object
427 428 429 430 431 432 433 434 |
# File 'lib/rhubarb/rhubarb.rb', line 427 def find_tuple(id) res = self.db.execute("select * from #{table_name} where row_id = ?", id) if res.size == 0 nil else res[0] end 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
klasswill delete all rows referencing that row through this reference
128 129 130 |
# File 'lib/rhubarb/rhubarb.rb', line 128 def references(table, ={}) Reference.new(table, ) end |
#table_decl ⇒ Object
Returns a string consisting of the DDL statement to create a table corresponding to this class.
372 373 374 375 376 377 378 379 380 |
# File 'lib/rhubarb/rhubarb.rb', line 372 def table_decl cols = columns.join(", ") consts = constraints.join(", ") if consts.size > 0 "create table #{table_name} (#{cols}, #{consts});" else "create table #{table_name} (#{cols});" end end |
#table_name ⇒ Object
Returns the name of the database table modeled by this class. Defaults to the name of the class (sans module names)
116 117 118 |
# File 'lib/rhubarb/rhubarb.rb', line 116 def table_name @table_name ||= self.name.split("::").pop.downcase end |