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