rhubarb 0.2.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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