og 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (51) hide show
  1. data/ProjectInfo +2 -5
  2. data/README +2 -0
  3. data/doc/AUTHORS +4 -1
  4. data/doc/RELEASES +53 -0
  5. data/examples/run.rb +2 -2
  6. data/lib/{og/mixin → glue}/hierarchical.rb +19 -19
  7. data/lib/{og/mixin → glue}/optimistic_locking.rb +1 -1
  8. data/lib/glue/orderable.rb +235 -0
  9. data/lib/glue/revisable.rb +2 -0
  10. data/lib/glue/taggable.rb +176 -0
  11. data/lib/{og/mixin/taggable.rb → glue/taggable_old.rb} +6 -0
  12. data/lib/glue/timestamped.rb +37 -0
  13. data/lib/{og/mixin → glue}/tree.rb +3 -8
  14. data/lib/og.rb +21 -20
  15. data/lib/og/collection.rb +15 -1
  16. data/lib/og/entity.rb +256 -114
  17. data/lib/og/manager.rb +60 -27
  18. data/lib/og/{mixin/schema_inheritance_base.rb → markers.rb} +5 -2
  19. data/lib/og/relation.rb +70 -74
  20. data/lib/og/relation/belongs_to.rb +5 -3
  21. data/lib/og/relation/has_many.rb +1 -0
  22. data/lib/og/relation/joins_many.rb +5 -4
  23. data/lib/og/store.rb +25 -46
  24. data/lib/og/store/alpha/filesys.rb +1 -1
  25. data/lib/og/store/alpha/kirby.rb +30 -30
  26. data/lib/og/store/alpha/memory.rb +49 -49
  27. data/lib/og/store/alpha/sqlserver.rb +7 -7
  28. data/lib/og/store/kirby.rb +38 -38
  29. data/lib/og/store/mysql.rb +43 -43
  30. data/lib/og/store/psql.rb +222 -53
  31. data/lib/og/store/sql.rb +165 -105
  32. data/lib/og/store/sqlite.rb +29 -25
  33. data/lib/og/validation.rb +24 -14
  34. data/lib/{vendor → og/vendor}/README +0 -0
  35. data/lib/{vendor → og/vendor}/kbserver.rb +1 -1
  36. data/lib/{vendor → og/vendor}/kirbybase.rb +230 -79
  37. data/lib/{vendor → og/vendor}/mysql.rb +0 -0
  38. data/lib/{vendor → og/vendor}/mysql411.rb +0 -0
  39. data/test/og/mixin/tc_hierarchical.rb +1 -1
  40. data/test/og/mixin/tc_optimistic_locking.rb +1 -1
  41. data/test/og/mixin/tc_orderable.rb +1 -1
  42. data/test/og/mixin/tc_taggable.rb +2 -2
  43. data/test/og/mixin/tc_timestamped.rb +2 -2
  44. data/test/og/tc_finder.rb +33 -0
  45. data/test/og/tc_inheritance.rb +2 -2
  46. data/test/og/tc_scoped.rb +45 -0
  47. data/test/og/tc_store.rb +1 -7
  48. metadata +21 -18
  49. data/lib/og/mixin/orderable.rb +0 -174
  50. data/lib/og/mixin/revisable.rb +0 -0
  51. data/lib/og/mixin/timestamped.rb +0 -24
@@ -14,19 +14,19 @@ require 'og/store/sql'
14
14
 
15
15
  class SQLite3::ResultSet
16
16
  alias_method :blank?, :eof?
17
-
17
+
18
18
  def each_row
19
19
  each do |row|
20
20
  yield(row, 0)
21
21
  end
22
22
  end
23
-
23
+
24
24
  def first_value
25
25
  val = self.next[0]
26
26
  close
27
27
  return val
28
- end
29
-
28
+ end
29
+
30
30
  alias_method :fields, :columns
31
31
  end
32
32
 
@@ -39,11 +39,12 @@ module Og
39
39
  class SqliteStore < SqlStore
40
40
 
41
41
  # Override if needed.
42
-
42
+
43
43
  def self.db_filename(options)
44
+ options[:name] ||= 'data'
44
45
  "#{options[:name]}.db"
45
46
  end
46
-
47
+
47
48
  def self.destroy(options)
48
49
  begin
49
50
  FileUtils.rm(db_filename(options))
@@ -53,6 +54,9 @@ class SqliteStore < SqlStore
53
54
  end
54
55
  end
55
56
 
57
+ # Initialize the Sqlite store.
58
+ # This store provides a default name.
59
+
56
60
  def initialize(options)
57
61
  super
58
62
  @conn = SQLite3::Database.new(self.class.db_filename(options))
@@ -62,9 +66,9 @@ class SqliteStore < SqlStore
62
66
  @conn.close
63
67
  super
64
68
  end
65
-
69
+
66
70
  def enchant(klass, manager)
67
- if klass.ann.this.primary_key.symbol == :oid
71
+ if klass.ann.self.primary_key.symbol == :oid
68
72
  unless klass.properties.include? :oid
69
73
  klass.property :oid, Fixnum, :sql => 'integer PRIMARY KEY'
70
74
  end
@@ -91,12 +95,12 @@ class SqliteStore < SqlStore
91
95
  @conn.transaction if @transaction_nesting < 1
92
96
  @transaction_nesting += 1
93
97
  end
94
-
98
+
95
99
  def commit
96
100
  @transaction_nesting -= 1
97
101
  @conn.commit if @transaction_nesting < 1
98
102
  end
99
-
103
+
100
104
  def rollback
101
105
  @transaction_nesting -= 1
102
106
  @conn.rollback if @transaction_nesting < 1
@@ -117,18 +121,18 @@ private
117
121
  fields = fields_for_class(klass)
118
122
 
119
123
  sql = "CREATE TABLE #{klass::OGTABLE} (#{fields.join(', ')}"
120
-
124
+
121
125
  # Create table constraints.
122
-
123
- if constraints = klass.ann.this[:sql_constraint]
126
+
127
+ if constraints = klass.ann.self[:sql_constraint]
124
128
  sql << ", #{constraints.join(', ')}"
125
129
  end
126
-
130
+
127
131
  sql << ");"
128
-
132
+
129
133
  # Create indices.
130
-
131
- if indices = klass.ann.this[:index]
134
+
135
+ if indices = klass.ann.self[:index]
132
136
  for data in indices
133
137
  idx, options = *data
134
138
  idx = idx.to_s
@@ -150,11 +154,11 @@ private
150
154
  raise
151
155
  end
152
156
  end
153
-
157
+
154
158
  # Create join tables if needed. Join tables are used in
155
159
  # 'many_to_many' relations.
156
-
157
- if join_tables = klass.ann.this[:join_tables]
160
+
161
+ if join_tables = klass.ann.self[:join_tables]
158
162
  for info in join_tables
159
163
  begin
160
164
  create_join_table_sql(info).each do |sql|
@@ -176,13 +180,13 @@ private
176
180
  def create_field_map(klass)
177
181
  res = @conn.query "SELECT * FROM #{klass::OGTABLE} LIMIT 1"
178
182
  map = {}
179
-
183
+
180
184
  fields = res.columns
181
185
 
182
186
  res.fields.size.times do |i|
183
187
  map[fields[i].intern] = i
184
188
  end
185
-
189
+
186
190
  return map
187
191
  ensure
188
192
  res.close if res
@@ -197,15 +201,15 @@ private
197
201
  props << Property.new(:symbol => :ogtype, :klass => String)
198
202
  values << ", '#{klass}'"
199
203
  end
200
-
204
+
201
205
  sql = "INSERT INTO #{klass::OGTABLE} (#{props.collect {|p| field_for_property(p)}.join(',')}) VALUES (#{values})"
202
206
 
203
207
  klass.class_eval %{
204
208
  def og_insert(store)
205
- #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
209
+ #{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)}
206
210
  store.query("#{sql}").close
207
211
  @#{pk} = store.last_insert_rowid
208
- #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
212
+ #{Glue::Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)}
209
213
  end
210
214
  }
211
215
  end
@@ -14,34 +14,41 @@ module Validation
14
14
  cattr_accessor :invalid_relation, 'Invalid relations'
15
15
  end
16
16
 
17
- module MetaLanguage
18
-
19
- # Validate the value of this attribute is unique
20
- # for this class.
17
+ module ClassMethods
18
+
19
+ # Validates that the given field(s) contain unique values.
20
+ # Ensures that if a record is found with a matching value,
21
+ # that it is the same record, allowing updates.
21
22
  #
22
23
  # The Og libraries are required for this methdod to
23
24
  # work. You can override this method if you want to
24
25
  # use another OR mapping library.
25
26
  #
27
+ # === Example
28
+ #
29
+ # validate_unique :param, :msg => 'Value is already in use'
26
30
  #--
27
31
  # TODO: :unique should implicitly generate
28
32
  # validate_unique.
29
33
  #++
30
-
34
+
31
35
  def validate_unique(*params)
32
- c = {
33
- :msg => Glue::Validation::Errors.not_unique,
34
- :on => :save
36
+ c = {
37
+ :on => :save
35
38
  }
36
39
  c.update(params.pop) if params.last.is_a?(Hash)
37
40
 
38
- for name in params
39
- # FIXME: improve the generated code.
41
+ for name in params do
42
+ c[:msg] ||= "#{self.name} with this #{name} already exists"
43
+
40
44
  code = %{
41
- others = obj.class.find_by_#{name}(obj.#{name})
42
- unless (others.is_a?(Array) and others.empty?)
43
- errors.add(:#{name}, '#{c[:msg]}')
44
- end;
45
+ others = obj.class.send(:find_by_#{name}, obj.send(:#{name}))
46
+
47
+ unless others.empty?
48
+ if (others.size != 1) or (others[0].oid != obj.oid)
49
+ errors.add(:#{name}, '#{c[:msg]}')
50
+ end
51
+ end
45
52
  }
46
53
 
47
54
  validations! << [code, c[:on]]
@@ -75,3 +82,6 @@ module Validation
75
82
  end
76
83
 
77
84
  end
85
+
86
+ # * George Moschovitis <gm@navel.gr>
87
+ # * Bryan Soto
File without changes
@@ -11,7 +11,7 @@ port = 44444
11
11
  puts 'Initializing database server and indexes...'
12
12
 
13
13
  # Create an instance of the database.
14
- db = KirbyBase.new(:server)
14
+ db = KirbyBase.new(:server)
15
15
 
16
16
  DRb.start_service('druby://:44444', db)
17
17
 
@@ -1,4 +1,5 @@
1
1
  require 'date'
2
+ require 'time'
2
3
  require 'drb'
3
4
  require 'csv'
4
5
  require 'fileutils'
@@ -83,6 +84,31 @@ require 'yaml'
83
84
  # :manager, :person (where :manager is the field name, and :person is the
84
85
  # name of a table). See the docs for the specifics or ask Hal. :)
85
86
  #
87
+ # 2005-11-13:: Version 2.4
88
+ # * Added a new column type: :Time. Thanks to George Moschovitis for coding
89
+ # this enhancement.
90
+ # * Added more functionality to Memo and Blob fields. They are no longer
91
+ # just read-only. You can now also write to them from KirbyBase. The
92
+ # interface for Memo and Blob fields has changed because of this.
93
+ # * Added the ability to specify, when you initialize a database connection,
94
+ # a base directory where memo/blob fields will be stored.
95
+ # * Changed the way indexes are handled by KBTable in client/server mode.
96
+ # Now, when KBTable grabs an index from KBEngine, it will hold onto it and
97
+ # re-use it unless it has been modified since the last time it grabbed it.
98
+ # This speeds up subsequent queries on the same index.
99
+ # * Removed the restriction that the child table had to exist before you
100
+ # could define a Link_many field in #create_table. I did this so that
101
+ # it would possible to now define many-to-many links. See the example in
102
+ # the distribution. This also goes for Lookup fields.
103
+ # * Added two sample scripts: kbserverctl.rb and kbserver_daemon.rb, that
104
+ # show how to set up a KirbyBase server process as a Windows Service.
105
+ # Thanks to Daniel Berger for his excellent package, win32-service.
106
+ # * Thouroughly revised the manual. I used the excellent text document
107
+ # formatter, AsciiDoc. Many thanks to Stuart Rackham for developing this
108
+ # great tool.
109
+ # * Fixed a bug in KBTable#clear that was causing the recno counter not to
110
+ # be reset. Thanks to basi for this.
111
+ #
86
112
  #---------------------------------------------------------------------------
87
113
  # KirbyBase
88
114
  #---------------------------------------------------------------------------
@@ -91,7 +117,7 @@ class KirbyBase
91
117
 
92
118
  attr_reader :engine
93
119
 
94
- attr_accessor(:connect_type, :host, :port, :path, :ext)
120
+ attr_accessor(:connect_type, :host, :port, :path, :ext, :memo_blob_path)
95
121
 
96
122
  #-----------------------------------------------------------------------
97
123
  # initialize
@@ -107,14 +133,17 @@ class KirbyBase
107
133
  # (Only valid if connect_type is :client.)
108
134
  # *path*:: String specifying path to location of database tables.
109
135
  # *ext*:: String specifying extension of table files.
136
+ # *memo_blob_path*:: String specifying path to location of memo/blob
137
+ # files.
110
138
  #
111
139
  def initialize(connect_type=:local, host=nil, port=nil, path='./',
112
- ext='.tbl')
140
+ ext='.tbl', memo_blob_path='./')
113
141
  @connect_type = connect_type
114
142
  @host = host
115
143
  @port = port
116
144
  @path = path
117
145
  @ext = ext
146
+ @memo_blob_path = memo_blob_path
118
147
 
119
148
  # See if user specified any method arguments via a code block.
120
149
  yield self if block_given?
@@ -122,7 +151,7 @@ class KirbyBase
122
151
  # After the yield, make sure the user doesn't change any of these
123
152
  # instance variables.
124
153
  class << self
125
- private(:connect_type=, :host=, :path=, :ext=)
154
+ private(:connect_type=, :host=, :path=, :ext=, :memo_blob_path=)
126
155
  end
127
156
 
128
157
  # Did user supply full and correct arguments to method?
@@ -134,6 +163,7 @@ class KirbyBase
134
163
  @port.nil?
135
164
  raise "Invalid path!" if @path.nil?
136
165
  raise "Invalid extension!" if @ext.nil?
166
+ raise "Invalid memo/blob path!" if @memo_blob_path.nil?
137
167
 
138
168
  @table_hash = {}
139
169
 
@@ -144,6 +174,7 @@ class KirbyBase
144
174
  @engine = @server.engine
145
175
  @path = @server.path
146
176
  @ext = @server.ext
177
+ @memo_blob_path = @server.memo_blob_path
147
178
  else
148
179
  @engine = KBEngine.create_called_from_database_instance(self)
149
180
  end
@@ -230,8 +261,8 @@ class KirbyBase
230
261
  return @table_hash[name]
231
262
  else
232
263
  @table_hash[name] = \
233
- KBTable.create_called_from_database_instance(self,
234
- name, File.join(@path, name.to_s + @ext))
264
+ KBTable.create_called_from_database_instance(self, name,
265
+ File.join(@path, name.to_s + @ext))
235
266
  return @table_hash[name]
236
267
  end
237
268
  end
@@ -445,6 +476,13 @@ class KBEngine
445
476
  @recno_indexes.delete(tablename)
446
477
  end
447
478
 
479
+ #-----------------------------------------------------------------------
480
+ # update_recno_index
481
+ #-----------------------------------------------------------------------
482
+ def update_recno_index(table, recno, fpos)
483
+ @recno_indexes[table.name].update_index_rec(recno, fpos)
484
+ end
485
+
448
486
  #-----------------------------------------------------------------------
449
487
  # recno_index_exists?
450
488
  #-----------------------------------------------------------------------
@@ -508,20 +546,6 @@ class KBEngine
508
546
  end
509
547
  end
510
548
 
511
- #-----------------------------------------------------------------------
512
- # index_exists?
513
- #-----------------------------------------------------------------------
514
- def index_exists?(table, index_fields)
515
- @indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
516
- end
517
-
518
- #-----------------------------------------------------------------------
519
- # update_recno_index
520
- #-----------------------------------------------------------------------
521
- def update_recno_index(table, recno, fpos)
522
- @recno_indexes[table.name].update_index_rec(recno, fpos)
523
- end
524
-
525
549
  #-----------------------------------------------------------------------
526
550
  # update_to_indexes
527
551
  #-----------------------------------------------------------------------
@@ -532,6 +556,13 @@ class KBEngine
532
556
  end
533
557
  end
534
558
 
559
+ #-----------------------------------------------------------------------
560
+ # index_exists?
561
+ #-----------------------------------------------------------------------
562
+ def index_exists?(table, index_fields)
563
+ @indexes.include?("#{table.name}_#{index_fields.join('_')}".to_sym)
564
+ end
565
+
535
566
  #-----------------------------------------------------------------------
536
567
  # get_index
537
568
  #-----------------------------------------------------------------------
@@ -539,6 +570,13 @@ class KBEngine
539
570
  return @indexes["#{table.name}_#{index_name}".to_sym].get_idx
540
571
  end
541
572
 
573
+ #-----------------------------------------------------------------------
574
+ # get_index_timestamp
575
+ #-----------------------------------------------------------------------
576
+ def get_index_timestamp(table, index_name)
577
+ return @indexes["#{table.name}_#{index_name}".to_sym].get_timestamp
578
+ end
579
+
542
580
  #-----------------------------------------------------------------------
543
581
  # get_recno_index
544
582
  #-----------------------------------------------------------------------
@@ -686,6 +724,19 @@ class KBEngine
686
724
  return get_recs(table).size
687
725
  end
688
726
 
727
+ #-----------------------------------------------------------------------
728
+ # reset_recno_ctr
729
+ #-----------------------------------------------------------------------
730
+ def reset_recno_ctr(table)
731
+ with_write_locked_table(table) do |fptr|
732
+ last_rec_no, rest_of_line = get_header_record(table, fptr
733
+ ).split('|', 2)
734
+ write_header_record(table, fptr,
735
+ ['%06d' % 0, rest_of_line].join('|'))
736
+ return true
737
+ end
738
+ end
739
+
689
740
  #-----------------------------------------------------------------------
690
741
  # get_header_vars
691
742
  #-----------------------------------------------------------------------
@@ -769,8 +820,11 @@ class KBEngine
769
820
  # Skip header rec.
770
821
  fptr.readline
771
822
 
772
- recnos.collect { |r| [recno_idx[r], r]
773
- }.sort.each do |r|
823
+ # Take all the recnos you want to get, add the file positions
824
+ # to them, and sort by file position, so that when we seek
825
+ # through the physical file we are going in ascending file
826
+ # position order, which should be fastest.
827
+ recnos.collect { |r| [recno_idx[r], r] }.sort.each do |r|
774
828
  fptr.seek(r[0])
775
829
  line = fptr.readline
776
830
  line.chomp!
@@ -1079,29 +1133,53 @@ class KBEngine
1079
1133
  end
1080
1134
 
1081
1135
  #-----------------------------------------------------------------------
1082
- # get_memo
1136
+ # read_memo_file
1083
1137
  #-----------------------------------------------------------------------
1084
- def get_memo(filepath)
1138
+ def read_memo_file(filepath)
1085
1139
  begin
1086
- f = File.new(filepath)
1087
- return f.readlines
1140
+ f = File.new(File.join(@db.memo_blob_path, filepath))
1141
+ return f.read
1088
1142
  ensure
1089
1143
  f.close
1090
1144
  end
1091
1145
  end
1092
1146
 
1093
1147
  #-----------------------------------------------------------------------
1094
- # get_blob
1148
+ # write_memo_file
1095
1149
  #-----------------------------------------------------------------------
1096
- def get_blob(filepath)
1150
+ def write_memo_file(filepath, contents)
1097
1151
  begin
1098
- f = File.new(filepath, 'rb')
1152
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'w')
1153
+ f.write(contents)
1154
+ ensure
1155
+ f.close
1156
+ end
1157
+ end
1158
+
1159
+ #-----------------------------------------------------------------------
1160
+ # read_blob_file
1161
+ #-----------------------------------------------------------------------
1162
+ def read_blob_file(filepath)
1163
+ begin
1164
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'rb')
1099
1165
  return f.read
1100
1166
  ensure
1101
1167
  f.close
1102
1168
  end
1103
1169
  end
1104
1170
 
1171
+ #-----------------------------------------------------------------------
1172
+ # write_blob_file
1173
+ #-----------------------------------------------------------------------
1174
+ def write_blob_file(filepath, contents)
1175
+ begin
1176
+ f = File.new(File.join(@db.memo_blob_path, filepath), 'wb')
1177
+ f.write(contents)
1178
+ ensure
1179
+ f.close
1180
+ end
1181
+ end
1182
+
1105
1183
 
1106
1184
  #-----------------------------------------------------------------------
1107
1185
  # PRIVATE METHODS
@@ -1217,17 +1295,6 @@ class KBEngine
1217
1295
  end
1218
1296
  end
1219
1297
 
1220
- #-----------------------------------------------------------------------
1221
- # reset_rec_no_ctr
1222
- #-----------------------------------------------------------------------
1223
- def reset_rec_no_ctr(table, fptr)
1224
- last_rec_no, rest_of_line = get_header_record(table, fptr).split(
1225
- '|', 2)
1226
- write_header_record(table, fptr, ['%06d' % 0, rest_of_line].join(
1227
- '|'))
1228
- return true
1229
- end
1230
-
1231
1298
  #-----------------------------------------------------------------------
1232
1299
  # incr_rec_no_ctr
1233
1300
  #-----------------------------------------------------------------------
@@ -1344,10 +1411,10 @@ module KBTypeConversionsMixin
1344
1411
  else
1345
1412
  return true
1346
1413
  end
1414
+ when :Time
1415
+ return Time.parse(s)
1347
1416
  when :Date
1348
1417
  return Date.parse(s)
1349
- when :Time
1350
- return Time.parse(s)
1351
1418
  when :DateTime
1352
1419
  return DateTime.parse(s)
1353
1420
  when :YAML
@@ -1370,9 +1437,13 @@ module KBTypeConversionsMixin
1370
1437
  return YAML.load(s)
1371
1438
  end
1372
1439
  when :Memo
1373
- return KBMemo.new(@tbl.db, s)
1440
+ memo = KBMemo.new(@tbl.db, s)
1441
+ memo.read_from_file
1442
+ return memo
1374
1443
  when :Blob
1375
- return KBBlob.new(@tbl.db, s)
1444
+ blob = KBBlob.new(@tbl.db, s)
1445
+ blob.read_from_file
1446
+ return blob
1376
1447
  else
1377
1448
  raise "Invalid field type: %s" % data_type
1378
1449
  end
@@ -1391,7 +1462,7 @@ class KBTable
1391
1462
  private_class_method :new
1392
1463
 
1393
1464
  VALID_FIELD_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1394
- :DateTime, :Memo, :ResultSet, :YAML]
1465
+ :DateTime, :Memo, :Blob, :ResultSet, :YAML]
1395
1466
 
1396
1467
  VALID_INDEX_TYPES = [:String, :Integer, :Float, :Boolean, :Date, :Time,
1397
1468
  :DateTime]
@@ -1452,6 +1523,8 @@ class KBTable
1452
1523
  @filename = filename
1453
1524
  @encrypted = false
1454
1525
  @lookup_key = :recno
1526
+ @idx_timestamps = {}
1527
+ @idx_arrs = {}
1455
1528
 
1456
1529
  # Alias delete_all to clear method.
1457
1530
  alias delete_all clear
@@ -1547,6 +1620,14 @@ class KBTable
1547
1620
  # Check the field values to make sure they are proper types.
1548
1621
  validate_input(input_rec)
1549
1622
 
1623
+ if @field_types.include?(:Memo)
1624
+ input_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
1625
+ end
1626
+
1627
+ if @field_types.include?(:Blob)
1628
+ input_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
1629
+ end
1630
+
1550
1631
  return @db.engine.insert_record(self, @field_names.zip(@field_types
1551
1632
  ).collect do |fn, ft|
1552
1633
  convert_to_string(ft, input_rec.fetch(fn, ''))
@@ -1624,6 +1705,14 @@ class KBTable
1624
1705
  # type.
1625
1706
  validate_input(update_rec)
1626
1707
 
1708
+ if @field_types.include?(:Memo)
1709
+ update_rec.each_value { |r| r.write_to_file if r.is_a?(KBMemo) }
1710
+ end
1711
+
1712
+ if @field_types.include?(:Blob)
1713
+ update_rec.each_value { |r| r.write_to_file if r.is_a?(KBBlob) }
1714
+ end
1715
+
1627
1716
  updated_recs = []
1628
1717
 
1629
1718
  # For each one of the recs that matched the update query, apply the
@@ -1680,7 +1769,7 @@ class KBTable
1680
1769
  delete { true }
1681
1770
  pack
1682
1771
 
1683
- @db.engine.reset_recno_ctr if reset_recno_ctr
1772
+ @db.engine.reset_recno_ctr(self) if reset_recno_ctr
1684
1773
  end
1685
1774
 
1686
1775
  #-----------------------------------------------------------------------
@@ -1794,12 +1883,15 @@ class KBTable
1794
1883
  # *csv_filename*:: filename of csv file to import.
1795
1884
  #
1796
1885
  def import_csv(csv_filename)
1886
+ records_inserted = 0
1797
1887
  tbl_rec = @table_class.new(self)
1798
1888
 
1799
1889
  CSV.open(csv_filename, 'r') do |row|
1800
1890
  tbl_rec.populate([nil] + row)
1801
1891
  insert(tbl_rec)
1892
+ records_inserted += 1
1802
1893
  end
1894
+ return records_inserted
1803
1895
  end
1804
1896
 
1805
1897
  #-----------------------------------------------------------------------
@@ -1813,7 +1905,7 @@ class KBTable
1813
1905
  def create_indexes
1814
1906
  # Create the recno index. A recno index always gets created even if
1815
1907
  # there are no user-defined indexes for the table.
1816
- @db.engine.init_recno_index(self)
1908
+ @db.engine.init_recno_index(self)
1817
1909
 
1818
1910
  # There can be up to 5 different indexes on a table. Any of these
1819
1911
  # indexes can be single or compound.
@@ -1830,7 +1922,7 @@ class KBTable
1830
1922
  next if index_col_names.empty?
1831
1923
 
1832
1924
  # Create this index on the engine.
1833
- @db.engine.init_index(self, index_col_names)
1925
+ @db.engine.init_index(self, index_col_names)
1834
1926
 
1835
1927
  # For each index found, add an instance method for it so that
1836
1928
  # it can be used for #selects.
@@ -1844,7 +1936,10 @@ class KBTable
1844
1936
  [:#{index_col_names.join(',:')}], filter, select_cond)
1845
1937
  end
1846
1938
  END_OF_STRING
1847
- self.class.class_eval(select_meth_str)
1939
+ self.class.class_eval(select_meth_str) unless @db.server?
1940
+
1941
+ @idx_timestamps[index_col_names.join('_')] = nil
1942
+ @idx_arrs[index_col_names.join('_')] = nil
1848
1943
  end
1849
1944
  end
1850
1945
 
@@ -1893,14 +1988,11 @@ class KBTable
1893
1988
  END_OF_STRING
1894
1989
  else
1895
1990
  begin
1896
- @db.get_table(lookup_table)
1897
- rescue RuntimeError
1898
- raise "Must create child table first when using " +
1899
- "'Lookup'"
1900
- end
1901
-
1902
- if @db.get_table(lookup_table).respond_to?(
1903
- 'select_by_%s_index' % key_field)
1991
+ unless @db.get_table(lookup_table).respond_to?(
1992
+ 'select_by_%s_index' % key_field)
1993
+ raise RuntimeError
1994
+ end
1995
+
1904
1996
  get_meth_str = <<-END_OF_STRING
1905
1997
  def #{field_name}
1906
1998
  table = @tbl.db.get_table(:#{lookup_table})
@@ -1908,7 +2000,7 @@ class KBTable
1908
2000
  r.#{key_field} == @#{field_name} }.first
1909
2001
  end
1910
2002
  END_OF_STRING
1911
- else
2003
+ rescue RuntimeError
1912
2004
  get_meth_str = <<-END_OF_STRING
1913
2005
  def #{field_name}
1914
2006
  table = @tbl.db.get_table(:#{lookup_table})
@@ -1926,14 +2018,11 @@ class KBTable
1926
2018
  link_table, link_field = rest.split('.')
1927
2019
 
1928
2020
  begin
1929
- @db.get_table(link_table)
1930
- rescue RuntimeError
1931
- raise "Must create child table first when using " +
1932
- "'Link_many'"
1933
- end
1934
-
1935
- if @db.get_table(link_table).respond_to?(
1936
- 'select_by_%s_index' % link_field)
2021
+ unless @db.get_table(link_table).respond_to?(
2022
+ 'select_by_%s_index' % link_field)
2023
+ raise RuntimeError
2024
+ end
2025
+
1937
2026
  get_meth_str = <<-END_OF_STRING
1938
2027
  def #{field_name}
1939
2028
  table = @tbl.db.get_table(:#{link_table})
@@ -1941,7 +2030,7 @@ class KBTable
1941
2030
  r.send(:#{link_field}) == @#{lookup_field} }
1942
2031
  end
1943
2032
  END_OF_STRING
1944
- else
2033
+ rescue RuntimeError
1945
2034
  get_meth_str = <<-END_OF_STRING
1946
2035
  def #{field_name}
1947
2036
  table = @tbl.db.get_table(:#{link_table})
@@ -2011,7 +2100,11 @@ class KBTable
2011
2100
  ).gsub("|", '&pipe;')
2012
2101
  else
2013
2102
  return x
2014
- end
2103
+ end
2104
+ when :Memo
2105
+ return x.filepath
2106
+ when :Blob
2107
+ return x.filepath
2015
2108
  else
2016
2109
  return x.to_s
2017
2110
  end
@@ -2103,9 +2196,15 @@ class KBTable
2103
2196
 
2104
2197
  next if data[f].nil?
2105
2198
  case @field_types[@field_names.index(f)]
2106
- when /:String|:Memo|:Blob/
2199
+ when /:String|:Blob/
2107
2200
  raise 'Invalid String value for: %s' % f unless \
2108
2201
  data[f].respond_to?(:to_str)
2202
+ when :Memo
2203
+ raise 'Invalid Memo value for: %s' % f unless \
2204
+ data[f].is_a?(KBMemo)
2205
+ when :Blob
2206
+ raise 'Invalid Blob value for: %s' % f unless \
2207
+ data[f].is_a?(KBBlob)
2109
2208
  when :Boolean
2110
2209
  raise 'Invalid Boolean value for: %s' % f unless \
2111
2210
  data[f].is_a?(TrueClass) or data[f].kind_of?(FalseClass)
@@ -2115,12 +2214,12 @@ class KBTable
2115
2214
  when :Float
2116
2215
  raise 'Invalid Float value for: %s' % f unless \
2117
2216
  data[f].respond_to?(:to_f)
2217
+ when :Time
2218
+ raise 'Invalid Time value for: %s' % f unless \
2219
+ data[f].is_a?(Time)
2118
2220
  when :Date
2119
2221
  raise 'Invalid Date value for: %s' % f unless \
2120
2222
  data[f].is_a?(Date)
2121
- when :Time
2122
- raise 'Invalid Time value for: %s' % f unless \
2123
- data[f].is_a?(Time)
2124
2223
  when :DateTime
2125
2224
  raise 'Invalid DateTime value for: %s' % f unless \
2126
2225
  data[f].is_a?(DateTime)
@@ -2241,7 +2340,28 @@ class KBTable
2241
2340
  idx_struct = Struct.new(*(index_fields + [:recno]))
2242
2341
 
2243
2342
  begin
2244
- @db.engine.get_index(self, index_fields.join('_')).each do |rec|
2343
+ if @db.client?
2344
+ # If client, check to see if the copy of the index we have
2345
+ # is up-to-date. If it is not up-to-date, grab a new copy
2346
+ # of the index array from the engine.
2347
+ unless @idx_timestamps[index_fields.join('_')] == \
2348
+ @db.engine.get_index_timestamp(self, index_fields.join(
2349
+ '_'))
2350
+ @idx_timestamps[index_fields.join('_')] = \
2351
+ @db.engine.get_index_timestamp(self, index_fields.join(
2352
+ '_'))
2353
+
2354
+ @idx_arrs[index_fields.join('_')] = \
2355
+ @db.engine.get_index(self, index_fields.join('_'))
2356
+ end
2357
+ else
2358
+ # If running single-user, grab the index array from the
2359
+ # engine.
2360
+ @idx_arrs[index_fields.join('_')] = \
2361
+ @db.engine.get_index(self, index_fields.join('_'))
2362
+ end
2363
+
2364
+ @idx_arrs[index_fields.join('_')].each do |rec|
2245
2365
  good_matches << rec[-1] if select_cond.call(
2246
2366
  idx_struct.new(*rec))
2247
2367
  end
@@ -2327,14 +2447,23 @@ end
2327
2447
  # KBMemo
2328
2448
  #---------------------------------------------------------------------------
2329
2449
  class KBMemo
2330
- attr_reader :filepath, :memo
2450
+ attr_accessor :filepath, :contents
2331
2451
 
2332
2452
  #-----------------------------------------------------------------------
2333
2453
  # initialize
2334
2454
  #-----------------------------------------------------------------------
2335
- def initialize(db, filepath)
2455
+ def initialize(db, filepath, contents='')
2456
+ @db = db
2336
2457
  @filepath = filepath
2337
- @memo = db.engine.get_memo(@filepath)
2458
+ @contents = contents
2459
+ end
2460
+
2461
+ def read_from_file
2462
+ @contents = @db.engine.read_memo_file(@filepath)
2463
+ end
2464
+
2465
+ def write_to_file
2466
+ @db.engine.write_memo_file(@filepath, @contents)
2338
2467
  end
2339
2468
  end
2340
2469
 
@@ -2342,14 +2471,23 @@ end
2342
2471
  # KBBlob
2343
2472
  #---------------------------------------------------------------------------
2344
2473
  class KBBlob
2345
- attr_reader :filepath, :blob
2474
+ attr_accessor :filepath, :contents
2346
2475
 
2347
2476
  #-----------------------------------------------------------------------
2348
2477
  # initialize
2349
2478
  #-----------------------------------------------------------------------
2350
- def initialize(db, filepath)
2479
+ def initialize(db, filepath, contents='')
2480
+ @db = db
2351
2481
  @filepath = filepath
2352
- @blob = db.engine.get_blob(@filepath)
2482
+ @contents = contents
2483
+ end
2484
+
2485
+ def read_from_file
2486
+ @contents = @db.engine.read_blob_file(@filepath)
2487
+ end
2488
+
2489
+ def write_to_file
2490
+ @db.engine.write_blob_file(@filepath, @contents)
2353
2491
  end
2354
2492
  end
2355
2493
 
@@ -2366,6 +2504,7 @@ class KBIndex
2366
2504
  # initialize
2367
2505
  #-----------------------------------------------------------------------
2368
2506
  def initialize(table, index_fields)
2507
+ @last_update = Time.new
2369
2508
  @idx_arr = []
2370
2509
  @table = table
2371
2510
  @index_fields = index_fields
@@ -2382,6 +2521,13 @@ class KBIndex
2382
2521
  return @idx_arr
2383
2522
  end
2384
2523
 
2524
+ #-----------------------------------------------------------------------
2525
+ # get_timestamp
2526
+ #-----------------------------------------------------------------------
2527
+ def get_timestamp
2528
+ return @last_update
2529
+ end
2530
+
2385
2531
  #-----------------------------------------------------------------------
2386
2532
  # rebuild
2387
2533
  #-----------------------------------------------------------------------
@@ -2429,8 +2575,10 @@ class KBIndex
2429
2575
  # Here's how we break out of the loop...
2430
2576
  rescue EOFError
2431
2577
  end
2578
+
2579
+ @last_update = Time.new
2432
2580
  end
2433
-
2581
+
2434
2582
  #-----------------------------------------------------------------------
2435
2583
  # add_index_rec
2436
2584
  #-----------------------------------------------------------------------
@@ -2438,6 +2586,8 @@ class KBIndex
2438
2586
  @idx_arr << @col_poss.zip(@col_types).collect do |col_pos, col_type|
2439
2587
  convert_to(col_type, rec[col_pos])
2440
2588
  end + [rec.first.to_i]
2589
+
2590
+ @last_update = Time.new
2441
2591
  end
2442
2592
 
2443
2593
  #-----------------------------------------------------------------------
@@ -2446,6 +2596,7 @@ class KBIndex
2446
2596
  def delete_index_rec(recno)
2447
2597
  i = @idx_arr.rassoc(recno.to_i)
2448
2598
  @idx_arr.delete_at(@idx_arr.index(i)) unless i.nil?
2599
+ @last_update = Time.new
2449
2600
  end
2450
2601
 
2451
2602
  #-----------------------------------------------------------------------