momomoto 0.1.0

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 (65) hide show
  1. data/LICENSE +340 -0
  2. data/Rakefile +38 -0
  3. data/lib/momomoto.rb +5 -0
  4. data/lib/momomoto/base.rb +162 -0
  5. data/lib/momomoto/database.rb +179 -0
  6. data/lib/momomoto/datatype.rb +20 -0
  7. data/lib/momomoto/datatype/base.rb +78 -0
  8. data/lib/momomoto/datatype/bigint.rb +9 -0
  9. data/lib/momomoto/datatype/boolean.rb +23 -0
  10. data/lib/momomoto/datatype/bytea.rb +13 -0
  11. data/lib/momomoto/datatype/character.rb +7 -0
  12. data/lib/momomoto/datatype/character_varying.rb +7 -0
  13. data/lib/momomoto/datatype/date.rb +22 -0
  14. data/lib/momomoto/datatype/inet.rb +14 -0
  15. data/lib/momomoto/datatype/integer.rb +16 -0
  16. data/lib/momomoto/datatype/interval.rb +30 -0
  17. data/lib/momomoto/datatype/numeric.rb +17 -0
  18. data/lib/momomoto/datatype/real.rb +7 -0
  19. data/lib/momomoto/datatype/smallint.rb +7 -0
  20. data/lib/momomoto/datatype/text.rb +24 -0
  21. data/lib/momomoto/datatype/time_with_time_zone.rb +10 -0
  22. data/lib/momomoto/datatype/time_without_time_zone.rb +30 -0
  23. data/lib/momomoto/datatype/timestamp_with_time_zone.rb +7 -0
  24. data/lib/momomoto/datatype/timestamp_without_time_zone.rb +29 -0
  25. data/lib/momomoto/information_schema/columns.rb +45 -0
  26. data/lib/momomoto/information_schema/fetch_procedure_columns.rb +14 -0
  27. data/lib/momomoto/information_schema/fetch_procedure_parameters.rb +14 -0
  28. data/lib/momomoto/information_schema/key_column_usage.rb +19 -0
  29. data/lib/momomoto/information_schema/routines.rb +12 -0
  30. data/lib/momomoto/information_schema/table_constraints.rb +19 -0
  31. data/lib/momomoto/join.rb +66 -0
  32. data/lib/momomoto/momomoto.rb +10 -0
  33. data/lib/momomoto/order.rb +56 -0
  34. data/lib/momomoto/procedure.rb +129 -0
  35. data/lib/momomoto/row.rb +63 -0
  36. data/lib/momomoto/table.rb +251 -0
  37. data/sql/install.sql +10 -0
  38. data/sql/procedures.sql +54 -0
  39. data/sql/types.sql +11 -0
  40. data/test/test_base.rb +17 -0
  41. data/test/test_bigint.rb +26 -0
  42. data/test/test_boolean.rb +30 -0
  43. data/test/test_bytea.rb +35 -0
  44. data/test/test_character.rb +27 -0
  45. data/test/test_character_varying.rb +17 -0
  46. data/test/test_database.rb +63 -0
  47. data/test/test_datatype.rb +62 -0
  48. data/test/test_date.rb +50 -0
  49. data/test/test_inet.rb +27 -0
  50. data/test/test_information_schema.rb +27 -0
  51. data/test/test_integer.rb +37 -0
  52. data/test/test_interval.rb +38 -0
  53. data/test/test_join.rb +19 -0
  54. data/test/test_numeric.rb +30 -0
  55. data/test/test_procedure.rb +75 -0
  56. data/test/test_real.rb +17 -0
  57. data/test/test_row.rb +47 -0
  58. data/test/test_smallint.rb +26 -0
  59. data/test/test_table.rb +233 -0
  60. data/test/test_text.rb +25 -0
  61. data/test/test_time_with_time_zone.rb +17 -0
  62. data/test/test_time_without_time_zone.rb +40 -0
  63. data/test/test_timestamp_with_time_zone.rb +17 -0
  64. data/test/test_timestamp_without_time_zone.rb +28 -0
  65. metadata +116 -0
@@ -0,0 +1,63 @@
1
+
2
+ module Momomoto
3
+ # base class for all Rows
4
+ class Row
5
+
6
+ # undefing fields to avoid conflicts
7
+ undef :id,:type
8
+
9
+ def self.table
10
+ class_variable_get( :@@table )
11
+ end
12
+
13
+ def []( fieldname )
14
+ send( fieldname )
15
+ end
16
+
17
+ def []=( fieldname, value )
18
+ send( fieldname.to_s + '=', value )
19
+ end
20
+
21
+ def dirty?
22
+ @dirty
23
+ end
24
+
25
+ def dirty=( value )
26
+ @dirty = !!value
27
+ end
28
+
29
+ def initialize( data = [] )
30
+ @data = data
31
+ @new_record = false
32
+ @dirty = false
33
+ end
34
+
35
+ def new_record?
36
+ @new_record
37
+ end
38
+
39
+ def new_record=( value )
40
+ @new_record = !!value
41
+ end
42
+
43
+ # write the row to the database
44
+ def write
45
+ self.class.table.write( self )
46
+ end
47
+
48
+ # delete the row
49
+ def delete
50
+ self.class.table.delete( self )
51
+ end
52
+
53
+ def to_hash
54
+ hash = {}
55
+ self.class.table.columns.keys.each do | key |
56
+ hash[key] = self[key]
57
+ end
58
+ hash
59
+ end
60
+
61
+ end
62
+
63
+ end
@@ -0,0 +1,251 @@
1
+
2
+ module Momomoto
3
+
4
+ # this class implements access to tables/views
5
+ # it must not be used directly but you should inherit from this class
6
+ class Table < Base
7
+
8
+ class << self
9
+
10
+ # set the default order for selects
11
+ def default_order=( order )
12
+ @default_order = order
13
+ end
14
+
15
+ # get/set the default order for selects
16
+ def default_order( order = nil )
17
+ return self.default_order=( order ) if order
18
+ @default_order
19
+ end
20
+
21
+ # set the columns of the table this class operates on
22
+ def columns=( columns )
23
+ class_variable_set( :@@columns, columns)
24
+ end
25
+
26
+ # get the columns of the table this class operates on
27
+ def columns( columns = nil )
28
+ return self.columns=( columns ) if columns
29
+ begin
30
+ class_variable_get( :@@columns )
31
+ rescue NameError
32
+ initialize_table
33
+ class_variable_get( :@@columns )
34
+ end
35
+ end
36
+
37
+ def initialize_table # :nodoc:
38
+
39
+ unless class_variables.member?( '@@table_name' )
40
+ table_name( construct_table_name( self.name ) )
41
+ end
42
+
43
+ unless class_variables.member?( '@@schema_name' )
44
+ schema_name( construct_schema_name( self.name ) )
45
+ end
46
+
47
+ unless class_variables.member?( '@@columns' )
48
+ columns( database.fetch_table_columns( table_name(), schema_name() ) )
49
+ end
50
+ raise CriticalError, "No fields in table #{table_name}" if columns.keys.empty?
51
+
52
+ unless class_variables.member?( '@@primary_keys' )
53
+ primary_keys( database.fetch_primary_keys( table_name(), schema_name() ) )
54
+ end
55
+
56
+ const_set( :Row, Class.new( Momomoto::Row ) ) if not const_defined?( :Row )
57
+ initialize_row( const_get( :Row ), self )
58
+
59
+ # mark class as initialized
60
+ class_variable_set( :@@initialized, true)
61
+
62
+ end
63
+
64
+ # guesses the table name of the table this class works on
65
+ def construct_table_name( classname ) # :nodoc:
66
+ classname.split('::').last.downcase.gsub(/[^a-z_0-9]/, '')
67
+ end
68
+
69
+ # set the table_name of the table this class operates on
70
+ def table_name=( table_name )
71
+ class_variable_set( :@@table_name, table_name )
72
+ end
73
+
74
+ # get the table_name of the table this class operates on
75
+ def table_name( table_name = nil )
76
+ return self.table_name=( table_name ) if table_name
77
+ begin
78
+ class_variable_get( :@@table_name )
79
+ rescue NameError
80
+ construct_table_name( self.name )
81
+ end
82
+ end
83
+
84
+ # get the full name of a table including schema if set
85
+ def full_name
86
+ "#{ schema_name ? schema_name + '.' : ''}#{table_name}"
87
+ end
88
+
89
+ # set the primary key fields of the table
90
+ def primary_keys=( keys ) # :nodoc:
91
+ class_variable_set( :@@primary_keys, keys )
92
+ end
93
+
94
+ # get the primary key fields of the table
95
+ def primary_keys( keys = nil )
96
+ return self.primary_keys=( keys ) if keys
97
+ begin
98
+ class_variable_get( :@@primary_keys )
99
+ rescue
100
+ self.primary_keys=( database.fetch_primary_keys( table_name(), schema_name()) )
101
+ end
102
+ end
103
+
104
+ ## Searches for records and returns an array containing the records
105
+ def select( conditions = {}, options = {} )
106
+ initialize_table unless class_variables.member?('@@initialized')
107
+ sql = "SELECT " + columns.keys.map{ | field | '"' + field.to_s + '"' }.join( "," ) + " FROM "
108
+ sql += full_name
109
+ sql += compile_where( conditions )
110
+ sql += compile_order( options[:order] ) if options[:order] || default_order
111
+ sql += compile_limit( options[:limit] ) if options[:limit]
112
+ sql += compile_offset( options[:offset] ) if options[:offset]
113
+ data = []
114
+ database.execute( sql ).each do | row |
115
+ data << const_get(:Row).new( row )
116
+ end
117
+ data
118
+ end
119
+
120
+ ## constructor for a record in this table accepts a hash with presets for the fields of the record
121
+ def new( fields = {} )
122
+ initialize_table unless class_variables.member?('@@initialized')
123
+ new_row = const_get(:Row).new( [] )
124
+ new_row.new_record = true
125
+ # set default values
126
+ columns.each do | key, value |
127
+ next if primary_keys.member?( key )
128
+ if value.default
129
+ if value.default.match( /^\d+$/ )
130
+ new_row[ key ] = value.default
131
+ elsif value.default == "true"
132
+ new_row[ key ] = true
133
+ elsif value.default == "false"
134
+ new_row[ key ] = false
135
+ elsif m = value.default.match( /^'([^']+)'::(text|interval|timestamp with time(out)? zone|time with(out)? time zone)$/ )
136
+ new_row[ key ] = m[1]
137
+ end
138
+ end
139
+ end
140
+ fields.each do | key, value |
141
+ new_row[ key ] = value
142
+ end
143
+ new_row
144
+ end
145
+
146
+ ## Tries to find a specific record and creates a new one if it does not find it
147
+ # raises an exception if multiple records are found
148
+ # You can pass a block which has to deliver the respective values for the
149
+ # primary key fields
150
+ def select_or_new( conditions = {}, options = {} )
151
+ begin
152
+ if block_given?
153
+ conditions = conditions.dup
154
+ primary_keys.each do | field |
155
+ conditions[ field ] = yield( field ) if not conditions[ field ]
156
+ raise ConversionError if not conditions[ field ]
157
+ end
158
+ end
159
+ rows = select( conditions, options )
160
+ rescue ConversionError
161
+ end
162
+ if rows && rows.length > 1
163
+ raise Too_many_records, "Multiple values found in select_or_new for #{self}:#{conditions.inspect}"
164
+ elsif rows && rows.length == 1
165
+ rows.first
166
+ else
167
+ new( options[:copy_values] != false ? conditions : {} )
168
+ end
169
+ end
170
+
171
+ ## Select a single row from the database, raises an exception if more or zero
172
+ # rows are found
173
+ def select_single( conditions = {}, options = {} )
174
+ data = select( conditions, options )
175
+ case data.length
176
+ when 0 then raise Nothing_found, "nothing found in #{table_name}"
177
+ when 1 then return data[0]
178
+ else raise Too_many_records, "too many records found in #{table_name}"
179
+ end
180
+ end
181
+
182
+ # write row back to database
183
+ def write( row ) # :nodoc:
184
+ raise CriticalError unless row.class == const_get(:Row)
185
+ if row.new_record?
186
+ insert( row )
187
+ else
188
+ # return false unless row.dirty?
189
+ update( row )
190
+ end
191
+ row.dirty = false
192
+ true
193
+ end
194
+
195
+ # create an insert statement for a row
196
+ def insert( row ) # :nodoc:
197
+ fields, values = [], []
198
+ columns.each do | field_name, datatype |
199
+ # check for set primary key fields or fetch respective default values
200
+ if primary_keys.member?( field_name ) && row.send( field_name ) == nil
201
+ if datatype.default
202
+ row.send( "#{field_name}=", database.execute("SELECT #{datatype.default};")[0][0] )
203
+ end
204
+ if row.send( field_name ) == nil
205
+ raise Error, "Primary key fields(#{field_name}) need to be set or must have a default"
206
+ end
207
+ end
208
+ next if row.send( field_name ).nil?
209
+ fields << field_name
210
+ values << datatype.escape( row.send( field_name ))
211
+ end
212
+ raise Error, "insert with all fields nil" if fields.empty?
213
+ sql = "INSERT INTO " + full_name + '(' + fields.join(',') + ') VALUES (' + values.join(',') + ');'
214
+ row.new_record = false
215
+ database.execute( sql )
216
+ end
217
+
218
+ # create an update statement for a row
219
+ def update( row ) # :nodoc:
220
+ raise CriticalError, 'Updating is only allowed for tables with primary keys' if primary_keys.empty?
221
+ setter, conditions = [], {}
222
+ columns.each do | field_name, data_type |
223
+ setter << field_name.to_s + ' = ' + data_type.escape(row.send(field_name))
224
+ end
225
+ primary_keys.each do | field_name |
226
+ raise Error, "Primary key fields must not be empty!" if not row.send( field_name )
227
+ conditions[field_name] = row.send( field_name )
228
+ end
229
+ sql = 'UPDATE ' + full_name + ' SET ' + setter.join(',') + compile_where( conditions ) + ';'
230
+ database.execute( sql )
231
+ end
232
+
233
+ def delete( row ) # :nodoc:
234
+ raise CriticalError, 'Deleting is only allowed for tables with primary keys' if primary_keys.empty?
235
+ raise Error, "this is a new record" if row.new_record?
236
+ conditions = {}
237
+ primary_keys.each do | field_name |
238
+ raise Error, "Primary key fields must not be empty!" if not row.send( field_name )
239
+ conditions[field_name] = row.send( field_name )
240
+ end
241
+ sql = "DELETE FROM #{table_name} #{compile_where(conditions)};"
242
+ row.new_record = true
243
+ database.execute( sql )
244
+ end
245
+
246
+ end
247
+
248
+ end
249
+
250
+ end
251
+
data/sql/install.sql ADDED
@@ -0,0 +1,10 @@
1
+
2
+ BEGIN;
3
+
4
+ CREATE SCHEMA momomoto;
5
+
6
+ \i types.sql
7
+ \i procedures.sql
8
+
9
+ COMMIT;
10
+
@@ -0,0 +1,54 @@
1
+
2
+ CREATE OR REPLACE FUNCTION momomoto.fetch_procedure_columns( procedure_name TEXT ) RETURNS SETOF momomoto.procedure_column AS $$
3
+ DECLARE
4
+ proc RECORD;
5
+ typ RECORD;
6
+ att RECORD;
7
+ col momomoto.procedure_column%rowtype;
8
+ BEGIN
9
+ SELECT INTO proc * FROM pg_proc WHERE proname = procedure_name;
10
+ IF FOUND THEN
11
+ SELECT INTO typ * FROM pg_type WHERE oid = proc.prorettype;
12
+
13
+ IF typ.typtype = 'b' THEN
14
+ col.column_name = procedure_name;
15
+ SELECT INTO col.data_type format_type( proc.prorettype, NULL::integer );
16
+ RETURN NEXT col;
17
+ ELSIF typ.typtype = 'c' THEN
18
+ FOR col IN
19
+ SELECT attname AS column_name, format_type(atttypid, NULL) FROM pg_attribute WHERE attrelid = typ.typrelid AND attnum > 0 ORDER BY attnum
20
+ LOOP
21
+ RETURN NEXT col;
22
+ END LOOP;
23
+ ELSE
24
+ RAISE EXCEPTION 'Not yet implemented';
25
+ END IF;
26
+ END IF;
27
+ RETURN;
28
+ END;
29
+ $$ LANGUAGE plpgsql;
30
+
31
+ CREATE OR REPLACE FUNCTION momomoto.fetch_procedure_parameters( procedure_name TEXT ) RETURNS SETOF momomoto.procedure_parameter AS $$
32
+ DECLARE
33
+ proc RECORD;
34
+ typ RECORD;
35
+ col momomoto.procedure_parameter%rowtype;
36
+ i INTEGER;
37
+ j INTEGER;
38
+ k INTEGER;
39
+ BEGIN
40
+ SELECT INTO proc proargnames, proallargtypes, proargtypes FROM pg_proc WHERE proname = procedure_name;
41
+ IF FOUND THEN
42
+ j = array_lower(proc.proargtypes, 1);
43
+ k = array_upper(proc.proargtypes, 1);
44
+ FOR i IN j .. k
45
+ LOOP
46
+ col.parameter_name = proc.proargnames[ i + array_lower( proc.proargnames, 1 )];
47
+ col.data_type = format_type( proc.proargtypes[i], NULL );
48
+ RETURN NEXT col;
49
+ END LOOP;
50
+ END IF;
51
+ RETURN;
52
+ END;
53
+ $$ LANGUAGE plpgsql;
54
+
data/sql/types.sql ADDED
@@ -0,0 +1,11 @@
1
+
2
+ CREATE TYPE momomoto.procedure_parameter AS (
3
+ parameter_name TEXT,
4
+ data_type TEXT
5
+ );
6
+
7
+ CREATE TYPE momomoto.procedure_column AS (
8
+ column_name TEXT,
9
+ data_type TEXT
10
+ );
11
+
data/test/test_base.rb ADDED
@@ -0,0 +1,17 @@
1
+
2
+ class TestBase < Test::Unit::TestCase
3
+
4
+ def test_compile_where
5
+ t = Class.new( Momomoto::Table )
6
+ t.table_name = 'person'
7
+ t.columns
8
+ assert_equal( " WHERE person_id = '1'" , t.compile_where( :person_id => '1' ) )
9
+ assert_equal( " WHERE person_id IN ('1')" , t.compile_where( :person_id => ['1'] ) )
10
+ assert_equal( " WHERE person_id IN ('1','2')" , t.compile_where( :person_id => ['1',2] ) )
11
+ assert_equal( " WHERE first_name = '1'" , t.compile_where( :first_name => '1' ) )
12
+ assert_equal( " WHERE first_name = 'chu''nky'" , t.compile_where( :first_name => "chu'nky" ) )
13
+ assert_equal( " WHERE first_name IN ('chu''nky','bac''n')" , t.compile_where( :first_name => ["chu'nky","bac'n"] ) )
14
+ end
15
+
16
+ end
17
+
@@ -0,0 +1,26 @@
1
+
2
+ class TestBigint < Test::Unit::TestCase
3
+
4
+ def test_samples
5
+ c = Class.new( Momomoto::Table )
6
+ c.table_name = 'test_bigint'
7
+ [nil,1,2,4294967296,5294967296].each do | number |
8
+ r = c.new( :data => number )
9
+ assert_equal( number, r.data )
10
+ r.write
11
+ r2 = c.select(:id=>r.id).first
12
+ assert_equal( number, r2.data )
13
+ end
14
+ end
15
+
16
+ def test_compile_rule
17
+ t = Momomoto::Datatype::Bigint.new
18
+ input = [ 1, '1', [1], ['1'], [1,2,3],['1','2','3'], {:eq=>1}, {:eq=>'1'}, {:lt=>10, :gt=>5}, {:lt=>'10', :gt=>'5'} ]
19
+
20
+ input.each do | test_input |
21
+ assert_instance_of( String, t.compile_rule( :field, test_input ) )
22
+ end
23
+ end
24
+
25
+ end
26
+
@@ -0,0 +1,30 @@
1
+
2
+ class TestBoolean < Test::Unit::TestCase
3
+
4
+ def test_samples
5
+ c = Class.new( Momomoto::Table )
6
+ c.table_name = 'test_boolean'
7
+ [nil,true,false].each do | value |
8
+ r = c.new( :data => value )
9
+ assert_equal( value, r.data )
10
+ r.write
11
+ r2 = c.select(:id=>r.id).first
12
+ assert_equal( value, r2.data )
13
+ end
14
+ r = c.new
15
+ [nil,''].each do | input |
16
+ r.data = input
17
+ assert_equal( nil, r.data )
18
+ end
19
+ [true,'t',1].each do | input |
20
+ r.data = input
21
+ assert_equal( true, r.data )
22
+ end
23
+ [false,'f',0].each do | input |
24
+ r.data = input
25
+ assert_equal( false, r.data )
26
+ end
27
+ end
28
+
29
+ end
30
+