og 0.24.0 → 0.25.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.
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
  #-----------------------------------------------------------------------