momomoto 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +340 -0
- data/Rakefile +38 -0
- data/lib/momomoto.rb +5 -0
- data/lib/momomoto/base.rb +162 -0
- data/lib/momomoto/database.rb +179 -0
- data/lib/momomoto/datatype.rb +20 -0
- data/lib/momomoto/datatype/base.rb +78 -0
- data/lib/momomoto/datatype/bigint.rb +9 -0
- data/lib/momomoto/datatype/boolean.rb +23 -0
- data/lib/momomoto/datatype/bytea.rb +13 -0
- data/lib/momomoto/datatype/character.rb +7 -0
- data/lib/momomoto/datatype/character_varying.rb +7 -0
- data/lib/momomoto/datatype/date.rb +22 -0
- data/lib/momomoto/datatype/inet.rb +14 -0
- data/lib/momomoto/datatype/integer.rb +16 -0
- data/lib/momomoto/datatype/interval.rb +30 -0
- data/lib/momomoto/datatype/numeric.rb +17 -0
- data/lib/momomoto/datatype/real.rb +7 -0
- data/lib/momomoto/datatype/smallint.rb +7 -0
- data/lib/momomoto/datatype/text.rb +24 -0
- data/lib/momomoto/datatype/time_with_time_zone.rb +10 -0
- data/lib/momomoto/datatype/time_without_time_zone.rb +30 -0
- data/lib/momomoto/datatype/timestamp_with_time_zone.rb +7 -0
- data/lib/momomoto/datatype/timestamp_without_time_zone.rb +29 -0
- data/lib/momomoto/information_schema/columns.rb +45 -0
- data/lib/momomoto/information_schema/fetch_procedure_columns.rb +14 -0
- data/lib/momomoto/information_schema/fetch_procedure_parameters.rb +14 -0
- data/lib/momomoto/information_schema/key_column_usage.rb +19 -0
- data/lib/momomoto/information_schema/routines.rb +12 -0
- data/lib/momomoto/information_schema/table_constraints.rb +19 -0
- data/lib/momomoto/join.rb +66 -0
- data/lib/momomoto/momomoto.rb +10 -0
- data/lib/momomoto/order.rb +56 -0
- data/lib/momomoto/procedure.rb +129 -0
- data/lib/momomoto/row.rb +63 -0
- data/lib/momomoto/table.rb +251 -0
- data/sql/install.sql +10 -0
- data/sql/procedures.sql +54 -0
- data/sql/types.sql +11 -0
- data/test/test_base.rb +17 -0
- data/test/test_bigint.rb +26 -0
- data/test/test_boolean.rb +30 -0
- data/test/test_bytea.rb +35 -0
- data/test/test_character.rb +27 -0
- data/test/test_character_varying.rb +17 -0
- data/test/test_database.rb +63 -0
- data/test/test_datatype.rb +62 -0
- data/test/test_date.rb +50 -0
- data/test/test_inet.rb +27 -0
- data/test/test_information_schema.rb +27 -0
- data/test/test_integer.rb +37 -0
- data/test/test_interval.rb +38 -0
- data/test/test_join.rb +19 -0
- data/test/test_numeric.rb +30 -0
- data/test/test_procedure.rb +75 -0
- data/test/test_real.rb +17 -0
- data/test/test_row.rb +47 -0
- data/test/test_smallint.rb +26 -0
- data/test/test_table.rb +233 -0
- data/test/test_text.rb +25 -0
- data/test/test_time_with_time_zone.rb +17 -0
- data/test/test_time_without_time_zone.rb +40 -0
- data/test/test_timestamp_with_time_zone.rb +17 -0
- data/test/test_timestamp_without_time_zone.rb +28 -0
- metadata +116 -0
data/lib/momomoto/row.rb
ADDED
@@ -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
data/sql/procedures.sql
ADDED
@@ -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
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
|
+
|
data/test/test_bigint.rb
ADDED
@@ -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
|
+
|