kansas 0.9.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 63cde5e095dfff7d050db228382340f5e55e58f4
4
+ data.tar.gz: 98282e840a73eeddad1968cbb661aa0ae4459640
5
+ SHA512:
6
+ metadata.gz: 8b198c707bca9ff84f8a4b8fbb576f7a55f94031c2fd10695e5def07192dd66623df6a7afc171fc3fbbc6460457e9d344e1c01a30c0bcfdc7c0882275e2559b8
7
+ data.tar.gz: ca59bd739d3d4b8fb98e89b8a837486f223155b71211ef33b2fd9a612575a124324001bdae6abd124e7890c4828a7a6b974cab689d8c372950af2f79054b9bee
@@ -0,0 +1,13 @@
1
+ # Contributor Code of Conduct
2
+
3
+ As contributors and maintainers of this project, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities.
4
+
5
+ We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, ethnicity, age, or religion.
6
+
7
+ Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct.
8
+
9
+ Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team.
10
+
11
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers.
12
+
13
+ This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/)
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in kansas.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Kirk Haines
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,97 @@
1
+ # Kansas
2
+
3
+ Kansas is an object-releational mapping library for Ruby. It has been around and in use since 2004 in many production projects, but has received very little open source exposure in that time, being used almost exclusively in closed source commercial projects.
4
+
5
+ Kansas's original design leveraged DBI, and provided an ORM mapping with a ruby based query syntax.
6
+
7
+ I am putting it on github, and starting the repo with a version of kansas from circa 2006-2007ish times. It will then be updated with a somewhat newer version that implements a few minor changes. The library will be converted to a modern Ruby packaging structure, and modernized.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ gem 'kansas'
15
+ ```
16
+
17
+ And then execute:
18
+
19
+ $ bundle
20
+
21
+ Or install it yourself as:
22
+
23
+ $ gem install kansas
24
+
25
+ ## Usage
26
+
27
+ ```ruby
28
+ ksdbh = KSDatabase.new("dbi:Mysql:DBNAME:DBHOST", USERNAME, PASSWORD)
29
+ ksdbh.map_all_tables
30
+ ```
31
+
32
+ ```ruby
33
+ # Equivalent to:
34
+ # select * from table_as_classname
35
+ all_rows = ksdbh.select(:TableAsClassname)
36
+ ```
37
+
38
+ ```ruby
39
+ # Equivalent to:
40
+ # select * from table_as_classname where field = "blah"
41
+ some_rows = ksdbh.select(:TableAsClassname) {|t| t.field == "blah"}
42
+ ```
43
+
44
+ ```ruby
45
+ # Equivalent to:
46
+ # select * from table table_as_classname where field = "blah" and field2 > 100 and field3 between(3,6)
47
+ some_rows = ksdbh.select(:TableAsClassname) {|t| ( t.field == "blah" ) & ( t.field2 > 100 ) & (t.field3.between(3,6) }
48
+ ```
49
+
50
+ ```ruby
51
+ first_record = some_rows.first
52
+ # Unless autocommit is set to false, this will update the database synchronously
53
+ first_record.field4 = "more stuff"
54
+ ```
55
+
56
+ ```ruby
57
+ # Implied transactions
58
+ ksdbh.autcommit = false
59
+ first_record.field = "bleargh"
60
+ first_record.field2 = 1000
61
+ $ksdbh.commit # use ksdbh.rollback to revert the changes, instead
62
+ ```
63
+
64
+ ```ruby
65
+ # Explicit transitions via a block
66
+ ksdbh.transaction do
67
+ first_record.field3 = "xyzzy"
68
+ first_record.field4 = do_calculation
69
+
70
+ if first_record.field4 < 100
71
+ ksdbh.rollback
72
+ else
73
+ ksdbh.commit
74
+ end
75
+ end
76
+ ```
77
+
78
+ ```ruby
79
+ # Delete a record
80
+ first_record.delete
81
+ ```
82
+
83
+ ## Development
84
+
85
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
86
+
87
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
88
+
89
+ ## Contributing
90
+
91
+ Bug reports and pull requests are welcome on GitHub at https://github.com/wyhaines/kansas. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
92
+
93
+
94
+ ## License
95
+
96
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
97
+
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ puts "TESTING: #{FileList['test/**/*_test.rb'].inspect}"
6
+ t.libs << "test"
7
+ t.libs << "lib"
8
+ t.test_files = FileList['test/**/*_test.rb']
9
+ end
10
+
11
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "kansas"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/kansas.gemspec ADDED
@@ -0,0 +1,35 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'kansas/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "kansas"
8
+ spec.version = Kansas::VERSION
9
+ spec.authors = ["Kirk Haines"]
10
+ spec.email = ["wyhaines@gmail.com"]
11
+
12
+ spec.summary = %q{Object/Relational mapper with a Ruby based query syntax}
13
+ spec.description = %q{Object/Relational mapper with a Ruby based query syntax}
14
+ spec.homepage = "https://github.com/wyhaines/kansas"
15
+ spec.license = "MIT"
16
+
17
+ # Prevent pushing this gem to RubyGems.org by setting 'allowed_push_host', or
18
+ # delete this section to allow pushing this gem to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = "https://rubygems.org"
21
+ else
22
+ raise "RubyGems 2.0 or newer is required to protect against public gem pushes."
23
+ end
24
+
25
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
26
+ spec.bindir = "exe"
27
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ["lib"]
29
+
30
+ spec.add_development_dependency "bundler", "~> 1.10"
31
+ spec.add_development_dependency "rake", "~> 10.0"
32
+ spec.add_development_dependency "minitest"
33
+ spec.add_runtime_dependency "dbi"
34
+ spec.add_development_dependency "dbd-mysql" # can be changes to a different db driver, or commented out if not running tests.
35
+ end
data/lib/kansas.rb ADDED
@@ -0,0 +1,42 @@
1
+ require 'dbi'
2
+ require 'dbi/sql'
3
+ require 'kansas/patch_dbi'
4
+ require 'kansas/Database'
5
+ require 'kansas/Table'
6
+ require 'kansas/TableClass'
7
+ require 'kansas/ToOne'
8
+ require 'kansas/ToMany'
9
+ require 'kansas/BelongsTo'
10
+ require 'kansas/Expression'
11
+
12
+ module Kansas
13
+ def Kansas.log(*args)
14
+ $stderr.puts(*args)
15
+ end
16
+ end
17
+
18
+
19
+ module DBI
20
+ module SQL
21
+ module BasicQuote
22
+ class Coerce
23
+ def as_timestamp(str)
24
+ return nil if str.nil? or str.empty?
25
+ ary = ParseDate.parsedate(str)
26
+ time = nil
27
+ begin
28
+ time = ::Time.local(*(ary[0,6]))
29
+ if m = /^((\+|\-)\d+)(:\d+)?$/.match(ary[6])
30
+ diff = m[1].to_i * 3600 # seconds per hour
31
+ time -= diff
32
+ time.localtime
33
+ end
34
+ rescue Exception
35
+ time = nil
36
+ end
37
+ time ? DBI::Timestamp.new(time) : DBI::Timestamp.new(0)
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,23 @@
1
+ class KSBelongsTo
2
+
3
+ attr_reader :foreignTable, :foreignField, :localTable, :localField
4
+
5
+ def initialize(*args)
6
+ @localTable, @foreignTable, @localField, @foreignField = args
7
+ @foreignField = @foreignTable.primaries.first unless @foreignField != ''
8
+ end
9
+
10
+ def get(parent)
11
+ parent.context.get_object(@foreignTable, [parent.row[@localField]])
12
+ end
13
+
14
+ def set(parent, child)
15
+ parent.row[@localField] = child.row[@foreignField]
16
+ parent.changed
17
+ end
18
+
19
+ def join
20
+ "#{@localTable.table_name}.#{@localField} = #{@foreignTable.table_name}.#{@foreignField}"
21
+ end
22
+
23
+ end
@@ -0,0 +1,659 @@
1
+ require 'forwardable'
2
+
3
+ #####
4
+ # Note: If you are reading the code, be aware that a lot of this is going to
5
+ # be changing. Better exception handling (really, any exception handling
6
+ # will be better exception handling), better code organization, more comments,
7
+ # and more consistency in the use of snake_case.
8
+ # If you want to spend a little time helping out, I welcome patches.
9
+ # khaines@enigo.com
10
+ #####
11
+
12
+ #####
13
+ # Note: One fairly highly ranked item on the todo list is to add capability
14
+ # to join on fields other than primary key fields. I also want to be able
15
+ # to define views -- objects that present a joined view of part or all
16
+ # of two or more tables. Along with this will probably come the ability,
17
+ # when mapping a table, to specify specific columns to map or not to
18
+ # map.
19
+ #####
20
+
21
+ # Escapes quotes inside of a SQL statement.
22
+ # ToDo: Make this obsolete by having all SQL statements use bind variables.
23
+
24
+ def sql_escape(val)
25
+ "'" + val.to_s.gsub("'", "\\\\'") + "'"
26
+ end
27
+
28
+ class String
29
+
30
+ # Converts a string so that each initial letter after an underscore is
31
+ # an uppercase letter.
32
+
33
+ def mixcase
34
+ if self == upcase
35
+ res = downcase
36
+ else
37
+ res = dup
38
+ end
39
+ res.gsub(/_(\w)/) {$1.upcase}
40
+ end
41
+
42
+ end
43
+
44
+ class KSBadTable < Exception;end
45
+ class KSNoTable < Exception;end
46
+ class KSBadFieldName < Exception;end
47
+ class KSRollback < Exception;end
48
+ class KSNull < NilClass;end
49
+
50
+ require 'drb'
51
+
52
+ class KSDatabase
53
+
54
+ include DRbUndumped
55
+
56
+ extend Forwardable
57
+ def_delegators('@dbh','connected?','disconnect','do','ping','prepare','execute','select_all','select_one','quote','tables','columns')
58
+
59
+ @@partial_to_complete_map = {}
60
+
61
+ def KSDatabase.partial_to_complete_map
62
+ @@partial_to_complete_map
63
+ end
64
+
65
+ attr_reader :options, :tables
66
+ attr_accessor :dbh
67
+
68
+ # Initialize Kansas. Kansas optionally accepts a database handle. This
69
+ # allows an external system, such as a database connection pool, to be
70
+ # used with Kansas.
71
+
72
+ def initialize(arg1 = nil, arg2 = nil, arg3 = nil, arg4 = nil)
73
+ # If a database handle is passed in as the first arg, then the second should be an options has.
74
+ # If the first arg is a string, then it is assumed to be a dsn and the next two args will be
75
+ # username and password, followed by options.
76
+ if String === arg1
77
+ dsn = arg1
78
+ username = arg2
79
+ password = arg3
80
+ options = arg4 ? arg4 : {}
81
+ else
82
+ dbh = arg1
83
+ options = arg2 ? arg2 : {}
84
+ end
85
+
86
+ if dbh
87
+ @dbh = dbh
88
+ @options = options
89
+ elsif dsn
90
+ connect_using(dsn,username,password,options)
91
+ new_dbh
92
+ else
93
+ @options = options
94
+ end
95
+
96
+ @objects = {}
97
+ @changed = []
98
+ @remote_to_local_map = {}
99
+ set_autocommit
100
+ end
101
+
102
+ # Set the parameters used for making a DBI connection.
103
+
104
+ def connect_using(dsn, user, password, options = {})
105
+ @dsn = dsn
106
+ @user = user
107
+ @password = password
108
+ @options = options
109
+ end
110
+ alias :connectUsing :connect_using
111
+
112
+ def new_dbh
113
+ @dbh = DBI.connect(@dsn, @user, @password) if @dsn
114
+ end
115
+ alias :newDbh :new_dbh
116
+
117
+ def query(*args,&blk)
118
+ error_recovery ||= false
119
+ yield(@dbh,*args)
120
+ rescue Exception => e
121
+ unless error_recovery
122
+ error_recovery = true
123
+ new_dbh
124
+ retry
125
+ else
126
+ raise e
127
+ end
128
+ end
129
+
130
+ def autocommit?
131
+ @options[:autocommit]
132
+ end
133
+
134
+ def autocommit
135
+ autocommit?
136
+ end
137
+
138
+ def autocommit=(setting)
139
+ set_autocommit setting
140
+ end
141
+
142
+ def set_autocommit(setting = nil)
143
+ if setting != nil
144
+ @options[:autocommit] = setting ? true : false
145
+ else
146
+ @options[:autocommit] = true
147
+ end
148
+ end
149
+ alias :setAutocommit :set_autocommit
150
+
151
+ def has_object?(table, key)
152
+ if tableHash = @objects[table]
153
+ tableHash[key]
154
+ else
155
+ @objects[table] = {}
156
+ nil
157
+ end
158
+ end
159
+
160
+ def get_object(table, key)
161
+ unless obj = has_object?(table,key)
162
+ if obj = load_object(table,key,true)
163
+ obj.set_serialized
164
+ register_object(obj)
165
+ end
166
+ end
167
+ obj
168
+ end
169
+
170
+ # Updates the record in the database for the object.
171
+
172
+ def store_object(object)
173
+ #sql = build_update(object) + build_where(object)
174
+ #query {|dbh| dbh.do(sql)}
175
+ rs = build_update(object)
176
+ sql = rs.first + build_where(object)
177
+ query {|dbh| dbh.do(sql,*rs.last)}
178
+ end
179
+
180
+ def get_table_from_string(table)
181
+ if KSDatabase.partial_to_complete_map.has_key?(table)
182
+ table = self.class.const_get KSDatabase.partial_to_complete_map[table]
183
+ elsif @remote_to_local_map.has_key?(table)
184
+ table = self.class.const_get @remote_to_local_map[table]
185
+ else
186
+ nil
187
+ end
188
+ end
189
+
190
+ def check_query_args(*args)
191
+ tables = []
192
+ read_only = false
193
+ final_value = args.length > 1 ? args.last : nil
194
+ sql = ''
195
+
196
+ args.each do |table|
197
+ if table == :__read_only__
198
+ read_only = true
199
+ next
200
+ end
201
+
202
+ final_flag = (table == final_value)
203
+ table = table.to_s if table.kind_of?(Symbol)
204
+ if table.kind_of?(String)
205
+ tclass = get_table_from_string(table)
206
+ if tclass
207
+ table = tclass
208
+ else
209
+ if final_flag
210
+ sql << table
211
+ else
212
+ self.map_all_tables
213
+ table = get_table_from_string(table)
214
+ end
215
+ end
216
+ end
217
+ tables << table if table
218
+ end
219
+
220
+ unless sql != ''
221
+ if tables.length == 0
222
+ raise KSNoTable, "KSNoTable: No valid tables were found to operate against."
223
+ else
224
+ tables.each do |t|
225
+ unless t.respond_to?(:is_a_kstable?)
226
+ raise KSBadTable.new,"KSBadTable: #{t} is not a valid table."
227
+ end
228
+ end
229
+ end
230
+ end
231
+
232
+ [tables,sql,read_only]
233
+ end
234
+
235
+
236
+ # Selects from a table. If a block is given, that block will be used
237
+ # to construct the SQL statement. Otherwise, if a SQL statement was
238
+ # given as the second argument, that statement will be used. If no
239
+ # statement was given, the default is to select * from the table provided
240
+ # as the first argument. The table can either be specified by passing
241
+ # the class for the table to use (e.x. KSDatabase::Students), by passing
242
+ # the remote name (i.e. the name of the actual database table), or by
243
+ # passing the local name for the table that was given when mapping the
244
+ # table. Most of the time the local name or the remote name should be
245
+ # used as it works just as well as the full class name.
246
+ #
247
+ # Note that select'd currently operates outside of the local rollback cache.
248
+ def select(*args)
249
+ error_recovery ||= false
250
+ results = []
251
+ tables,sql,read_only = check_query_args(*args)
252
+
253
+ if block_given?
254
+ context = KSExpression::Context.new(*tables)
255
+ yield_args = tables.collect {|t| KSTableExpr.new(t,context)}
256
+ queryExpr = yield *yield_args
257
+ sql = queryExpr.select_sql
258
+ elsif sql == ''
259
+ sql = "SELECT * FROM #{tables.collect {|t| t.table_name}.join(',')}"
260
+ end
261
+
262
+ Kansas::log("select: '#{sql}'") if $DEBUG
263
+
264
+ rawResults = query do |dbh|
265
+ dbh.select_all(sql) do |row|
266
+ results << add_object(tables[0].new.load(row.to_h, self, read_only))
267
+ end
268
+ end
269
+
270
+ results
271
+ rescue DBI::DatabaseError => e
272
+ unless error_recovery
273
+ error_recovery = true
274
+ new_dbh
275
+ retry
276
+ else
277
+ raise e
278
+ end
279
+ end
280
+
281
+ # Return only one value from a query.
282
+
283
+ def select_first(*args,&blk)
284
+ select(*args) {|*yargs| blk.call(*yargs)}.first
285
+ end
286
+
287
+ # Returns only a count of the records selected by the query.
288
+ def count(*args)
289
+ error_recovery ||= false
290
+ results = nil
291
+ tables,sql = check_query_args(*args)
292
+
293
+ if block_given?
294
+ context = KSExpression::Context.new(*tables)
295
+ yield_args = tables.collect {|t| KSTableExpr.new(t,context)}
296
+ queryExpr = yield *yield_args
297
+ sql = queryExpr.count_sql
298
+ elsif sql == ''
299
+ sql = "SELECT count(*) FROM #{tables.collect {|t| t.table_name}.join(',')}"
300
+ end
301
+
302
+ Kansas::log("select: '#{sql}'") if $DEBUG
303
+
304
+ rawResults = query do |dbh|
305
+ dbh.select_all(sql) do |row|
306
+ results = row[0]
307
+ end
308
+ end
309
+
310
+ results
311
+ rescue DBI::DatabaseError => e
312
+ unless error_recovery
313
+ error_recovery = true
314
+ new_dbh
315
+ retry
316
+ else
317
+ raise e
318
+ end
319
+ end
320
+
321
+ # A convenience usage of count(). Returns true if the query would return
322
+ # any number of rows, or false if the query would not return any rows.
323
+
324
+ def exists?(*args,&block)
325
+ if block
326
+ 0 < count(*args) {|*blockargs| block.call(*blockargs)}
327
+ else
328
+ 0 < count(*args)
329
+ end
330
+ end
331
+ # Delete removes one or more rows from the database. It _also_ returns
332
+ # an array of objects matching the rows deleted. If Autocommit is not on,
333
+ # it will return the array of rows that would be deleted _at the time of
334
+ # the call_ and will defer the actual deletion from the database until a
335
+ # commit() is invoked.
336
+
337
+ def delete(table, sql=nil)
338
+ error_recovery ||= false
339
+ table = table.to_s if table.kind_of?(Symbol)
340
+
341
+ if table.kind_of?(String)
342
+ if @remote_to_local_map.has_key?(table)
343
+ table = self.class.const_get @remote_to_local_map[table]
344
+ elsif KSDatabase.partial_to_complete_map.has_key?(table)
345
+ table = self.class.const_get KSDatabase.partial_to_complete_map[table]
346
+ else
347
+ table = nil
348
+ end
349
+ end
350
+
351
+ unless Class === table
352
+ raise KSBadTable,"KSBadTable: #{table} is not a valid table."
353
+ end
354
+
355
+ if block_given?
356
+ queryExpr = yield KSTableExpr.new(table)
357
+ select_sql = queryExpr.sql
358
+ delete_sql = queryExpr.delete_sql
359
+ elsif sql == nil
360
+ select_sql = "SELECT * FROM #{table.table_name}"
361
+ delete_sql = "DELETE FROM #{table.table_name}"
362
+ end
363
+
364
+ Kansas::log("delete: '#{sql}'") if $DEBUG
365
+
366
+ results = []
367
+ rawResults = query do |dbh|
368
+ dbh.select_all(select_sql) do |row|
369
+ results.push add_object(table.new.load(row.to_h, self))
370
+ end
371
+ end
372
+
373
+ if autocommit?
374
+ query {|dbh| dbh.do delete_sql}
375
+ else
376
+ changed delete_sql
377
+ end
378
+
379
+ results
380
+ rescue DBI::DatabaseError => e
381
+ unless error_recovery
382
+ error_recovery = true
383
+ new_dbh
384
+ retry
385
+ else
386
+ raise e
387
+ end
388
+ end
389
+
390
+ def delete_one(object)
391
+ sql = "DELETE FROM #{object.table_name} #{build_where(object)}"
392
+ result = query {|dbh| dbh.do sql}
393
+ unregister_object object
394
+ result
395
+ end
396
+
397
+ # Use decoupled_object() to create an object that is not yet connected to
398
+ # a database.
399
+
400
+ def decoupled_object(table, rowdata = {})
401
+ table = table.to_s if table.is_a? Symbol
402
+ if table.kind_of? String
403
+ if @remote_to_local_map.has_key?(table)
404
+ table = self.class.const_get @remote_to_local_map[table]
405
+ elsif KSDatabase.partial_to_complete_map.has_key?(table)
406
+ table = self.class.const_get KSDatabase.partial_to_complete_map[table]
407
+ else
408
+ table = nil
409
+ end
410
+ end
411
+
412
+ rowdata.each do |k,v|
413
+ if k.is_a?(Symbol)
414
+ rowdata.delete(k)
415
+ rowdata[k.to_s] = v
416
+ end
417
+ end
418
+ table.new.load(rowdata)
419
+ end
420
+
421
+ # Use new_object() to create new records within a table.
422
+
423
+ def new_object(table,rowdata = {})
424
+ object = decoupled_object(table,rowdata)
425
+ register_object object
426
+ object.context = self
427
+ if autocommit?
428
+ insert_object object
429
+ object.set_serialized
430
+ else
431
+ changed object
432
+ end
433
+
434
+ object
435
+ end
436
+ alias :newObject :new_object
437
+
438
+ # Take a table object that is not tied to the database and tie it.
439
+
440
+ def register_and_insert_object(object)
441
+ register_object object
442
+ object.context = self
443
+ insert_object object
444
+ object.set_serialized
445
+ object
446
+ end
447
+ # Block oriented interface to commit/rollback.
448
+
449
+ def transaction
450
+ old_autocommit = autocommit?
451
+ old_dbh_autocommit = @dbh['AutoCommit']
452
+ set_autocommit false
453
+ @dbh['AutoCommit'] = false
454
+ @transaction_flag = true
455
+ begin
456
+ commit
457
+ yield self
458
+ commit
459
+ rescue KSRollback
460
+ # We caught a Rollback; this isn't really an error.
461
+ @transaction_flag = false
462
+ rollback
463
+ rescue Exception
464
+ # Something bad happened.
465
+ @transaction_flag = false
466
+ rollback
467
+ raise
468
+ end
469
+ set_autocommit(old_autocommit)
470
+ @dbh['AutoCommit'] = old_dbh_autocommit
471
+ end
472
+
473
+ # Commit transaction to the database.
474
+
475
+ def commit
476
+ query do |dbh|
477
+ dbh.transaction do
478
+ @changed.uniq.each do |o|
479
+ if o.kind_of?(String)
480
+ query {|innerdbh| innerdbh.do o.to_s}
481
+ elsif o.pending_deletion?
482
+ delete_one o
483
+ elsif o.serialized?
484
+ store_object o
485
+ else
486
+ insert_object o
487
+ end
488
+ o.rollback_buffer = []
489
+ end
490
+ @changed.clear
491
+ end
492
+ dbh.commit
493
+ end
494
+ end
495
+
496
+ # Rollback uncommitted transactions on an object so that they never get
497
+ # to the database.
498
+
499
+ def rollback
500
+ if @transaction_flag
501
+ raise KSRollback
502
+ else
503
+ @changed.uniq.each do |o|
504
+ next if o.kind_of?(String)
505
+ o.rollback
506
+ end
507
+ @changed.clear
508
+ end
509
+ query {|dbh| dbh.rollback}
510
+ end
511
+
512
+ def changed(obj)
513
+ @changed << obj
514
+ if autocommit?
515
+ commit
516
+ end
517
+ end
518
+
519
+ def all_tables
520
+ query do |dbh|
521
+ dbh.tables.each do |table_name|
522
+ Kansas.log("Defining table: #{table_name} as #{canonical(table_name)}") if $DEBUG
523
+ table(canonical(table_name), table_name)
524
+ end
525
+ end
526
+ end
527
+ alias :map_all_tables :all_tables
528
+
529
+ def table(local_name, remote_name=nil)
530
+ local_name = local_name.to_s
531
+ remote_name = local_name unless remote_name
532
+ remote_name = remote_name.to_s
533
+
534
+ old_local_name = local_name
535
+ local_name = make_constant_name(local_name)
536
+ table = Class.new(KSTable)
537
+ table.const_set('Name', remote_name)
538
+ table.const_set('Database', self)
539
+ table.all_fields
540
+
541
+ @remote_to_local_map[remote_name] = local_name
542
+ KSDatabase.partial_to_complete_map[old_local_name] = local_name
543
+ addTable(local_name, table)
544
+ end
545
+ alias :map_table :table
546
+
547
+ private
548
+
549
+ def addTable(name, table)
550
+ self.class.const_set(name, table) unless self.class.const_defined?(name)
551
+ if defined?(@tables) && @tables
552
+ @tables[name] = table
553
+ else
554
+ @tables = {name => table}
555
+ end
556
+ end
557
+
558
+ def canonical(table_name)
559
+ newName = table_name.mixcase
560
+ make_constant_name(newName)
561
+ end
562
+
563
+ def make_constant_name(name)
564
+ name = name.dup
565
+ name[0,1] = name[0,1].upcase
566
+ name
567
+ end
568
+
569
+ # Inserts a new record in the database for the object.
570
+ # Because auto increment fields and timestamp fields on MySQL, and
571
+ # presumably other field types on other dbs, require that no explicit
572
+ # overriding value be inserted into the field, any field that has a nil
573
+ # value is omitted from the insert statement. To explicity place a
574
+ # null value in a field when inserting into the database, use
575
+ # KSNull as the value of the field.
576
+
577
+ def insert_object(object)
578
+ unless object.serialized?
579
+ table = object.class
580
+ fields = table.fields.values.collect {|f| object.row[f] != nil ? f : nil}.compact
581
+ sql = "INSERT into #{table.table_name} (#{fields.join(',')}) VALUES ("
582
+ sql << fields.collect {|f| '?'}.join(',') << ')'
583
+ query {|dbh| dbh.do(sql,*fields.collect {|f| object.row[f] == KSNull ? nil : object.row[f]})}
584
+ end
585
+ end
586
+
587
+ def add_object(object)
588
+ if cachedObj = has_object?(object.class, object.key)
589
+ cachedObj.row = object.row
590
+ cachedObj
591
+ else
592
+ object.set_serialized
593
+ register_object(object)
594
+ end
595
+ end
596
+
597
+ def register_object(object)
598
+ key = object.key
599
+ has_object?(object.class,key)
600
+ @objects[object.class][key] = object
601
+ end
602
+
603
+ def unregister_object(object)
604
+ key = object.key
605
+ @objects[object.class].delete key
606
+ end
607
+
608
+ def load_object(object, key, tableflag = false)
609
+ table = tableflag ? object : object.class
610
+ query = "SELECT * FROM #{table.table_name}" + build_where_from_key(object, key, tableflag)
611
+ row = nil
612
+ row = query {|dbh| dbh.select_one(query)}
613
+ if row
614
+ table.new.load(row.to_h, self)
615
+ else
616
+ nil
617
+ end
618
+ end
619
+
620
+ def build_where(object)
621
+ build_where_from_key(object, object.key)
622
+ end
623
+
624
+ def build_where_from_key(object, key, tableflag = false)
625
+ table = tableflag ? object : object.class
626
+ where = " WHERE "
627
+ where_ary = []
628
+ fields = table.primaries
629
+
630
+ fields.each_index do |i|
631
+ if !tableflag && object.rollback_hash[fields[i]]
632
+ val = object.rollback_hash[fields[i]].last
633
+ else
634
+ val = key[i]
635
+ end
636
+
637
+ where_ary.push "#{fields[i]} = #{sql_escape(val)}"
638
+ end
639
+ where + where_ary.join(' AND ')
640
+ end
641
+
642
+ def build_update(object)
643
+ vals = []
644
+ update = "UPDATE #{object.table_name} SET "
645
+ object.row.each do |field, val|
646
+ #update << " #{field} = #{sql_escape(val)},"
647
+ update << " #{field} = ?,"
648
+ vals << val
649
+ end
650
+ [update.chop!,vals]
651
+ #update.chop!
652
+ end
653
+
654
+ def build_select(table, criteria)
655
+ criteriaString = "WHERE #{criteria}" if criteria
656
+ "SELECT * FROM #{table.table_name} #{criteriaString}"
657
+ end
658
+
659
+ end