momomoto 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
+