momomoto 0.1.13 → 0.1.14

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.
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