webink 1.1.4

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.
@@ -0,0 +1,425 @@
1
+ module Ink
2
+
3
+ # = Database class
4
+ #
5
+ # == Config
6
+ #
7
+ # Currently there are two types of databases supported, MySQL and
8
+ # SQLite3. Either way, you need to specify them in a config-file
9
+ # that is located inside the web project folder.
10
+ #
11
+ # Sample config for MySQL:
12
+ # config = {
13
+ # "escape_post_data" => false,
14
+ # "production" => true,
15
+ # "db_type" => "mysql",
16
+ # "db_user" => "yourusername",
17
+ # "db_pass" => "yourpassword",
18
+ # "db_database" => "yourdatabase",
19
+ # "db_server" => "localhost",
20
+ # }
21
+ #
22
+ # Sample config for SQLite3:
23
+ # config = {
24
+ # "escape_post_data" => false,
25
+ # "production" => true,
26
+ # "db_type" => "sqlite3",
27
+ # "db_server" => "/full/path/to/database.sqlite",
28
+ # }
29
+ #
30
+ # == Usage
31
+ #
32
+ # Create an Ink::Database instance with the self.create class method.
33
+ # Now it can be accessed via the public class variable 'database'.
34
+ # Once that is done, can use it to execute various SQL statements
35
+ # to gather data.
36
+ #
37
+ # Ink::Database.database.query "SELECT * FROM x;"
38
+ #
39
+ # This is the most basic query, it returns an Array of results,
40
+ # and each element contains a Hash of column_name => column_entry.
41
+ #
42
+ # Ink::Database.database.find "apples", "WHERE id < 10 GROUP BY color"
43
+ # => self.query("SELECT * FROM apples WHERE id < 10 GROUP BY color;")
44
+ #
45
+ # This is different from the query method, because it returns an Array
46
+ # of Objects, created by the information stored in the database. So this
47
+ # find() will return you a set of Apple-instances.
48
+ #
49
+ # Ink::Database.database.find_union "apple", 5, "tree", ""
50
+ #
51
+ # find_union allows you to retrieve data through a many_many reference.
52
+ # When you define a many_many relationship, a helper-table is created
53
+ # inside the database, which is apple_tree, in this case. This statement
54
+ # will fetch an Array of all Trees, that connect to the Apple with primary
55
+ # key 5. Notice that the relationship database apple_tree is put together
56
+ # by the alphabetically first, and then second classname. The last quotes
57
+ # allow additional query informations to be passed along (like group by)
58
+ #
59
+ # Ink::Database.database.find_reference "tree", 1, "apple", ""
60
+ #
61
+ # find_reference is similar to find_union, only that it handles all
62
+ # other relationships. This statement above requires one Tree to have many
63
+ # Apples, so it will return an Array of Apples, all those that belong to
64
+ # the Tree with primary key 1
65
+ #
66
+ # Please close the dbinstance once you are done. This is automatically
67
+ # inserted in the init.rb of a project.
68
+ #
69
+ #
70
+ #
71
+ class Database
72
+ private_class_method :new
73
+ @@database = nil
74
+
75
+ # Private Constructor
76
+ #
77
+ # Uses the config parameter to create a database
78
+ # connection, and will throw an error, if that is not
79
+ # possible.
80
+ # [param config:] Hash of config parameters
81
+ def initialize(config)
82
+ @type = config["db_type"]
83
+ if @type == "mysql"
84
+ @db = Mysql.real_connect(config["db_server"],config["db_user"],config["db_pass"],config["db_database"])
85
+ @db.reconnect = true
86
+ elsif @type == "sqlite3"
87
+ @db = SQLite3::Database.new(config["db_server"])
88
+ else
89
+ raise ArgumentError.new("Database undefined.")
90
+ end
91
+ end
92
+
93
+ # Class method
94
+ #
95
+ # Instanciates a new Database if none is found
96
+ # [param config:] Hash of config parameters
97
+ def self.create(config)
98
+ @@database = new(config) if not @@database
99
+ end
100
+
101
+ # Class method
102
+ #
103
+ # Removes an instanciated Database
104
+ def self.drop
105
+ @@database = nil if @@database
106
+ end
107
+
108
+ # Class method
109
+ #
110
+ # Returns the Database instance or raises a Runtime Error
111
+ # [returns:] Database instance
112
+ def self.database
113
+ (@@database) ? @@database : (raise RuntimeError.new("No Database found. Create one first"))
114
+ end
115
+
116
+ # Instance method
117
+ #
118
+ # This will retrieve all tables nested into
119
+ # the connected database.
120
+ # [returns:] Array of tables
121
+ def tables
122
+ result = Array.new
123
+ if @type == "mysql"
124
+ re = @db.query "show tables;"
125
+ re.each do |row|
126
+ row.each do |t|
127
+ result.push t
128
+ end
129
+ end
130
+ elsif @type == "sqlite3"
131
+ re = @db.query "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name;"
132
+ re.each do |row|
133
+ row.each do |t|
134
+ result.push t
135
+ end
136
+ end
137
+ re.close
138
+ end
139
+ result
140
+ end
141
+
142
+ # Instance method
143
+ #
144
+ # Send an SQL query string to the database
145
+ # and retrieve a result set
146
+ # [param query:] SQL query string
147
+ # [returns:] Array of Hashes of column_name => column_entry
148
+ def query(query)
149
+ result = Array.new
150
+ if @type == "mysql"
151
+ re = @db.method("query").call query
152
+ if re
153
+ re.each_hash do |row|
154
+ result.push Hash.new
155
+ row.each do |k,v|
156
+ v = $&.to_i if v =~ /^[0-9]+$/
157
+ result[result.length-1][k] = v
158
+ end
159
+ end
160
+ end
161
+ elsif @type == "sqlite3"
162
+ re = @db.method("query").call query
163
+ re.each do |row|
164
+ result.push Hash.new
165
+ for i in 0...re.columns.length
166
+ row[i] = $&.to_i if row[i] =~ /^[0-9]+$/
167
+ result[result.length-1][re.columns[i]] = row[i]
168
+ end
169
+ end
170
+ re.close if not re.closed?
171
+ end
172
+ result
173
+ end
174
+
175
+ # Instance method
176
+ #
177
+ # Closes the database connection, there is no way
178
+ # to reopen without creating a new Ink::Database instance
179
+ def close
180
+ if @type == "sqlite3" and not @db.closed?
181
+ begin
182
+ @db.close
183
+ rescue SQLite3::BusyException
184
+ end
185
+ elsif @type == "mysql"
186
+ @db.close
187
+ end
188
+ self.class.drop
189
+ end
190
+
191
+ # Instance method
192
+ #
193
+ # Attempts to fetch the last inserted primary key
194
+ # [returns:] primary key or nil
195
+ def last_inserted_pk
196
+ string = ""
197
+ if @type == "mysql"
198
+ string = "LAST_INSERT_ID()"
199
+ elsif @type == "sqlite3"
200
+ string = "last_insert_rowid()"
201
+ end
202
+ response = self.query "SELECT #{string} as id"
203
+ return (response.length > 0) ? response[0]["id"] : nil
204
+ end
205
+
206
+ # Instance method
207
+ #
208
+ # Creates the SQL syntax for the chosen database type
209
+ # to define a primary key, autoincrementing field
210
+ # [returns:] SQL syntax for a primary key field
211
+ def primary_key_autoincrement(pk="id")
212
+ result = Array.new
213
+ if @type == "mysql"
214
+ result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "AUTO_INCREMENT"]
215
+ elsif @type == "sqlite3"
216
+ result = ["`#{pk}`", "INTEGER", "PRIMARY KEY", "ASC"]
217
+ end
218
+ result
219
+ end
220
+
221
+ # Instance method
222
+ #
223
+ # Delete something from the database.
224
+ # [param class_name:] Defines the table name
225
+ # [param params:] Additional SQL syntax like WHERE conditions (optional)
226
+ def remove(class_name, params="")
227
+ table_name = Ink::Model.str_to_tablename(class_name)
228
+ return if not table_name
229
+ self.query("DELETE FROM #{table_name} #{params};")
230
+ end
231
+
232
+ # Instance method
233
+ #
234
+ # Retrieve class instances, that are loaded with the database result set.
235
+ # [param class_name:] Defines the table name and resulting Instance classnames
236
+ # [param params:] Additional SQL syntax like WHERE conditions (optional)
237
+ # [returns:] Array of class_name instances from the SQL result set
238
+ def find(class_name, params="")
239
+ result = Array.new
240
+ table_name = Ink::Model.str_to_tablename(class_name)
241
+ return result if not table_name
242
+
243
+ re = self.query("SELECT * FROM #{table_name} #{params};")
244
+ re.each do |entry|
245
+ instance = Ink::Model.classname(class_name).new entry
246
+ result.push instance
247
+ end
248
+ result
249
+ end
250
+
251
+ # Instance method
252
+ #
253
+ # Retrieve class2 instances, that are related to the class1 instance with
254
+ # primary key class1_id. This is done via an additional relationship table.
255
+ # Only relevant for many_many relationships.
256
+ # [param class1:] Reference classname
257
+ # [param class1_id:] Primary key value of the reference classname
258
+ # [param class2:] Match classname
259
+ # [param params:] Additional SQL syntax like GROUP BY (optional)
260
+ # [returns:] Array of class2 instances from the SQL result set
261
+ def find_union(class1, class1_id, class2, params="")
262
+ result = Array.new
263
+ relationship = nil
264
+ Ink::Model.classname(class1).foreign.each do |k,v|
265
+ relationship = v if k.downcase == class2.downcase
266
+ end
267
+ return result if relationship != "many_many"
268
+ fk1 = Ink::Model.classname(class1).foreign_key[0]
269
+ pk2 = Ink::Model.classname(class2).primary_key[0]
270
+ fk2 = Ink::Model.classname(class2).foreign_key[0]
271
+ tablename1 = Ink::Model.str_to_tablename(class1)
272
+ tablename2 = Ink::Model.str_to_tablename(class2)
273
+ union_class = ((class1.downcase <=> class2.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
274
+ re = self.query("SELECT #{tablename2}.* FROM #{union_class}, #{tablename2} WHERE #{union_class}.#{fk1} = #{class1_id} AND #{union_class}.#{fk2} = #{tablename2}.#{pk2} #{params};")
275
+ re.each do |entry|
276
+ instance = Ink::Model.classname(tablename2).new entry
277
+ result.push instance
278
+ end
279
+ result
280
+ end
281
+
282
+ # Instance method
283
+ #
284
+ # Retrieve class2 instances, that are related to the class1 instance with
285
+ # primary key class1_id. Not relevant for many_many relationships
286
+ # [param class1:] Reference classname
287
+ # [param class1_id:] Primary key value of the reference classname
288
+ # [param class2:] Match classname
289
+ # [param params:] Additional SQL syntax like GROUP BY (optional)
290
+ # [returns:] Array of class2 instances from the SQL result set
291
+ def find_references(class1, class1_id, class2, params="")
292
+ result = Array.new
293
+ relationship = nil
294
+ Ink::Model.classname(class1).foreign.each do |k,v|
295
+ relationship = v if k.downcase == class2.downcase
296
+ end
297
+ return result if relationship == "many_many"
298
+ re = Array.new
299
+ fk1 = Ink::Model.classname(class1).foreign_key[0]
300
+ if ((class1.downcase <=> class2.downcase) < 0 and relationship == "one_one") or relationship == "one_many"
301
+ re = self.query "SELECT * FROM #{Ink::Model.str_to_tablename(class2)} WHERE #{Ink::Model.classname(class2).primary_key[0]}=(SELECT #{Ink::Model.classname(class2).foreign_key[0]} FROM #{Ink::Model.str_to_tablename(class1)} WHERE #{Ink::Model.classname(class1).primary_key[0]}=#{class1_id});"
302
+ else
303
+ re = self.query "SELECT * FROM #{Ink::Model.str_to_tablename(class2)} WHERE #{fk1} = #{class1_id} #{params};"
304
+ end
305
+
306
+ re.each do |entry|
307
+ instance = Ink::Model.classname(class2).new entry
308
+ result.push instance
309
+ end
310
+ result
311
+ end
312
+
313
+ # Instance method
314
+ #
315
+ # This method attempts to remove all existing relationship data
316
+ # of instance with link of type: type. For one_one relationships
317
+ # this works only one way, requiring a second call later on before
318
+ # setting a new value.
319
+ # [param instance:] Instance of a class that refers to an existing database entry
320
+ # [param link:] the related class (not a String, but class reference)
321
+ # [param type:] relationship type
322
+ def delete_all_links(instance, link, type)
323
+ if type == "one_one"
324
+ firstclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class : link
325
+ secondclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? link : instance.class
326
+ key = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class.primary_key[0] : instance.class.foreign_key[0]
327
+ value = instance.method(instance.class.primary_key[0]).call
328
+ @db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=NULL WHERE #{key}=#{value};"
329
+ elsif type == "one_many" or type == "many_one"
330
+ firstclass = (type == "one_many") ? instance.class : link
331
+ secondclass = (type == "one_many") ? link : instance.class
332
+ key = (type == "one_many") ? instance.class.primary_key[0] : instance.class.foreign_key[0]
333
+ value = instance.method(instance.class.primary_key[0]).call
334
+ @db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=NULL WHERE #{key}=#{value};"
335
+ elsif type == "many_many"
336
+ tablename1 = Ink::Model.str_to_tablename(instance.class.name)
337
+ tablename2 = Ink::Model.str_to_tablename(link.name)
338
+ union_class = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
339
+ value = instance.method(instance.class.primary_key[0]).call
340
+ @db.query "DELETE FROM #{union_class} WHERE #{instance.class.foreign_key[0]}=#{value};"
341
+ end
342
+ end
343
+
344
+ # Instance method
345
+ #
346
+ # Attempt to create links of instance to the data inside value.
347
+ # link is the class of the related data, and type refers to the
348
+ # relationship type of the two. When one tries to insert an array
349
+ # for a x_one relationship, the last entry will be set.
350
+ # [param instance:] Instance of a class that refers to an existing database entry
351
+ # [param link:] the related class (not a String, but class reference)
352
+ # [param type:] relationship type
353
+ # [param value:] relationship data that was set, either a primary key value, or an instance, or an array of both
354
+ def create_all_links(instance, link, type, value)
355
+ to_add = Array.new
356
+ if value.is_a? Array
357
+ value.each do |v|
358
+ if v.instance_of? link
359
+ to_add.push(v.method(link.primary_key[0]).call)
360
+ else
361
+ to_add.push v
362
+ end
363
+ end
364
+ elsif value.instance_of? link
365
+ to_add.push(value.method(link.primary_key[0]).call)
366
+ else
367
+ to_add.push value
368
+ end
369
+ to_add.each do |fk|
370
+ self.create_link instance, link, type, fk
371
+ end
372
+ end
373
+
374
+ # Instance method
375
+ #
376
+ # Creates a link between instance and a link with primary fk.
377
+ # The relationship between the two is defined by type. one_one
378
+ # relationships are placing an additional call to delete_all_links
379
+ # that will remove conflicts.
380
+ # [param instance:] Instance of a class that refers to an existing database entry
381
+ # [param link:] the related class (not a String, but class reference)
382
+ # [param type:] relationship type
383
+ # [param value:] primary key of the relationship, that is to be created
384
+ def create_link(instance, link, type, fk)
385
+ if type == "one_one"
386
+ if (instance.class.name.downcase <=> link.name.downcase) < 0
387
+ re = self.find(link.name, "WHERE #{link.primary_key[0]}=#{fk};")[0]
388
+ self.delete_all_links re, instance.class, type
389
+ end
390
+ firstclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class : link
391
+ secondclass = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? link : instance.class
392
+ key = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? instance.class.primary_key[0] : link.primary_key[0]
393
+ value = instance.method(instance.class.primary_key[0]).call
394
+ fk_set = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? fk : value
395
+ value_set = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? value : fk
396
+ @db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=#{fk} WHERE #{key}=#{value};"
397
+ elsif type == "one_many" or type == "many_one"
398
+ firstclass = (type == "one_many") ? instance.class : link
399
+ secondclass = (type == "one_many") ? link : instance.class
400
+ key = (type == "one_many") ? instance.class.primary_key[0] : link.primary_key[0]
401
+ value = instance.method(instance.class.primary_key[0]).call
402
+ fk_set = (type == "one_many") ? fk : value
403
+ value_set = (type == "one_many") ? value : fk
404
+ @db.query "UPDATE #{Ink::Model.str_to_tablename(firstclass.name)} SET #{secondclass.foreign_key[0]}=#{fk_set} WHERE #{key}=#{value_set};"
405
+ elsif type == "many_many"
406
+ tablename1 = Ink::Model.str_to_tablename(instance.class.name)
407
+ tablename2 = Ink::Model.str_to_tablename(link.name)
408
+ union_class = ((instance.class.name.downcase <=> link.name.downcase) < 0) ? "#{tablename1}_#{tablename2}" : "#{tablename2}_#{tablename1}"
409
+ value = instance.method(instance.class.primary_key[0]).call
410
+ @db.query "INSERT INTO #{union_class} (#{instance.class.foreign_key[0]}, #{link.foreign_key[0]}) VALUES (#{value}, #{fk});"
411
+ end
412
+ end
413
+
414
+ # Class method
415
+ #
416
+ # Formats a Time object according to the SQL TimeDate standard
417
+ # [param date:] Time object
418
+ # [returns:] Formatted string
419
+ def self.format_date(date)
420
+ (date.instance_of? Time) ? date.strftime("%Y-%m-%d %H:%M:%S") : ""
421
+ end
422
+
423
+ end
424
+
425
+ end
@@ -0,0 +1,311 @@
1
+ module Ink
2
+
3
+ # = Model class
4
+ #
5
+ # == Usage
6
+ #
7
+ # Models are usually derived from. So let's assume there is a
8
+ # class called Apple < Ink::Model
9
+ #
10
+ # apple = Apple.new {:color => "red", :diameter => 4}
11
+ #
12
+ # The constructor checks, if there are class methods 'fields'
13
+ # and 'foreign' defined. If that check is positive, it will
14
+ # match the parameter Hash to the fields, that are set for
15
+ # the database, and thow an exception if fields is lacking
16
+ # an entry (excluded the primary key). The other case just
17
+ # creates an Apple with the Hash as instance variables.
18
+ #
19
+ # puts apple.color
20
+ #
21
+ # This prints "red" to the stdout, since getter and setter
22
+ # methods are automatically added for either the Hash, or
23
+ # the fields and foreign keys.
24
+ #
25
+ # apple.tree = nil
26
+ # apple.save
27
+ #
28
+ # You can save your apple by using the save method. New instances
29
+ # will create a new row in the database, and update its primary
30
+ # key. Old instances just update the fields. Relationships, like
31
+ # in the sample below a tree, is set to nil by default, and therefore
32
+ # the save method will not touch relationships.
33
+ #
34
+ # treeinstance.apple = [1,2,myapple]
35
+ # treeinstance.save
36
+ #
37
+ # To insert relationship data, you can provide them by array, value
38
+ # or reference, so setting treeinstance.apple to 1 is allowed, also
39
+ # to myapple, or an array or a combination. An empty array [] will
40
+ # remove all references. This works both ways, just consider the
41
+ # relationship type, as an apple cannot have more than one tree.
42
+ #
43
+ # treeinstance.delete
44
+ #
45
+ # The model provides a convenience method for deletion. It removes all
46
+ # references from relationships, but does not remove the relationships
47
+ # themselves, so you must fetch all related data, and delete them by
48
+ # 'hand' if you will.
49
+ #
50
+ #
51
+ # = Fields and foreign sample config
52
+ #
53
+ # class Apple < Ink::Model
54
+ # def self.fields
55
+ # fields = {
56
+ # :id => "PRIMARY KEY"
57
+ # :color => [ "VARCHAR", "NOT NULL" ],
58
+ # :diameter => [ "NUMERIC", "NOT NULL" ]
59
+ # }
60
+ # fields
61
+ # end
62
+ # def self.foreign
63
+ # foreign = {
64
+ # "Tree" => "one_many"
65
+ # }
66
+ # foreign
67
+ # end
68
+ # end
69
+ #
70
+ # Let's look at this construct.
71
+ # The constructor is inherited from Ink::Model, so are its
72
+ # methods. 'fields' defines a Hash of Arrays, that will
73
+ # create the Database table for us.
74
+ # 'foreign' handles the contraints to other classes, here
75
+ # it reads: one "Tree" has many Apples, other constructs
76
+ # could be: [one "Tree" has one Apple, many "Tree"s have
77
+ # many Apples, many "Tree"s have one Apple] => [one_one,
78
+ # many_many, many_one]
79
+ # Obviously the Tree class requires a foreign with "Apple"
80
+ # mapped to "many_one" to match this schema.
81
+ #
82
+ #
83
+ #
84
+ class Model
85
+
86
+ # Constructor
87
+ #
88
+ # Keys from the data parameter will be converted into
89
+ # instance variables with getters and setters in place.
90
+ # The primary key has no setter, but adds a getter called
91
+ # pk for convenience.
92
+ # [param data:] Hash of String => Objects
93
+ def initialize(data)
94
+ if self.class.respond_to? :fields
95
+ self.class.fields.each do |k,v|
96
+ raise NameError.new("Model cannot use #{k} as field, it already exists") if self.class.respond_to? k or k.to_s.downcase == "pk"
97
+ raise LoadError.new("Model cannot be loaded, argument missing: #{k}") if not data[k.to_s] and self.class.primary_key[0] != k
98
+ entry = nil
99
+ if data[k.to_s].is_a? String
100
+ entry = data[k.to_s].gsub(/'/, '&#39;')
101
+ elsif data[k.to_s].is_a? Numeric
102
+ entry = data[k.to_s]
103
+ else
104
+ entry = "\'#{data[k.to_s]}\'"
105
+ end
106
+ instance_variable_set("@#{k}", entry)
107
+
108
+ self.class.send(:define_method, k) do
109
+ instance_variable_get "@#{k}"
110
+ end
111
+ if self.class.primary_key[0] != k
112
+ self.class.send(:define_method, "#{k}=") do |val|
113
+ if val.is_a? String
114
+ val = val.gsub(/'/, '&#39;')
115
+ elsif val.is_a? Numeric
116
+ val = val
117
+ else
118
+ val = "\'#{val}\'"
119
+ end
120
+ instance_variable_set "@#{k}", val
121
+ end
122
+ else
123
+ self.class.send(:define_method, "pk") do
124
+ instance_variable_get "@#{k.to_s.downcase}"
125
+ end
126
+ end
127
+ end
128
+ if self.class.respond_to? :foreign
129
+ self.class.foreign.each do |k,v|
130
+ raise NameError.new("Model cannot use #{k} as foreign, it already exists") if self.class.respond_to? k.to_sym or k.downcase == "pk"
131
+ instance_variable_set "@#{self.class.str_to_tablename(k)}", nil
132
+ self.class.send(:define_method, k.downcase) do
133
+ instance_variable_get "@#{k.downcase}"
134
+ end
135
+ self.class.send(:define_method, "#{k.downcase}=") do |val|
136
+ instance_variable_set "@#{k.downcase}", val
137
+ end
138
+ end
139
+ end
140
+ else
141
+ data.each do |k,v|
142
+ instance_variable_set "@#{k}", (v.is_a?(Numeric)) ? v : "\'#{v}\'"
143
+ end
144
+ end
145
+ end
146
+
147
+ # Instance method
148
+ #
149
+ # Save the instance to the database. Set all foreign sets to
150
+ # nil if you do not want to change them. Old references are
151
+ # automatically removed.
152
+ def save
153
+ raise NotImplementedError.new("Cannot save to Database without field definitions") if not self.class.respond_to? :fields
154
+ string = Array.new
155
+ keystring = Array.new
156
+ valuestring = Array.new
157
+ fields = self.class.fields
158
+ pkvalue = nil
159
+ for i in 0...fields.keys.length
160
+ k = fields.keys[i]
161
+ value = instance_variable_get "@#{k}"
162
+ value = "NULL" if not value
163
+ if k != self.class.primary_key[0]
164
+ string.push "`#{k}`=#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
165
+ keystring.push "`#{k}`"
166
+ valuestring.push "#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
167
+ else
168
+ pkvalue = "WHERE `#{self.class.primary_key[0]}`=#{(value.is_a?(Numeric)) ? value : "\'#{value}\'"}"
169
+ end
170
+ end
171
+ if pkvalue
172
+ response = Ink::Database.database.find self.class.name, pkvalue
173
+ if response.length == 1
174
+ Ink::Database.database.query "UPDATE #{Ink::Model.str_to_tablename(self.class.name)} SET #{string * ","} #{pkvalue}"
175
+ elsif response.length == 0
176
+ Ink::Database.database.query "INSERT INTO #{Ink::Model.str_to_tablename(self.class.name)} (#{keystring * ","}) VALUES (#{valuestring * ","});"
177
+ pk = Ink::Database.database.last_inserted_pk
178
+ instance_variable_set "@#{self.class.primary_key[0]}", pk.is_a?(Numeric) ? pk : "\'#{pk}\'" if pk
179
+ end
180
+ end
181
+
182
+ if self.class.respond_to? :foreign
183
+ self.class.foreign.each do |k,v|
184
+ value = instance_variable_get "@#{self.class.str_to_tablename(k)}"
185
+ if value
186
+ Ink::Database.database.delete_all_links self, Ink::Model.classname(k), v
187
+ Ink::Database.database.create_all_links self, Ink::Model.classname(k), v, value
188
+ end
189
+ end
190
+ end
191
+ end
192
+
193
+ # Instance method
194
+ #
195
+ # Deletes the data from the database, essentially making the instance
196
+ # obsolete. Disregard from using the instance anymore.
197
+ # All links between models will be removed also.
198
+ def delete
199
+ raise NotImplementedError.new("Cannot delete from Database without field definitions") if not self.class.respond_to? :fields
200
+ if self.class.respond_to? :foreign
201
+ self.class.foreign.each do |k,v|
202
+ Ink::Database.database.delete_all_links self, Ink::Model.classname(k), v
203
+ end
204
+ end
205
+
206
+ pkvalue = instance_variable_get "@#{self.class.primary_key[0]}"
207
+ Ink::Database.database.remove self.class.name, "WHERE `#{self.class.primary_key[0]}`=#{(pkvalue.is_a?(Numeric)) ? pkvalue : "\'#{pkvalue}\'"};"
208
+ end
209
+
210
+ # Class method
211
+ #
212
+ # This will create SQL statements for creating the
213
+ # database tables. 'fields' method is mandatory for
214
+ # this, and 'foreign' is optional.
215
+ # [returns:] Array of SQL statements
216
+ def self.create
217
+ result = Array.new
218
+ raise NotImplementedError.new("Cannot create a Database without field definitions") if not self.respond_to? :fields
219
+
220
+ string = "CREATE TABLE #{Model::str_to_tablename(self.name)} ("
221
+ mfk = self.foreign_key
222
+ fields = self.fields
223
+ for i in 0...fields.keys.length
224
+ k = fields.keys[i]
225
+ string += "`#{k}` #{fields[k]*" "}" if k != self.primary_key[0]
226
+ string += "#{Ink::Database.database.primary_key_autoincrement(k)*" "}" if k == self.primary_key[0]
227
+ string += "," if i < fields.keys.length - 1
228
+ end
229
+
230
+ if self.respond_to? :foreign
231
+ foreign = self.foreign
232
+ for i in 0...foreign.keys.length
233
+ k = foreign.keys[i]
234
+ v = foreign[k]
235
+ fk = Model::classname(k).foreign_key
236
+ string += ",`#{fk[0]}` #{fk[1]}" if fk.length > 0 and (v == "one_many" or (v == "one_one" and (self.name <=> k) < 0))
237
+
238
+ if mfk.length > 0 and fk.length > 1 and v == "many_many" and (self.name <=> k) < 0
239
+ result.push "CREATE TABLE #{Model::str_to_tablename(self.name)}_#{Model::str_to_tablename(k)} (#{Ink::Database.database.primary_key_autoincrement*" "}, `#{mfk[0]}` #{mfk[1]}, `#{fk[0]}` #{fk[1]});"
240
+ end
241
+ end
242
+ end
243
+ string += ");"
244
+ result.push string
245
+ result
246
+ end
247
+
248
+ # Class method
249
+ #
250
+ # This will check the parent module for existing classnames
251
+ # that match the input of the str parameter.
252
+ # [param str:] some string
253
+ # [returns:] valid classname or nil
254
+ def self.str_to_classname(str)
255
+ ((Module.const_get str.capitalize).is_a? Class) ? str.capitalize : nil
256
+ end
257
+
258
+ # Class method
259
+ #
260
+ # This will check the parent module for existing classnames
261
+ # that match the input of the str parameter. Once found, it
262
+ # converts the string into the matching tablename.
263
+ # [param str:] some string
264
+ # [returns:] valid tablename or nil
265
+ def self.str_to_tablename(str)
266
+ ((Module.const_get str.capitalize).is_a? Class) ? str.downcase : nil
267
+ end
268
+
269
+ # Class method
270
+ #
271
+ # This will check the parent module for existing classnames
272
+ # that match the input of the str parameter. Once found, it
273
+ # returns the class, not the string of the class.
274
+ # [param str:] some string
275
+ # [returns:] valid class or nil
276
+ def self.classname(str)
277
+ ((Module.const_get str.capitalize).is_a? Class) ? (Module.const_get str.capitalize) : nil
278
+ end
279
+
280
+ # Class method
281
+ #
282
+ # This will find the primary key, as defined in the fields class
283
+ # method.
284
+ # [returns:] Array of the form: key name, key type or empty
285
+ def self.primary_key
286
+ if self.respond_to? :fields
287
+ pk = nil
288
+ pktype = nil
289
+ self.fields.each do |k,v|
290
+ if v.is_a?(String) and v == "PRIMARY KEY"
291
+ pk = k
292
+ pktype = Ink::Database.database.primary_key_autoincrement(k)[1]
293
+ end
294
+ end
295
+ return [pk, pktype]
296
+ end
297
+ return []
298
+ end
299
+
300
+ # Class method
301
+ #
302
+ # This will create the foreign key from the defined primary key
303
+ # [returns:] Array of the form: key name, key type or empty
304
+ def self.foreign_key
305
+ pk = self.primary_key
306
+ return (pk) ? ["#{self.str_to_tablename(self.name)}_#{pk[0]}", pk[1]] : []
307
+ end
308
+
309
+ end
310
+
311
+ end