momomoto 0.1.13 → 0.1.14

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. data/Rakefile +15 -0
  2. data/lib/momomoto/base.rb +77 -12
  3. data/lib/momomoto/database.rb +16 -4
  4. data/lib/momomoto/datatype/base.rb +49 -8
  5. data/lib/momomoto/datatype/bigint.rb +2 -1
  6. data/lib/momomoto/datatype/boolean.rb +9 -0
  7. data/lib/momomoto/datatype/bytea.rb +4 -0
  8. data/lib/momomoto/datatype/character.rb +2 -0
  9. data/lib/momomoto/datatype/character_varying.rb +2 -0
  10. data/lib/momomoto/datatype/date.rb +5 -0
  11. data/lib/momomoto/datatype/inet.rb +4 -0
  12. data/lib/momomoto/datatype/integer.rb +5 -0
  13. data/lib/momomoto/datatype/interval.rb +14 -1
  14. data/lib/momomoto/datatype/numeric.rb +5 -0
  15. data/lib/momomoto/datatype/real.rb +2 -0
  16. data/lib/momomoto/datatype/smallint.rb +2 -0
  17. data/lib/momomoto/datatype/text.rb +14 -0
  18. data/lib/momomoto/datatype/time_with_time_zone.rb +3 -1
  19. data/lib/momomoto/datatype/time_without_time_zone.rb +10 -0
  20. data/lib/momomoto/datatype/timestamp_with_time_zone.rb +2 -0
  21. data/lib/momomoto/datatype/timestamp_without_time_zone.rb +5 -0
  22. data/lib/momomoto/information_schema/columns.rb +8 -0
  23. data/lib/momomoto/information_schema/fetch_procedure_columns.rb +2 -0
  24. data/lib/momomoto/information_schema/fetch_procedure_parameters.rb +2 -0
  25. data/lib/momomoto/information_schema/key_column_usage.rb +5 -0
  26. data/lib/momomoto/information_schema/routines.rb +4 -0
  27. data/lib/momomoto/information_schema/table_constraints.rb +5 -0
  28. data/lib/momomoto/order.rb +61 -0
  29. data/lib/momomoto/procedure.rb +14 -14
  30. data/lib/momomoto/row.rb +41 -4
  31. data/lib/momomoto/table.rb +106 -20
  32. data/lib/timeinterval.rb +90 -16
  33. data/test/test_datatype.rb +1 -1
  34. metadata +2 -2
data/lib/momomoto/row.rb CHANGED
@@ -1,62 +1,86 @@
1
1
 
2
2
  module Momomoto
3
- # base class for all Rows
3
+
4
+ # Base class for all Rows.
4
5
  class Row
5
6
 
6
- # undefing fields to avoid conflicts
7
+ # undefining fields to avoid conflicts
7
8
  undef :id,:type
8
9
 
10
+ # Getter for the table this Row is using.
9
11
  def self.table
10
12
  @table
11
13
  end
12
14
 
15
+ # Getter for the columns of this Row's table.
13
16
  def self.columns
14
17
  @columns
15
18
  end
16
19
 
20
+ # Getter for the order of columns. The order is built from
21
+ # +columns+.keys.
17
22
  def self.column_order
18
23
  @column_order
19
24
  end
20
25
 
26
+ # Gets the value of a given +fieldname+.
27
+ #
28
+ # feed = Feeds.select( {:url => 'https://www.c3d2.de/news-atom.xml' )[0]
29
+ # feed[:url] == 'https://www.c3d2.de/news-atom.xml'
30
+ # => true
31
+ #
21
32
  def []( fieldname )
22
33
  get_column( fieldname )
23
34
  end
24
35
 
36
+ # Sets +fieldname+ to +value+.
37
+ #
38
+ # feed = Feeds.select( {:url => 'http://www.c3d2.de/news-atom.xml' )[0]
39
+ # feed[:url_host] = 'https://www.c3d2.de/'
25
40
  def []=( fieldname, value )
26
41
  set_column( fieldname, value )
27
42
  end
28
43
 
44
+ # Compares +@data+ value of the row with +other+.
29
45
  def ==( other )
30
46
  @data == other.instance_variable_get( :@data )
31
47
  end
32
48
 
49
+ # Getter for +@dirty+ which holds all changed fields of a row.
33
50
  def dirty
34
51
  @dirty
35
52
  end
36
53
 
54
+ # Returns true if there are fields in +@dirty+.
37
55
  def dirty?
38
56
  @dirty.length > 0
39
57
  end
40
58
 
59
+ # Marks a field as dirty.
41
60
  def mark_dirty( field )
42
61
  field = field.to_sym
43
62
  @dirty.push( field ) if not @dirty.member?( field )
44
63
  end
45
64
 
65
+ # Removes all fields from +dirty+.
46
66
  def clean_dirty
47
67
  @dirty = []
48
68
  end
49
69
 
70
+ # Creates a new Row instance.
50
71
  def initialize( data = [] )
51
72
  @data = data
52
73
  @new_record = false
53
74
  clean_dirty
54
75
  end
55
76
 
77
+ # Returns true if the row is newly created.
56
78
  def new_record?
57
79
  @new_record
58
80
  end
59
81
 
82
+ # Sets +@new_record+ to +true+
83
+ # or +false+.
60
84
  def new_record=( value )
61
85
  @new_record = !!value
62
86
  end
@@ -71,7 +95,7 @@ module Momomoto
71
95
  self.class.table.delete( self )
72
96
  end
73
97
 
74
- # convert row to hash
98
+ # converts row to hash
75
99
  def to_hash
76
100
  hash = {}
77
101
  self.class.columns.keys.each do | key |
@@ -80,7 +104,20 @@ module Momomoto
80
104
  hash
81
105
  end
82
106
 
83
- # generic setter for column values
107
+ # Generic setter for column values. You should use this method when
108
+ # you are defining your own setter. This is useful for preprocessing +value+
109
+ # before it is written to database:
110
+ #
111
+ # class Person < Momomoto::Table
112
+ # module Methods
113
+ # def nickname=( value )
114
+ # set_column( :nickname, value.downcase )
115
+ # end
116
+ # end
117
+ # end
118
+ #
119
+ # This defines a custom setter nickname= which invokes downcase
120
+ # on the given +value+.
84
121
  def set_column( column, value )
85
122
  raise "Unknown column #{column}" if not self.class.column_order.member?( column.to_sym )
86
123
  table = self.class.table
@@ -1,8 +1,8 @@
1
1
 
2
2
  module Momomoto
3
3
 
4
- # this class implements access to tables/views
5
- # it must not be used directly but you should inherit from this class
4
+ # This class implements access to tables/views.
5
+ # It must not be used directly but you should inherit from this class.
6
6
  class Table < Base
7
7
 
8
8
  class << self
@@ -43,7 +43,7 @@ module Momomoto
43
43
  @table_name
44
44
  end
45
45
 
46
- # get the full name of a table including schema if set
46
+ # get the full name of table including, if set, schema
47
47
  def full_name
48
48
  "#{ schema_name ? schema_name + '.' : ''}#{table_name}"
49
49
  end
@@ -62,7 +62,64 @@ module Momomoto
62
62
  @primary_keys
63
63
  end
64
64
 
65
- # Searches for records and returns an array containing the records
65
+ # Searches for records and returns an Array containing the records.
66
+ # There are a bunch of different use cases as this method is the primary way
67
+ # to access all rows in the database.
68
+ #
69
+ # Selecting rows based on expression:
70
+ # #selects the feeds that match both the given url and author fields
71
+ # Posts.select(:feed_url => "https://www.c3d2.de/news-atom.xml",:author => "fnord")
72
+ #
73
+ # Using order statements:
74
+ # See Order#asc, Order#desc and Order#lower
75
+ #
76
+ # #Selects conferences depending on start_date, starting with the oldest date.
77
+ # #If two conferences start at the same date(day) use the second order parameter
78
+ # #start_time.
79
+ # Conference.select({},{:order => Momomoto.asc([:start_date,:start_time])} )
80
+ #
81
+ # Using limit statement:
82
+ # See Base#compile_limit
83
+ #
84
+ # #selects five feeds
85
+ # five_feeds = Feeds.select( {},{:limit => 5} )
86
+ #
87
+ # Using offset statement:
88
+ # See Base#compile_offset
89
+ #
90
+ # #selects five feeds ommitting the first 23 rows
91
+ # five_feeds = Feeds.select( {}, {:offset => 23, :limit => 5} )
92
+ #
93
+ # Using logical operators:
94
+ # See Datatype::Base#operator_sign for basic comparison operators
95
+ # See Base#logical_operator for the supported logical operators
96
+ #
97
+ # #selects the posts where the content field case-insensitevely matches
98
+ # #"surveillance".
99
+ # Posts.select( :content => {:ilike => 'surveillance'} )
100
+ #
101
+ # #selects all conferences with a start_date before the current time.
102
+ # Conferences.select( :start_date => {:le => Time.now} )
103
+ #
104
+ # feed1 = "https://www.c3d2.de/news-atom.xml"
105
+ # feed2 = "http://www.c3d2.de/news-atom.xml"
106
+ # #selects the feeds with a field url that matches either feed1 or feed2
107
+ # Feeds.select( :OR=>{:url => [feed1,feed2]} )
108
+ #
109
+ #
110
+ # Selecting only given columns:
111
+ # See Base#initialize_row for the implementation
112
+ #
113
+ # #selects title and content for every row found in table Posts.
114
+ # posts = Posts.select({},{:columns => [:title,:content]} )
115
+ #
116
+ # The returned rows are special. They do not contain getter and setter
117
+ # for the rest of the columns of the row. Only the specified columns
118
+ # and all the primary keys of the table have proper accessor methods.
119
+ #
120
+ # However, you can still change the rows and write them back to database:
121
+ # posts.first.title = "new title"
122
+ # posts.first.write
66
123
  def select( conditions = {}, options = {} )
67
124
  initialize_table unless initialized
68
125
  row_class = build_row_class( options )
@@ -74,6 +131,8 @@ module Momomoto
74
131
  data
75
132
  end
76
133
 
134
+ # experimental
135
+ #
77
136
  # Searches for records and returns an array containing the records
78
137
  def select_outer_join( conditions = {}, options = {} )
79
138
  initialize_table unless initialized
@@ -131,10 +190,14 @@ module Momomoto
131
190
  new_row
132
191
  end
133
192
 
134
- # Tries to find a specific record and creates a new one if it does not find it.
193
+ # Tries to select the specified record or creates a new one if it does not find it.
135
194
  # Raises an exception if multiple records are found.
136
195
  # You can pass a block which has to deliver the respective values for the
137
- # primary key fields
196
+ # primary key fields.
197
+ #
198
+ # # selects the feed row matching the specified URL or creates a new
199
+ # # row based on the given URL.
200
+ # Feeds.select_or_new( :url => "https://www.c3d2.de/news-atom.xml" )
138
201
  def select_or_new( conditions = {}, options = {} )
139
202
  begin
140
203
  if block_given?
@@ -157,19 +220,19 @@ module Momomoto
157
220
  end
158
221
 
159
222
  # Select a single row from the database
160
- # raises Momomoto::Nothing_found if nothing was found, raises
161
- # Momomoto::Too_many_records if more than one record was found.
223
+ # raises Momomoto::Nothing_found if no row matched.
224
+ # raises Momomoto::Too_many_records if more than one record was found.
162
225
  def select_single( conditions = {}, options = {} )
163
226
  data = select( conditions, options )
164
227
  case data.length
165
- when 0 then raise Nothing_found, "nothing found in #{table_name}"
228
+ when 0 then raise Nothing_found, "nothing found in #{full_name}"
166
229
  when 1 then return data[0]
167
- else raise Too_many_records, "too many records found in #{table_name}"
230
+ else raise Too_many_records, "too many records found in #{full_name}"
168
231
  end
169
232
  end
170
233
 
171
- # write row back to database
172
- # this function is called by Momomoto::Row#write
234
+ # Writes row back to database.
235
+ # This method is called by Momomoto::Row#write
173
236
  def write( row ) # :nodoc:
174
237
  if row.new_record?
175
238
  insert( row )
@@ -181,8 +244,8 @@ module Momomoto
181
244
  true
182
245
  end
183
246
 
184
- # create an insert statement for a row
185
- # do not call insert directly use row.write or Table.write( row ) instead
247
+ # Creates an insert statement for a row.
248
+ # Do not use it directly but use row.write or Table.write(row) instead.
186
249
  def insert( row )
187
250
  fields, values = [], []
188
251
  columns.each do | field_name, datatype |
@@ -205,8 +268,21 @@ module Momomoto
205
268
  database.execute( sql )
206
269
  end
207
270
 
208
- # create an update statement for a row
209
- # do not call update directly use row.write or Table.write( row ) instead
271
+ # Creates an update statement for a row.
272
+ # Do not call update directly but use row.write or Table.write(row) instead.
273
+ #
274
+ # Get the value of row.new_record? before writing to database to find out
275
+ # if you are updating a row that already exists in the database.
276
+ #
277
+ # feed = Feeds.select_single( :url => "http://www.c3d2.de/news-atom.xml" )
278
+ # feed.new_record? => false
279
+ # feed[:url] = "https://www.c3d2.de/news-atom.xml"
280
+ # feed.new_record? => false
281
+ #
282
+ # feed = Feeds.select_or_new( :url => "http://astroblog.spaceboyz.net/atom.rb" )
283
+ # feed.new_record? => true
284
+ # feed.write => true
285
+ # feed.new_record? => false
210
286
  def update( row )
211
287
  raise CriticalError, 'Updating is only allowed for tables with primary keys' if primary_keys.empty?
212
288
  setter, conditions = [], {}
@@ -222,7 +298,7 @@ module Momomoto
222
298
  database.execute( sql )
223
299
  end
224
300
 
225
- # delete _row_ from the database
301
+ # delete _row_ from table
226
302
  def delete( row )
227
303
  raise CriticalError, 'Deleting is only allowed for tables with primary keys' if primary_keys.empty?
228
304
  raise Error, "this is a new record" if row.new_record?
@@ -243,7 +319,7 @@ module Momomoto
243
319
  classname.split('::').last.downcase.gsub(/[^a-z_0-9]/, '')
244
320
  end
245
321
 
246
- # initialie a table class
322
+ # initializes a table class
247
323
  def initialize_table
248
324
 
249
325
  @table_name ||= construct_table_name( self.name )
@@ -266,7 +342,17 @@ module Momomoto
266
342
 
267
343
  end
268
344
 
269
- # builds the row class for this table
345
+ # Builds the row class for this table when executing #select.
346
+ # In the default Row class proper setter and getter are available
347
+ # for all columns. However, if you only want to select a few
348
+ # columns as in
349
+ #
350
+ # Feeds.select( {}, {:columns => [:url,:last_changed]} )
351
+ #
352
+ # #build_row_class does not return the default Row class for this table
353
+ # but invokes Base#initialize_row to create a new class without
354
+ # the setter and getter for the unused columns.
355
+ # For better performance the newly created class is cached in +@row_cache+.
270
356
  def build_row_class( options )
271
357
  if options[:columns]
272
358
  options[:columns] += primary_keys
@@ -286,7 +372,7 @@ module Momomoto
286
372
  end
287
373
  end
288
374
 
289
- # compile the select clause
375
+ # compiles the select clause
290
376
  def compile_select( conditions, options )
291
377
  if options[:columns]
292
378
  cols = {}
data/lib/timeinterval.rb CHANGED
@@ -1,31 +1,71 @@
1
1
 
2
2
  require 'date'
3
3
 
4
- # the class is used in Momomoto to represent the SQL interval datatype
4
+ # The class is used in Momomoto to represent time intervals.
5
5
  class TimeInterval
6
6
 
7
7
  include Comparable
8
8
 
9
+ # Is raised if a String cannot be converted to some representation of
10
+ # TimeInterval.
11
+ #
12
+ # TimeInterval.new( {:hour => '42', :min => '23'} )
13
+ # => #<TimeInterval:0x505c565c @hour="42", @sec=0, @min="23">
14
+ # TimeInterval.new('invalid')
15
+ # => TimeInterval::ParseError: Could not parse interval 'invalid'
9
16
  class ParseError < StandardError; end
10
17
 
18
+ # Getter methods for hours, minutes and seconds
19
+ #
20
+ # interval = TimeInterval.new( {:hour => 42, :min => 23} )
21
+ # time.hour => 42
22
+ # time.min => 23
23
+ # time.sec => 0
11
24
  attr_reader :hour, :min, :sec
12
25
 
13
26
  class << self
14
27
 
28
+ # Creates and returns a new instance of TimeInterval from the given +interval+
29
+ # interval = TimeInterval.new( "00:23" )
30
+ # => #<TimeInterval:0x5235d458 @hour=0, @sec=0, @min=23>
15
31
  def parse( interval )
16
32
  TimeInterval.new( interval )
17
33
  end
18
34
 
19
35
  end
20
36
 
21
- # compare two TimeInterval instances
22
- # the comparison is done by calling to_i on other
37
+ # Compares two TimeInterval instances by converting into seconds and
38
+ # applying #<=> to them.
39
+ # Returns 1 (first > last), 0 (first == last) or -1 (first < last).
40
+ #
41
+ # i1 = TimeInterval.new( {:hour => 42, :min => 23, :sec => 2} )
42
+ # i1.to_i => 152582
43
+ # i2 = TimeInterval.new( {:hour => 5, :min => 23, :sec => 2} )
44
+ # i2.to_i => 19382
45
+ #
46
+ # i1 <=> i2 #=> 1
47
+ #
23
48
  def <=>( other )
24
49
  self.to_i <=> other.to_i
25
50
  end
26
51
 
27
- # formats timeinterval according to the directives in the give format
28
- # string
52
+ # add to another TimeInterval instance
53
+ def +( other )
54
+ self.class.new( self.to_i + other.to_i )
55
+ end
56
+
57
+ # subtract something to a TimeInterval instance
58
+ def -( other )
59
+ self.class.new( self.to_i - other.to_i )
60
+ end
61
+
62
+ # Formats time interval according to the directives in the given format
63
+ # string.
64
+ #
65
+ # i = TimeInterval.new(2342) #2342 seconds
66
+ # i.strftime( "%H" ) => "00"
67
+ # i.strftime( "%M" ) => "39"
68
+ # i.strftime( "%S" ) => "02"
29
69
  def strftime( fmt = "%H:%M:%S" )
30
70
  fmt.gsub( /%(.)/ ) do | match |
31
71
  case match[1,1]
@@ -38,38 +78,72 @@ class TimeInterval
38
78
  end
39
79
  end
40
80
 
41
- # returns the value of timeinterval as number of seconds
81
+ # Returns the value of time interval as number of seconds
42
82
  def to_i
43
83
  @hour * 3600 + @min * 60 + @sec
44
84
  end
45
85
 
46
86
  alias_method :to_int, :to_i
47
87
 
48
- # Returns a string representing timeinterval. Equivalent to calling
88
+ # Returns the value of time interval as number of seconds
89
+ # Time.now + TimeInterval.new(2342)
90
+ # => Tue Dec 11 21:16:06 +0100 2007
91
+ def to_f
92
+ self.to_i.to_f
93
+ end
94
+
95
+ # Returns a string representing time interval. Equivalent to calling
49
96
  # Time#strftime with a format string of '%H:%M:%S'.
97
+ #
98
+ # i.inspect => "#<TimeInterval:0x517e36b8 @hour=0, @sec=2, @min=39>"
99
+ # i.to_s => "00:39:02"
100
+ # i.strftime => "00:39:02"
50
101
  def to_s
51
102
  strftime( '%H:%M:%S' )
52
103
  end
53
104
 
105
+ # Creates a new instance of TimeInterval with +d+ representing either
106
+ # a value of type #Hash
107
+ #
108
+ # TimeInterval.new( {:hour => 5}),
109
+ #
110
+ # a value of type #Integer in seconds
111
+ #
112
+ # TimeInterval.new( 23 ),
113
+ #
114
+ # or of type #String
115
+ #
116
+ # TimeInterval.new( "00:23" ).
117
+ #
118
+ # Use getter methods #hour, #min and #sec in your code.
54
119
  def initialize( d = {} )
55
120
  case d
56
121
  when Hash then
57
- @hour = d[:hour] || 0
58
- @min = d[:min] || 0
59
- @sec = d[:sec] || 0
122
+ init_from_hash( d )
60
123
  when Integer then
61
- @hour = d/3600
62
- @min = (d/60)%60
63
- @sec = d%60
124
+ init_from_int( d )
64
125
  when String then
65
126
  parsed = Date._parse( d, false)
66
127
  if ( parsed.empty? && d.length > 0 ) || !(parsed.keys - [:hour,:min,:sec,:sec_fraction]).empty?
67
128
  raise ParseError, "Could not parse interval `#{d}`"
68
129
  end
69
- @hour = parsed[:hour] || 0
70
- @min = parsed[:min] || 0
71
- @sec = parsed[:sec] || 0
130
+ init_from_hash( parsed )
72
131
  end
132
+
133
+ end
134
+
135
+ protected
136
+
137
+ def init_from_hash( d ) #:nodoc:
138
+ @hour = Integer( d[:hour] || 0 )
139
+ @min = Integer( d[:min] || 0 )
140
+ @sec = Integer( d[:sec] || 0 )
141
+ end
142
+
143
+ def init_from_int( d ) #:nodoc:
144
+ @hour = d/3600
145
+ @min = (d/60)%60
146
+ @sec = d%60
73
147
  end
74
148
 
75
149
  end