rhubarb 0.2.0 → 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGES CHANGED
@@ -1,4 +1,15 @@
1
- version 0.2.0
1
+ version 0.2.1
2
+
3
+ * Code cleanups
4
+
5
+ * Performance improvements
6
+
7
+ * Rhubarb now no longer uses explicit transactions internally. I have
8
+ become convinced that Rhubarb's use of transactions was overly
9
+ cautious; removing these enables Rhubarb users to use transactions
10
+ in their own code.
11
+
12
+ version 0.2.0 (eecc7cceda993ea9c20cd952aa709ebe470a9f3d)
2
13
 
3
14
  * First standalone release of Rhubarb. Previous releases are included
4
15
  with SPQR.
data/Rakefile CHANGED
@@ -1,6 +1,23 @@
1
1
  require 'rubygems'
2
2
  require 'rake'
3
3
 
4
+ begin
5
+ require 'metric_fu'
6
+ MetricFu::Configuration.run do |config|
7
+ #define which metrics you want to use
8
+ config.metrics = [:flog, :flay, :reek, :roodi]
9
+ config.graphs = [:flog, :flay, :reek, :roodi]
10
+ config.flay = { :dirs_to_flay => ['lib'],
11
+ :minimum_score => 10 }
12
+ config.flog = { :dirs_to_flog => ['lib'] }
13
+ config.reek = { :dirs_to_reek => ['lib'] }
14
+ config.roodi = { :dirs_to_roodi => ['lib'] }
15
+ config.graph_engine = :bluff
16
+ end
17
+ rescue LoadError
18
+ nil
19
+ end
20
+
4
21
  begin
5
22
  require 'jeweler'
6
23
  Jeweler::Tasks.new do |gem|
@@ -24,6 +41,65 @@ def pkg_version
24
41
  return version.chomp
25
42
  end
26
43
 
44
+ def pkg_version
45
+ version = File.exist?('VERSION') ? File.read('VERSION') : ""
46
+ return version.chomp
47
+ end
48
+
49
+ def pkg_name
50
+ return 'ruby-rhubarb'
51
+ end
52
+
53
+ def pkg_spec
54
+ return pkg_name() + ".spec"
55
+ end
56
+
57
+ def pkg_rel
58
+ return `grep -i 'define rel' #{pkg_spec} | awk '{print $3}'`.chomp()
59
+ end
60
+
61
+ def pkg_source
62
+ return pkg_name() + "-" + pkg_version() + "-" + pkg_rel() + ".tar.gz"
63
+ end
64
+
65
+ def pkg_dir
66
+ return pkg_name() + "-" + pkg_version()
67
+ end
68
+
69
+ def rpm_dirs
70
+ return %w{BUILD BUILDROOT RPMS SOURCES SPECS SRPMS}
71
+ end
72
+
73
+ desc "create RPMs"
74
+ task :rpms => [:build, :tarball] do
75
+ require 'fileutils'
76
+ FileUtils.cp pkg_spec(), 'SPECS'
77
+ sh "rpmbuild --define=\"_topdir \${PWD}\" -ba SPECS/#{pkg_spec}"
78
+ end
79
+
80
+ desc "Create a tarball"
81
+ task :tarball => :make_rpmdirs do
82
+ require 'fileutils'
83
+ FileUtils.cp_r 'lib', pkg_dir()
84
+ FileUtils.cp ['LICENSE', 'README.rdoc', 'CHANGES', 'TODO', 'VERSION'], pkg_dir()
85
+ sh "tar -cf #{pkg_source} #{pkg_dir}"
86
+ FileUtils.mv pkg_source(), 'SOURCES'
87
+ end
88
+
89
+ desc "Make dirs for building RPM"
90
+ task :make_rpmdirs => :rpm_clean do
91
+ require 'fileutils'
92
+ FileUtils.mkdir pkg_dir()
93
+ FileUtils.mkdir rpm_dirs()
94
+ end
95
+
96
+ desc "Cleanup after an RPM build"
97
+ task :rpm_clean do
98
+ require 'fileutils'
99
+ FileUtils.rm_r pkg_dir(), :force => true
100
+ FileUtils.rm_r rpm_dirs(), :force => true
101
+ end
102
+
27
103
  require 'spec/rake/spectask'
28
104
  Spec::Rake::SpecTask.new(:spec) do |spec|
29
105
  spec.libs << 'lib' << 'spec'
data/TODO CHANGED
@@ -4,6 +4,7 @@
4
4
  * Bring code coverage up to 100%
5
5
  * Documentation
6
6
  * Standalone examples (i.e. not just "please see the test suite")
7
+ * Resolve issues related to using prepared statements
7
8
 
8
9
  Legend:
9
10
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.2.1
@@ -0,0 +1,274 @@
1
+ # Class mixins for persisting classes. Part of Rhubarb.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ require 'rhubarb/mixins/freshness'
14
+
15
+ module Rhubarb
16
+ # Methods mixed in to the class object of a persisting class
17
+ module PersistingClassMixins
18
+ # Returns the name of the database table modeled by this class.
19
+ # Defaults to the name of the class (sans module names)
20
+ def table_name
21
+ @table_name ||= self.name.split("::").pop.downcase
22
+ end
23
+
24
+ # Enables setting the table name to a custom name
25
+ def declare_table_name(nm)
26
+ @table_name = nm
27
+ end
28
+
29
+ alias table_name= declare_table_name
30
+
31
+ # Models a foreign-key relationship. +options+ is a hash of options, which include
32
+ # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
33
+ # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
34
+ def references(table, options={})
35
+ Reference.new(table, options)
36
+ end
37
+
38
+ # Models a CHECK constraint.
39
+ def check(condition)
40
+ "check (#{condition})"
41
+ end
42
+
43
+ # Returns an object corresponding to the row with the given ID, or +nil+ if no such row exists.
44
+ def find(id)
45
+ tup = self.find_tuple(id)
46
+ tup ? self.new(tup) : nil
47
+ end
48
+
49
+ alias find_by_id find
50
+
51
+ def find_by(arg_hash)
52
+ arg_hash = arg_hash.dup
53
+ valid_cols = self.colnames.intersection arg_hash.keys
54
+ select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
55
+ arg_hash.each {|k,v| arg_hash[k] = v.row_id if v.respond_to? :row_id}
56
+
57
+ self.db.execute("select * from #{table_name} where #{select_criteria} order by row_id", arg_hash).map {|tup| self.new(tup) }
58
+ end
59
+
60
+ # Does what it says on the tin. Since this will allocate an object for each row, it isn't recomended for huge tables.
61
+ def find_all
62
+ self.db.execute("SELECT * from #{table_name}").map {|tup| self.new(tup)}
63
+ end
64
+
65
+ def delete_all
66
+ self.db.execute("DELETE from #{table_name}")
67
+ end
68
+
69
+ # 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.
70
+ def declare_query(name, query)
71
+ declare_custom_query(name, "select * from __TABLE__ where #{query}")
72
+ end
73
+
74
+ # 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.
75
+ def declare_custom_query(name, query)
76
+ klass = (class << self; self end)
77
+ klass.class_eval do
78
+ define_method name.to_s do |*args|
79
+ # handle reference parameters
80
+ args = args.map {|arg| Util::rhubarb_fk_identity(arg)}
81
+
82
+ res = self.db.execute(query.gsub("__TABLE__", "#{self.table_name}"), args)
83
+ res.map {|row| self.new(row) }
84
+ end
85
+ end
86
+ end
87
+
88
+ def declare_index_on(*fields)
89
+ @creation_callbacks << Proc.new do
90
+ idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
91
+ creation_cmd = "create index #{idx_name} on #{self.table_name} (#{fields.join(', ')})"
92
+ self.db.execute(creation_cmd)
93
+ end if fields.size > 0
94
+ end
95
+
96
+ # Adds a column named +cname+ to this table declaration, and adds the following methods to the class:
97
+ # * accessors for +cname+, called +cname+ and +cname=+
98
+ # * +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
99
+ # 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)
100
+ def declare_column(cname, kind, *quals)
101
+ ensure_accessors
102
+
103
+ find_method_name = "find_by_#{cname}".to_sym
104
+ find_first_method_name = "find_first_by_#{cname}".to_sym
105
+ find_query = "select * from #{table_name} where #{cname} = ?"
106
+
107
+ get_method_name = "#{cname}".to_sym
108
+ set_method_name = "#{cname}=".to_sym
109
+
110
+ # does this column reference another table?
111
+ rf = quals.find {|q| q.class == Reference}
112
+ if rf
113
+ self.refs[cname] = rf
114
+ end
115
+
116
+ # add a find for this column (a class method)
117
+ klass = (class << self; self end)
118
+ klass.class_eval do
119
+ define_method find_method_name do |arg|
120
+ res = self.db.execute(find_query, arg)
121
+ res.map {|row| self.new(row)}
122
+ end
123
+
124
+ define_method find_first_method_name do |arg|
125
+ res = self.db.get_first_row(find_query, arg)
126
+ res ? self.new(res) : nil
127
+ end
128
+ end
129
+
130
+ self.colnames.merge([cname])
131
+ self.columns << Column.new(cname, kind, quals)
132
+
133
+ # add accessors
134
+ define_method get_method_name do
135
+ freshen
136
+ return @tuple["#{cname}"] if @tuple
137
+ nil
138
+ end
139
+
140
+ if not rf
141
+ define_method set_method_name do |arg|
142
+ @tuple["#{cname}"] = arg
143
+ update cname, arg
144
+ end
145
+ else
146
+ # this column references another table; create a set
147
+ # method that can handle either row objects or row IDs
148
+ define_method set_method_name do |arg|
149
+ freshen
150
+
151
+ arg_id = nil
152
+
153
+ if arg.class == Fixnum
154
+ arg_id = arg
155
+ arg = rf.referent.find arg_id
156
+ else
157
+ arg_id = arg.row_id
158
+ end
159
+ @tuple["#{cname}"] = arg
160
+
161
+ update cname, arg_id
162
+ end
163
+
164
+ # Finally, add appropriate triggers to ensure referential integrity.
165
+ # If rf has an on_delete trigger, also add the necessary
166
+ # triggers to cascade deletes.
167
+ # Note that we do not support update triggers, since the API does
168
+ # not expose the capacity to change row IDs.
169
+
170
+ rtable = rf.referent.table_name
171
+
172
+ self.creation_callbacks << Proc.new do
173
+ @ccount ||= 0
174
+
175
+ insert_trigger_name, delete_trigger_name = %w{insert delete}.map {|op| "ri_#{op}_#{self.table_name}_#{@ccount}_#{rtable}" }
176
+
177
+ 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 \"#{rtable}\" WHERE new.\"#{cname}\" == \"#{rf.column}\") BEGIN SELECT RAISE(ABORT, 'constraint #{insert_trigger_name} (#{rtable} missing foreign key row) failed'); END;")
178
+
179
+ self.db.execute_batch("CREATE TRIGGER #{delete_trigger_name} BEFORE DELETE ON \"#{rtable}\" 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.options[:on_delete] == :cascade
180
+
181
+ @ccount = @ccount + 1
182
+ end
183
+ end
184
+ end
185
+
186
+ # Declares a constraint. Only check constraints are supported; see
187
+ # the check method.
188
+ def declare_constraint(cname, kind, *details)
189
+ ensure_accessors
190
+ @constraints << "constraint #{cname} #{kind} #{details.join(" ")}"
191
+ end
192
+
193
+ # Creates a new row in the table with the supplied column values.
194
+ # May throw a SQLite3::SQLException.
195
+ def create(*args)
196
+ new_row = args[0]
197
+ new_row[:created] = new_row[:updated] = Util::timestamp
198
+
199
+ cols = colnames.intersection new_row.keys
200
+ colspec, valspec = [:to_s, :inspect].map {|msg| (cols.map {|col| col.send(msg)}).join(", ")}
201
+ res = nil
202
+
203
+ # resolve any references in the args
204
+ new_row.each do |column,value|
205
+ new_row[column] = Util::rhubarb_fk_identity(value)
206
+ end
207
+
208
+ stmt = "insert into #{table_name} (#{colspec}) values (#{valspec})"
209
+ db.execute(stmt, new_row)
210
+ res = find(db.last_insert_row_id)
211
+
212
+ res
213
+ end
214
+
215
+ # Returns a string consisting of the DDL statement to create a table
216
+ # corresponding to this class.
217
+ def table_decl
218
+ ddlspecs = [columns.join(", "), constraints.join(", ")].reject {|str| str.size==0}.join(", ")
219
+ "create table #{table_name} (#{ddlspecs});"
220
+ end
221
+
222
+ # Creates a table in the database corresponding to this class.
223
+ def create_table(dbkey=:default)
224
+ self.db ||= Persistence::dbs[dbkey] unless @explicitdb
225
+ self.db.execute(table_decl)
226
+ @creation_callbacks.each {|func| func.call}
227
+ end
228
+
229
+ def db
230
+ @db || Persistence::db
231
+ end
232
+
233
+ def db=(dbo)
234
+ @explicitdb = true
235
+ @db = dbo
236
+ end
237
+
238
+ # Ensure that all the necessary accessors on our class instance are defined
239
+ # and that all metaclass fields have the appropriate values
240
+ def ensure_accessors
241
+ # Define singleton accessors
242
+ if not self.respond_to? :columns
243
+ class << self
244
+ # Arrays of columns, column names, and column constraints.
245
+ # Note that colnames does not contain id, created, or updated.
246
+ # The API purposefully does not expose the ability to create a
247
+ # row with a given id, and created and updated values are
248
+ # maintained automatically by the API.
249
+ attr_accessor :columns, :colnames, :constraints, :dirtied, :refs, :creation_callbacks
250
+ end
251
+ end
252
+
253
+ # Ensure singleton fields are initialized
254
+ self.columns ||= [Column.new(:row_id, :integer, [:primary_key]), Column.new(:created, :integer, []), Column.new(:updated, :integer, [])]
255
+ self.colnames ||= Set.new [:created, :updated]
256
+ self.constraints ||= []
257
+ self.dirtied ||= {}
258
+ self.refs ||= {}
259
+ self.creation_callbacks ||= []
260
+ end
261
+
262
+ # Returns the number of rows in the table backing this class
263
+ def count
264
+ self.db.get_first_value("select count(row_id) from #{table_name}").to_i
265
+ end
266
+
267
+ def find_tuple(id)
268
+ self.db.get_first_row("select * from #{table_name} where row_id = ?", id)
269
+ end
270
+
271
+ include FindFreshest
272
+
273
+ end
274
+ end
@@ -0,0 +1,32 @@
1
+ # Column abstraction for Rhubarb.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ module Rhubarb
14
+ class Column
15
+ attr_reader :name
16
+
17
+ def initialize(name, kind, quals)
18
+ @name, @kind = name, kind
19
+ @quals = quals.map {|x| x.to_s.gsub("_", " ") if x.class == Symbol}
20
+ @quals.map
21
+ end
22
+
23
+ def to_s
24
+ qualifiers = @quals.join(" ")
25
+ if qualifiers == ""
26
+ "#@name #@kind"
27
+ else
28
+ "#@name #@kind #{qualifiers}"
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,62 @@
1
+ # Freshness-based queries ("find_freshest") for Rhubarb classes.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ module Rhubarb
14
+ module FindFreshest
15
+ # args contains the following keys
16
+ # * :group_by maps to a list of columns to group by (mandatory)
17
+ # * :select_by maps to a hash mapping from column symbols to values (optional)
18
+ # * :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)
19
+ def find_freshest(args)
20
+ args = args.dup
21
+
22
+ args[:version] ||= Util::timestamp
23
+ args[:select_by] ||= {}
24
+
25
+ query_params = {}
26
+ query_params[:version] = args[:version]
27
+
28
+ select_clauses = ["created <= :version"]
29
+
30
+ valid_cols = self.colnames.intersection args[:select_by].keys
31
+
32
+ valid_cols.map do |col|
33
+ select_clauses << "#{col.to_s} = #{col.inspect}"
34
+ val = args[:select_by][col]
35
+ val = val.row_id if val.respond_to? :row_id
36
+ query_params[col] = val
37
+ end
38
+
39
+ group_by_clause = "GROUP BY " + args[:group_by].join(", ")
40
+ where_clause = "WHERE " + select_clauses.join(" AND ")
41
+ projection = self.colnames - [:created]
42
+ join_clause = projection.map do |column|
43
+ "__fresh.#{column} = __freshest.#{column}"
44
+ end
45
+
46
+ projection << "MAX(created) AS __current_version"
47
+ join_clause << "__fresh.__current_version = __freshest.created"
48
+
49
+ query = "
50
+ SELECT __freshest.* FROM (
51
+ SELECT #{projection.to_a.join(', ')} FROM (
52
+ SELECT * from #{table_name} #{where_clause}
53
+ ) #{group_by_clause}
54
+ ) as __fresh INNER JOIN #{table_name} as __freshest ON
55
+ #{join_clause.join(' AND ')}
56
+ ORDER BY row_id
57
+ "
58
+
59
+ self.db.execute(query, query_params).map {|tup| self.new(tup) }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ # Database interface for Rhubarb
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ module Rhubarb
14
+ module Persistence
15
+ class DbCollection < Hash
16
+ alias orig_set []=
17
+
18
+ def []=(key,db)
19
+ setup_db(db) if db
20
+ orig_set(key,db)
21
+ end
22
+
23
+ private
24
+ def setup_db(db)
25
+ db.results_as_hash = true
26
+ db.type_translation = true
27
+ end
28
+ end
29
+
30
+ @dbs = DbCollection.new
31
+
32
+ def self.open(filename, which=:default)
33
+ dbs[which] = SQLite3::Database.new(filename)
34
+ end
35
+
36
+ def self.close(which=:default)
37
+ if dbs[which]
38
+ dbs[which].close
39
+ dbs.delete(which)
40
+ end
41
+ end
42
+
43
+ def self.db
44
+ dbs[:default]
45
+ end
46
+
47
+ def self.db=(d)
48
+ dbs[:default] = d
49
+ end
50
+
51
+ def self.dbs
52
+ @dbs
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,122 @@
1
+ # Instance mixins for persisting classes. Part of Rhubarb.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ module Rhubarb
14
+
15
+ module Persisting
16
+ def self.included(other)
17
+ class << other
18
+ include PersistingClassMixins
19
+ end
20
+
21
+ other.class_eval do
22
+ attr_reader :row_id
23
+ attr_reader :created
24
+ attr_reader :updated
25
+ end
26
+ end
27
+
28
+ def db
29
+ self.class.db
30
+ end
31
+
32
+ # Returns true if the row backing this object has been deleted from the database
33
+ def deleted?
34
+ freshen
35
+ not @tuple
36
+ end
37
+
38
+ # Initializes a new instance backed by a tuple of values. Do not call this directly.
39
+ # Create new instances with the create or find methods.
40
+ def initialize(tup)
41
+ @backed = true
42
+ @tuple = tup
43
+ mark_fresh
44
+ @row_id = @tuple["row_id"]
45
+ @created = @tuple["created"]
46
+ @updated = @tuple["updated"]
47
+ resolve_referents
48
+ self.class.dirtied[@row_id] ||= @expired_after
49
+ self
50
+ end
51
+
52
+ # Deletes the row corresponding to this object from the database;
53
+ # invalidates =self= and any other objects backed by this row
54
+ def delete
55
+ self.db.execute("delete from #{self.class.table_name} where row_id = ?", @row_id)
56
+ mark_dirty
57
+ @tuple = nil
58
+ @row_id = nil
59
+ end
60
+
61
+ ## Begin private methods
62
+
63
+ private
64
+
65
+ # Fetches updated attribute values from the database if necessary
66
+ def freshen
67
+ if needs_refresh?
68
+ @tuple = self.class.find_tuple(@row_id)
69
+ if @tuple
70
+ @updated = @tuple["updated"]
71
+ else
72
+ @row_id = @updated = @created = nil
73
+ end
74
+ mark_fresh
75
+ resolve_referents
76
+ end
77
+ end
78
+
79
+ # True if the underlying row in the database is inconsistent with the state
80
+ # of this object, whether because the row has changed, or because this object has no row id
81
+ def needs_refresh?
82
+ if not @row_id
83
+ @tuple != nil
84
+ else
85
+ @expired_after < self.class.dirtied[@row_id]
86
+ end
87
+ end
88
+
89
+ # Mark this row as dirty so that any other objects backed by this row will
90
+ # update from the database before their attributes are inspected
91
+ def mark_dirty
92
+ self.class.dirtied[@row_id] = Util::timestamp
93
+ end
94
+
95
+ # Mark this row as consistent with the underlying database as of now
96
+ def mark_fresh
97
+ @expired_after = Util::timestamp
98
+ end
99
+
100
+ # Helper method to update the row in the database when one of our fields changes
101
+ def update(attr_name, value)
102
+ mark_dirty
103
+ self.db.execute("update #{self.class.table_name} set #{attr_name} = ?, updated = ? where row_id = ?", value, Util::timestamp, @row_id)
104
+ end
105
+
106
+ # Resolve any fields that reference other tables, replacing row ids with referred objects
107
+ def resolve_referents
108
+ refs = self.class.refs
109
+
110
+ refs.each do |c,r|
111
+ c = c.to_s
112
+ if r.referent == self.class and @tuple[c] == row_id
113
+ @tuple[c] = self
114
+ else
115
+ row = r.referent.find @tuple[c]
116
+ @tuple[c] = row if row
117
+ end
118
+ end
119
+ end
120
+
121
+ end
122
+ end
@@ -0,0 +1,39 @@
1
+ # Foreign-key abstraction for Rhubarb.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ module Rhubarb
14
+ class Reference
15
+ attr_reader :referent, :column, :options
16
+
17
+ # Creates a new Reference object, modeling a foreign-key relationship to another table. +klass+ is a class that includes Persisting; +options+ is a hash of options, which include
18
+ # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
19
+ # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
20
+ def initialize(klass, options={})
21
+ @referent = klass
22
+ @options = options
23
+ @options[:column] ||= "row_id"
24
+ @column = options[:column]
25
+ end
26
+
27
+ def to_s
28
+ trigger = ""
29
+ trigger = " on delete cascade" if options[:on_delete] == :cascade
30
+ "references #{@referent}(#{@column})#{trigger}"
31
+ end
32
+
33
+ def managed_ref?
34
+ # XXX?
35
+ return false if referent.class == String
36
+ referent.ancestors.include? Persisting
37
+ end
38
+ end
39
+ end
@@ -13,533 +13,10 @@
13
13
 
14
14
  require 'rubygems'
15
15
  require 'set'
16
- require 'time'
17
16
  require 'sqlite3'
18
-
19
- module Rhubarb
20
-
21
- module SQLBUtil
22
- def self.timestamp(tm=nil)
23
- tm ||= Time.now.utc
24
- (tm.tv_sec * 1000000) + tm.tv_usec
25
- end
26
- end
27
-
28
-
29
- module Persistence
30
- class DbCollection < Hash
31
- alias orig_set []=
32
-
33
- def []=(k,v)
34
- v.results_as_hash = true if v
35
- v.type_translation = true if v
36
- orig_set(k,v)
37
- end
38
- end
39
-
40
- @@dbs = DbCollection.new
41
-
42
- def self.open(filename, which=:default)
43
- dbs[which] = SQLite3::Database.new(filename)
44
- end
45
-
46
- def self.close(which=:default)
47
- if dbs[which]
48
- dbs[which].close
49
- dbs.delete(which)
50
- end
51
- end
52
-
53
- def self.db
54
- dbs[:default]
55
- end
56
-
57
- def self.db=(d)
58
- dbs[:default] = d
59
- end
60
-
61
- def self.dbs
62
- @@dbs
63
- end
64
- end
65
-
66
- class Column
67
- attr_reader :name
68
-
69
- def initialize(name, kind, quals)
70
- @name, @kind = name, kind
71
- @quals = quals.map {|x| x.to_s.gsub("_", " ") if x.class == Symbol}
72
- @quals.map
73
- end
74
-
75
- def to_s
76
- qualifiers = @quals.join(" ")
77
- if qualifiers == ""
78
- "#@name #@kind"
79
- else
80
- "#@name #@kind #{qualifiers}"
81
- end
82
- end
83
- end
84
-
85
- class Reference
86
- attr_reader :referent, :column, :options
87
-
88
- # Creates a new Reference object, modeling a foreign-key relationship to another table. +klass+ is a class that includes Persisting; +options+ is a hash of options, which include
89
- # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
90
- # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
91
- def initialize(klass, options={})
92
- @referent = klass
93
- @options = options
94
- @options[:column] ||= "row_id"
95
- @column = options[:column]
96
- end
97
-
98
- def to_s
99
- trigger = ""
100
- trigger = " on delete cascade" if options[:on_delete] == :cascade
101
- "references #{@referent}(#{@column})#{trigger}"
102
- end
103
-
104
- def managed_ref?
105
- # XXX?
106
- return false if referent.class == String
107
- referent.ancestors.include? Persisting
108
- end
109
- end
110
-
111
-
112
- # Methods mixed in to the class object of a persisting class
113
- module PersistingClassMixins
114
- # Returns the name of the database table modeled by this class.
115
- # Defaults to the name of the class (sans module names)
116
- def table_name
117
- @table_name ||= self.name.split("::").pop.downcase
118
- end
119
-
120
- # Enables setting the table name to a custom name
121
- def declare_table_name(nm)
122
- @table_name = nm
123
- end
124
-
125
- # Models a foreign-key relationship. +options+ is a hash of options, which include
126
- # +:column => + _name_:: specifies the name of the column to reference in +klass+ (defaults to row id)
127
- # +:on_delete => :cascade+:: specifies that deleting the referenced row in +klass+ will delete all rows referencing that row through this reference
128
- def references(table, options={})
129
- Reference.new(table, options)
130
- end
131
-
132
- # Models a CHECK constraint.
133
- def check(condition)
134
- "check (#{condition})"
135
- end
136
-
137
- # Returns an object corresponding to the row with the given ID, or +nil+ if no such row exists.
138
- def find(id)
139
- tup = self.find_tuple(id)
140
- return self.new(tup) if tup
141
- nil
142
- end
143
-
144
- alias find_by_id find
145
-
146
- def find_by(arg_hash)
147
- arg_hash = arg_hash.dup
148
- valid_cols = self.colnames.intersection arg_hash.keys
149
- select_criteria = valid_cols.map {|col| "#{col.to_s} = #{col.inspect}"}.join(" AND ")
150
- arg_hash.each {|k,v| arg_hash[k] = v.row_id if v.respond_to? :row_id}
151
-
152
- self.db.execute("select * from #{table_name} where #{select_criteria} order by row_id", arg_hash).map {|tup| self.new(tup) }
153
- end
154
-
155
- # args contains the following keys
156
- # * :group_by maps to a list of columns to group by (mandatory)
157
- # * :select_by maps to a hash mapping from column symbols to values (optional)
158
- # * :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
- def find_freshest(args)
160
- args = args.dup
161
-
162
- args[:version] ||= SQLBUtil::timestamp
163
- args[:select_by] ||= {}
164
-
165
- query_params = {}
166
- query_params[:version] = args[:version]
167
-
168
- select_clauses = ["created <= :version"]
169
-
170
- valid_cols = self.colnames.intersection args[:select_by].keys
171
-
172
- valid_cols.map do |col|
173
- select_clauses << "#{col.to_s} = #{col.inspect}"
174
- val = args[:select_by][col]
175
- val = val.row_id if val.respond_to? :row_id
176
- query_params[col] = val
177
- end
178
-
179
- group_by_clause = "GROUP BY " + args[:group_by].join(", ")
180
- where_clause = "WHERE " + select_clauses.join(" AND ")
181
- projection = self.colnames - [:created]
182
- join_clause = projection.map do |column|
183
- "__fresh.#{column} = __freshest.#{column}"
184
- end
185
-
186
- projection << "MAX(created) AS __current_version"
187
- join_clause << "__fresh.__current_version = __freshest.created"
188
-
189
- query = "
190
- SELECT __freshest.* FROM (
191
- SELECT #{projection.to_a.join(', ')} FROM (
192
- SELECT * from #{table_name} #{where_clause}
193
- ) #{group_by_clause}
194
- ) as __fresh INNER JOIN #{table_name} as __freshest ON
195
- #{join_clause.join(' AND ')}
196
- ORDER BY row_id
197
- "
198
-
199
- self.db.execute(query, query_params).map {|tup| self.new(tup) }
200
- end
201
-
202
- # Does what it says on the tin. Since this will allocate an object for each row, it isn't recomended for huge tables.
203
- def find_all
204
- self.db.execute("SELECT * from #{table_name}").map {|tup| self.new(tup)}
205
- end
206
-
207
- def delete_all
208
- self.db.execute("DELETE from #{table_name}")
209
- end
210
-
211
- # 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
- def declare_query(name, query)
213
- klass = (class << self; self end)
214
- klass.class_eval do
215
- define_method name.to_s do |*args|
216
- # handle reference parameters
217
- args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
218
-
219
- res = self.db.execute("select * from #{table_name} where #{query}", args)
220
- res.map {|row| self.new(row)}
221
- end
222
- end
223
- end
224
-
225
- # 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
- def declare_custom_query(name, query)
227
- klass = (class << self; self end)
228
- klass.class_eval do
229
- define_method name.to_s do |*args|
230
- # handle reference parameters
231
- args = args.map {|x| (x.row_id if x.class.ancestors.include? Persisting) or x}
232
-
233
- res = self.db.execute(query.gsub("__TABLE__", "#{self.table_name}"), args)
234
- # XXX: should freshen each row?
235
- res.map {|row| self.new(row) }
236
- end
237
- end
238
- end
239
-
240
- def declare_index_on(*fields)
241
- @creation_callbacks << Proc.new do
242
- idx_name = "idx_#{self.table_name}__#{fields.join('__')}__#{@creation_callbacks.size}"
243
- creation_cmd = "create index #{idx_name} on #{self.table_name} (#{fields.join(', ')})"
244
- self.db.execute(creation_cmd)
245
- end if fields.size > 0
246
- end
247
-
248
- # Adds a column named +cname+ to this table declaration, and adds the following methods to the class:
249
- # * accessors for +cname+, called +cname+ and +cname=+
250
- # * +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
251
- # 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
- def declare_column(cname, kind, *quals)
253
- ensure_accessors
254
-
255
- find_method_name = "find_by_#{cname}".to_sym
256
- find_first_method_name = "find_first_by_#{cname}".to_sym
257
-
258
- get_method_name = "#{cname}".to_sym
259
- set_method_name = "#{cname}=".to_sym
260
-
261
- # does this column reference another table?
262
- rf = quals.find {|q| q.class == Reference}
263
- if rf
264
- self.refs[cname] = rf
265
- end
266
-
267
- # add a find for this column (a class method)
268
- klass = (class << self; self end)
269
- klass.class_eval do
270
- define_method find_method_name do |arg|
271
- res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg)
272
- res.map {|row| self.new(row)}
273
- end
274
-
275
- define_method find_first_method_name do |arg|
276
- res = self.db.execute("select * from #{table_name} where #{cname} = ?", arg)
277
- return self.new(res[0]) if res.size > 0
278
- nil
279
- end
280
- end
281
-
282
- self.colnames.merge([cname])
283
- self.columns << Column.new(cname, kind, quals)
284
-
285
- # add accessors
286
- define_method get_method_name do
287
- freshen
288
- return @tuple["#{cname}"] if @tuple
289
- nil
290
- end
291
-
292
- if not rf
293
- define_method set_method_name do |arg|
294
- @tuple["#{cname}"] = arg
295
- update cname, arg
296
- end
297
- else
298
- # this column references another table; create a set
299
- # method that can handle either row objects or row IDs
300
- define_method set_method_name do |arg|
301
- freshen
302
-
303
- arg_id = nil
304
-
305
- if arg.class == Fixnum
306
- arg_id = arg
307
- arg = rf.referent.find arg_id
308
- else
309
- arg_id = arg.row_id
310
- end
311
- @tuple["#{cname}"] = arg
312
-
313
- update cname, arg_id
314
- end
315
-
316
- # Finally, add appropriate triggers to ensure referential integrity.
317
- # If rf has an on_delete trigger, also add the necessary
318
- # triggers to cascade deletes.
319
- # Note that we do not support update triggers, since the API does
320
- # not expose the capacity to change row IDs.
321
-
322
- self.creation_callbacks << Proc.new do
323
- @ccount ||= 0
324
-
325
- insert_trigger_name = "ri_insert_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}"
326
- delete_trigger_name = "ri_delete_#{self.table_name}_#{@ccount}_#{rf.referent.table_name}"
327
-
328
- 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;")
329
-
330
- 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.options[:on_delete] == :cascade
331
-
332
- @ccount = @ccount + 1
333
- end
334
- end
335
- end
336
-
337
- # Declares a constraint. Only check constraints are supported; see
338
- # the check method.
339
- def declare_constraint(cname, kind, *details)
340
- ensure_accessors
341
- info = details.join(" ")
342
- @constraints << "constraint #{cname} #{kind} #{info}"
343
- end
344
-
345
- # Creates a new row in the table with the supplied column values.
346
- # May throw a SQLite3::SQLException.
347
- def create(*args)
348
- new_row = args[0]
349
- new_row[:created] = new_row[:updated] = SQLBUtil::timestamp
350
-
351
- cols = colnames.intersection new_row.keys
352
- colspec = (cols.map {|col| col.to_s}).join(", ")
353
- valspec = (cols.map {|col| col.inspect}).join(", ")
354
- res = nil
355
-
356
- # resolve any references in the args
357
- new_row.each do |k,v|
358
- new_row[k] = v.row_id if v.class.ancestors.include? Persisting
359
- end
360
-
361
- self.db.transaction do |db|
362
- stmt = "insert into #{table_name} (#{colspec}) values (#{valspec})"
363
- # p stmt
364
- db.execute(stmt, new_row)
365
- res = find(db.last_insert_row_id)
366
- end
367
- res
368
- end
369
-
370
- # Returns a string consisting of the DDL statement to create a table
371
- # corresponding to this class.
372
- def table_decl
373
- cols = columns.join(", ")
374
- consts = constraints.join(", ")
375
- if consts.size > 0
376
- "create table #{table_name} (#{cols}, #{consts});"
377
- else
378
- "create table #{table_name} (#{cols});"
379
- end
380
- end
381
-
382
- # Creates a table in the database corresponding to this class.
383
- def create_table(dbkey=:default)
384
- self.db ||= Persistence::dbs[dbkey]
385
- self.db.execute(table_decl)
386
- @creation_callbacks.each {|func| func.call}
387
- end
388
-
389
- def db
390
- @db || Persistence::db
391
- end
392
-
393
- def db=(d)
394
- @db = d
395
- end
396
-
397
- # Ensure that all the necessary accessors on our class instance are defined
398
- # and that all metaclass fields have the appropriate values
399
- def ensure_accessors
400
- # Define singleton accessors
401
- if not self.respond_to? :columns
402
- class << self
403
- # Arrays of columns, column names, and column constraints.
404
- # Note that colnames does not contain id, created, or updated.
405
- # The API purposefully does not expose the ability to create a
406
- # row with a given id, and created and updated values are
407
- # maintained automatically by the API.
408
- attr_accessor :columns, :colnames, :constraints, :dirtied, :refs, :creation_callbacks
409
- end
410
- end
411
-
412
- # Ensure singleton fields are initialized
413
- self.columns ||= [Column.new(:row_id, :integer, [:primary_key]), Column.new(:created, :integer, []), Column.new(:updated, :integer, [])]
414
- self.colnames ||= Set.new [:created, :updated]
415
- self.constraints ||= []
416
- self.dirtied ||= {}
417
- self.refs ||= {}
418
- self.creation_callbacks ||= []
419
- end
420
-
421
- # Returns the number of rows in the table backing this class
422
- def count
423
- result = self.db.execute("select count(row_id) from #{table_name}")[0]
424
- result[0].to_i
425
- end
426
-
427
- def find_tuple(id)
428
- res = self.db.execute("select * from #{table_name} where row_id = ?", id)
429
- if res.size == 0
430
- nil
431
- else
432
- res[0]
433
- end
434
- end
435
- end
436
-
437
- module Persisting
438
- def self.included(other)
439
- class << other
440
- include PersistingClassMixins
441
- end
442
-
443
- other.class_eval do
444
- attr_reader :row_id
445
- attr_reader :created
446
- attr_reader :updated
447
- end
448
- end
449
-
450
- def db
451
- self.class.db
452
- end
453
-
454
- # Returns true if the row backing this object has been deleted from the database
455
- def deleted?
456
- freshen
457
- not @tuple
458
- end
459
-
460
- # Initializes a new instance backed by a tuple of values. Do not call this directly.
461
- # Create new instances with the create or find methods.
462
- def initialize(tup)
463
- @backed = true
464
- @tuple = tup
465
- mark_fresh
466
- @row_id = @tuple["row_id"]
467
- @created = @tuple["created"]
468
- @updated = @tuple["updated"]
469
- resolve_referents
470
- self.class.dirtied[@row_id] ||= @expired_after
471
- self
472
- end
473
-
474
- # Deletes the row corresponding to this object from the database;
475
- # invalidates =self= and any other objects backed by this row
476
- def delete
477
- self.db.execute("delete from #{self.class.table_name} where row_id = ?", @row_id)
478
- mark_dirty
479
- @tuple = nil
480
- @row_id = nil
481
- end
482
-
483
- ## Begin private methods
484
-
485
- private
486
-
487
- # Fetches updated attribute values from the database if necessary
488
- def freshen
489
- if needs_refresh?
490
- @tuple = self.class.find_tuple(@row_id)
491
- if @tuple
492
- @updated = @tuple["updated"]
493
- else
494
- @row_id = @updated = @created = nil
495
- end
496
- mark_fresh
497
- resolve_referents
498
- end
499
- end
500
-
501
- # True if the underlying row in the database is inconsistent with the state
502
- # of this object, whether because the row has changed, or because this object has no row id
503
- def needs_refresh?
504
- if not @row_id
505
- @tuple != nil
506
- else
507
- @expired_after < self.class.dirtied[@row_id]
508
- end
509
- end
510
-
511
- # Mark this row as dirty so that any other objects backed by this row will
512
- # update from the database before their attributes are inspected
513
- def mark_dirty
514
- self.class.dirtied[@row_id] = SQLBUtil::timestamp
515
- end
516
-
517
- # Mark this row as consistent with the underlying database as of now
518
- def mark_fresh
519
- @expired_after = SQLBUtil::timestamp
520
- end
521
-
522
- # Helper method to update the row in the database when one of our fields changes
523
- def update(attr_name, value)
524
- mark_dirty
525
- self.db.execute("update #{self.class.table_name} set #{attr_name} = ?, updated = ? where row_id = ?", value, SQLBUtil::timestamp, @row_id)
526
- end
527
-
528
- # Resolve any fields that reference other tables, replacing row ids with referred objects
529
- def resolve_referents
530
- refs = self.class.refs
531
-
532
- refs.each do |c,r|
533
- c = c.to_s
534
- if r.referent == self.class and @tuple[c] == row_id
535
- @tuple[c] = self
536
- else
537
- row = r.referent.find @tuple[c]
538
- @tuple[c] = row if row
539
- end
540
- end
541
- end
542
-
543
- end
544
-
545
- end
17
+ require 'rhubarb/util'
18
+ require 'rhubarb/persistence'
19
+ require 'rhubarb/column'
20
+ require 'rhubarb/reference'
21
+ require 'rhubarb/classmixins'
22
+ require 'rhubarb/persisting'
@@ -0,0 +1,28 @@
1
+ # Timestamp function for Rhubarb.
2
+ #
3
+ # Copyright (c) 2009--2010 Red Hat, Inc.
4
+ #
5
+ # Author: William Benton (willb@redhat.com)
6
+ #
7
+ # Licensed under the Apache License, Version 2.0 (the "License");
8
+ # you may not use this file except in compliance with the License.
9
+ # You may obtain a copy of the License at
10
+ #
11
+ # http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ require 'time'
14
+
15
+ module Rhubarb
16
+ module Util
17
+ # A higher-resolution timestamp
18
+ def self.timestamp(tm=nil)
19
+ tm ||= Time.now.utc
20
+ (tm.tv_sec * 1000000) + tm.tv_usec
21
+ end
22
+
23
+ # Identity for objects that may be used as foreign keys
24
+ def self.rhubarb_fk_identity(object)
25
+ (object.row_id if object.class.ancestors.include? Persisting) || object
26
+ end
27
+ end
28
+ end
data/ruby-rhubarb.spec ADDED
@@ -0,0 +1,60 @@
1
+ %{!?ruby_sitelib: %global ruby_sitelib %(ruby -rrbconfig -e 'puts Config::CONFIG["sitelibdir"] ')}
2
+ %define rel 0.3
3
+
4
+ Summary: Simple versioned object-graph persistence layer
5
+ Name: ruby-rhubarb
6
+ Version: 0.2.0
7
+ Release: %{rel}%{?dist}
8
+ Group: Applications/System
9
+ License: ASL 2.0
10
+ URL: http://git.fedorahosted.org/git/grid/rhubarb.git
11
+ Source0: %{name}-%{version}-%{rel}.tar.gz
12
+ BuildRoot: %(mktemp -ud %{_tmppath}/%{name}-%{version}-%{release}-XXXXXX)
13
+ Requires: ruby-sqlite3
14
+ Requires: ruby
15
+ Requires: ruby(abi) = 1.8
16
+ BuildRequires: ruby
17
+ BuildArch: noarch
18
+
19
+ %description
20
+ A simple versioned object-graph persistence layer that stores
21
+ instances of specially-declared Ruby classes in a SQLite3 database
22
+
23
+ %prep
24
+ %setup -q
25
+
26
+ %build
27
+
28
+ %install
29
+ rm -rf %{buildroot}
30
+ mkdir -p %{buildroot}/%{ruby_sitelib}/rhubarb
31
+ mkdir -p %{buildroot}/%{ruby_sitelib}/rhubarb/mixins
32
+ cp -f lib/rhubarb/mixins/*.rb %{buildroot}/%{ruby_sitelib}/rhubarb/mixins
33
+ cp -f lib/rhubarb/*.rb %{buildroot}/%{ruby_sitelib}/rhubarb
34
+
35
+ %clean
36
+ rm -rf %{buildroot}
37
+
38
+ %files
39
+ %defattr(-, root, root, -)
40
+ %doc LICENSE README.rdoc CHANGES TODO VERSION
41
+ %{ruby_sitelib}/rhubarb/rhubarb.rb
42
+ %{ruby_sitelib}/rhubarb/classmixins.rb
43
+ %{ruby_sitelib}/rhubarb/mixins/freshness.rb
44
+ %{ruby_sitelib}/rhubarb/column.rb
45
+ %{ruby_sitelib}/rhubarb/reference.rb
46
+ %{ruby_sitelib}/rhubarb/util.rb
47
+ %{ruby_sitelib}/rhubarb/persisting.rb
48
+ %{ruby_sitelib}/rhubarb/persistence.rb
49
+
50
+ %changelog
51
+ * Fri Feb 5 2010 <rrat@redhat> - 0.2.0-0.3
52
+ - Explicitly list files
53
+ - Added ruby(abi) and dependency
54
+ - Added ruby build dependency
55
+
56
+ * Thu Feb 4 2010 <willb@redhat.com> - 0.2.0-0.2
57
+ - Post-0.2 cleanups
58
+
59
+ * Tue Feb 2 2010 <rrati@redhat> - 0.2.0-0.1
60
+ - Initial package
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rhubarb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - William Benton
@@ -9,7 +9,7 @@ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
11
 
12
- date: 2010-02-02 00:00:00 -06:00
12
+ date: 2010-02-15 00:00:00 -06:00
13
13
  default_executable:
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
@@ -51,7 +51,15 @@ files:
51
51
  - Rakefile
52
52
  - TODO
53
53
  - VERSION
54
+ - lib/rhubarb/classmixins.rb
55
+ - lib/rhubarb/column.rb
56
+ - lib/rhubarb/mixins/freshness.rb
57
+ - lib/rhubarb/persistence.rb
58
+ - lib/rhubarb/persisting.rb
59
+ - lib/rhubarb/reference.rb
54
60
  - lib/rhubarb/rhubarb.rb
61
+ - lib/rhubarb/util.rb
62
+ - ruby-rhubarb.spec
55
63
  - test/helper.rb
56
64
  - test/test_rhubarb.rb
57
65
  has_rdoc: true