mongoose 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/README +51 -1
- data/changes.txt +11 -0
- data/example/simple_examples.rb +47 -43
- data/lib/mongoose.rb +1 -3
- data/lib/mongoose/column.rb +10 -38
- data/lib/mongoose/database.rb +4 -13
- data/lib/mongoose/skiplist.rb +407 -336
- data/lib/mongoose/table.rb +53 -43
- data/lib/mongoose/util.rb +1 -1
- data/test/tc_relations.rb +139 -0
- data/test/tc_table.rb +51 -5
- data/test/ts_mongoose.rb +1 -0
- metadata +3 -2
data/README
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
= Mongoose 0.1.
|
1
|
+
= Mongoose 0.1.1
|
2
2
|
|
3
3
|
A database management system written in Ruby. It has an ActiveRecord-like
|
4
4
|
interface, uses Skiplists for its indexing, and Marshal for its data
|
@@ -35,6 +35,7 @@ Unpack the file you downloaded. Execute "ruby setup.rb".
|
|
35
35
|
can greatly reduce startup times.
|
36
36
|
* Supports any datatype that Marshal supports.
|
37
37
|
* Table relations supported via has_one, has_many.
|
38
|
+
* Fast, Fast, Fast!
|
38
39
|
|
39
40
|
|
40
41
|
== Documentation
|
@@ -88,6 +89,9 @@ end
|
|
88
89
|
|
89
90
|
# Delete a record.
|
90
91
|
Plane.find(1).destroy
|
92
|
+
|
93
|
+
# Close database.
|
94
|
+
db.close
|
91
95
|
|
92
96
|
== Manifest
|
93
97
|
|
@@ -134,3 +138,49 @@ jcribbs@netpromi.com
|
|
134
138
|
== Home Page
|
135
139
|
|
136
140
|
http://rubyforge.org/projects/mongoose/
|
141
|
+
|
142
|
+
|
143
|
+
== Why Did I Develop Mongoose?
|
144
|
+
|
145
|
+
Well, I started to look into performance improvements for KirbyBase. One thing
|
146
|
+
I noticed was that Ruby takes a comparatively long time converting strings to
|
147
|
+
native data types like Integer, Time, etc. Since KirbyBase stores its records
|
148
|
+
as strings, returning a large result set could take a long time. I found that
|
149
|
+
if I Marshaled records before I wrote them to disk, a subsequent read of those
|
150
|
+
records was significantly faster.
|
151
|
+
|
152
|
+
About the same time, I read a paper about skiplists. A skiplist is a data
|
153
|
+
structure that is relatively simple (compared to say a b-tree) to understand
|
154
|
+
and implement in code. I was able to take the pseudo-code in the paper and
|
155
|
+
implement a Ruby version in a couple of hours. Skiplists are pretty fast and
|
156
|
+
since they are pretty easy to understand, I think there is good potential to
|
157
|
+
tweak them. So, I wanted to try using skiplists for indexing rather than
|
158
|
+
KirbyBase's array-based indexes.
|
159
|
+
|
160
|
+
I started to retrofit both the Marshal serialization and Skiplists in KirbyBase,
|
161
|
+
but quickly found that it was going to be more work than just starting over from
|
162
|
+
scratch with a new design. Besides, I didn't want to radically change KirbyBase
|
163
|
+
and piss off the current user base (both of you know who you are).
|
164
|
+
|
165
|
+
So, I started from scratch. This also gave me the opportunity to make two other
|
166
|
+
major changes. First of all, I wanted to keep the query language as close to
|
167
|
+
KirbyBase's as possible (i.e. Ruby blocks), but I wanted more control over the
|
168
|
+
query expression. I took the opportunity to borrow a lot of idea's from Ezra's
|
169
|
+
ez_where plugin. I think this will give me the capability down the road to
|
170
|
+
tweak the query engine, based on the query itself.
|
171
|
+
|
172
|
+
The second thing I changed was that I have finally seen the light about
|
173
|
+
ActiveRecord, so I stole pretty much all of Logan's KirbyRecord code to give
|
174
|
+
Mongoose an ActiveRecord-like api.
|
175
|
+
|
176
|
+
The end result of all of this (I hope) is a database management system that is
|
177
|
+
small, easy to use, and fast.
|
178
|
+
|
179
|
+
|
180
|
+
== What about KirbyBase?
|
181
|
+
|
182
|
+
KirbyBase is not going anywhere. I still plan on supporting it into the
|
183
|
+
foreseeable future. However, I imagine that most new development will be
|
184
|
+
directed at Mongoose.
|
185
|
+
|
186
|
+
|
data/changes.txt
CHANGED
@@ -1,2 +1,13 @@
|
|
1
1
|
2006-07-19:: Version 0.1.0
|
2
2
|
* Initial release.
|
3
|
+
|
4
|
+
2006-07-20:: Version 0.1.1
|
5
|
+
* Fixed bug where saving an updated record that was not the same size as the
|
6
|
+
original was not working.
|
7
|
+
* Removed in-memory tracking of last_id_used and deleted_records_counter. Now
|
8
|
+
I just call out to the header file when I need to get a new id number, etc.
|
9
|
+
* Added #dump_to_hash and #load_from_hash methods to SkipList class. This has
|
10
|
+
cut index initialization times in half, increased query speed, and reduced
|
11
|
+
memory usage.
|
12
|
+
* Cleaned up the code in SkipList class.
|
13
|
+
* Added more unit tests.
|
data/example/simple_examples.rb
CHANGED
@@ -5,9 +5,11 @@ rescue LoadError
|
|
5
5
|
require 'mongoose'
|
6
6
|
end
|
7
7
|
|
8
|
-
|
9
|
-
|
10
|
-
|
8
|
+
unless ARGV[0] == 'keep-data'
|
9
|
+
puts 'Deleting old tables!'
|
10
|
+
Dir.glob('[plane|pilot|flight]*.mg?').each do |filename|
|
11
|
+
File.delete(filename)
|
12
|
+
end
|
11
13
|
end
|
12
14
|
|
13
15
|
class Plane < Mongoose::Table
|
@@ -18,50 +20,52 @@ end
|
|
18
20
|
db = Mongoose::Database.new
|
19
21
|
|
20
22
|
# Create new table. Notice how you specify whether a column is indexed or not.
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
23
|
+
unless ARGV[0] == 'keep-data'
|
24
|
+
db.create_table(:plane) do |tbl|
|
25
|
+
tbl.add_indexed_column(:plane_name, :string)
|
26
|
+
tbl.add_column(:country, :string)
|
27
|
+
tbl.add_indexed_column(:speed, :integer)
|
28
|
+
tbl.add_column(:range, :integer)
|
29
|
+
end
|
27
30
|
|
28
|
-
# Add records.
|
29
|
-
rec = Plane.new
|
30
|
-
rec.plane_name = 'P-51'
|
31
|
-
rec.country = 'USA'
|
32
|
-
rec.speed = 402
|
33
|
-
rec.range = 1205
|
34
|
-
rec.save
|
35
|
-
|
36
|
-
rec2 = Plane.new
|
37
|
-
rec2.plane_name = 'Spitfire'
|
38
|
-
rec2.country = 'Great Britain'
|
39
|
-
rec2.speed = 333
|
40
|
-
rec2.range = 454
|
41
|
-
rec2.save
|
42
|
-
|
43
|
-
rec3 = Plane.new
|
44
|
-
rec3.plane_name = 'ME-109'
|
45
|
-
rec3.country = 'Germany'
|
46
|
-
rec3.speed = 354
|
47
|
-
rec3.range = 501
|
48
|
-
rec3.save
|
49
|
-
|
50
|
-
rec4 = Plane.new
|
51
|
-
rec4.plane_name = 'P-39'
|
52
|
-
rec4.country = 'USA'
|
53
|
-
rec4.range = 701
|
54
|
-
|
55
|
-
# Forgot value for speed, which is a required field.
|
56
|
-
begin
|
31
|
+
# Add records.
|
32
|
+
rec = Plane.new
|
33
|
+
rec.plane_name = 'P-51'
|
34
|
+
rec.country = 'USA'
|
35
|
+
rec.speed = 402
|
36
|
+
rec.range = 1205
|
37
|
+
rec.save
|
38
|
+
|
39
|
+
rec2 = Plane.new
|
40
|
+
rec2.plane_name = 'Spitfire'
|
41
|
+
rec2.country = 'Great Britain'
|
42
|
+
rec2.speed = 333
|
43
|
+
rec2.range = 454
|
44
|
+
rec2.save
|
45
|
+
|
46
|
+
rec3 = Plane.new
|
47
|
+
rec3.plane_name = 'ME-109'
|
48
|
+
rec3.country = 'Germany'
|
49
|
+
rec3.speed = 354
|
50
|
+
rec3.range = 501
|
51
|
+
rec3.save
|
52
|
+
|
53
|
+
rec4 = Plane.new
|
54
|
+
rec4.plane_name = 'P-39'
|
55
|
+
rec4.country = 'USA'
|
56
|
+
rec4.range = 701
|
57
|
+
|
58
|
+
# Forgot value for speed, which is a required field.
|
59
|
+
begin
|
60
|
+
rec4.save
|
61
|
+
rescue RuntimeError => e
|
62
|
+
puts e
|
63
|
+
end
|
64
|
+
|
65
|
+
rec4.speed = 339
|
57
66
|
rec4.save
|
58
|
-
rescue RuntimeError => e
|
59
|
-
puts e
|
60
67
|
end
|
61
68
|
|
62
|
-
rec4.speed = 339
|
63
|
-
rec4.save
|
64
|
-
|
65
69
|
puts "\n\nFind P-51 record by ID"
|
66
70
|
p_51 = Plane.find(1)
|
67
71
|
p p_51
|
data/lib/mongoose.rb
CHANGED
@@ -18,13 +18,11 @@ require 'mongoose/util'
|
|
18
18
|
# Homepage:: http://rubyforge.org/projects/mongoose/
|
19
19
|
# Copyright:: Copyright (c) 2006 NetPro Technologies, LLC
|
20
20
|
# License:: Distributed under the same terms as Ruby
|
21
|
-
# History:
|
22
|
-
# 2006-07-19:: Version 0.1.0
|
23
|
-
# * Initial release.
|
24
21
|
#
|
25
22
|
#
|
26
23
|
module Mongoose
|
27
24
|
|
25
|
+
VERSION = '0.1.1'
|
28
26
|
DATA_TYPES = [:string, :integer, :float, :time, :date, :datetime, :boolean]
|
29
27
|
TBL_EXT = '.mgt'
|
30
28
|
TBL_HDR_EXT = '.mgh'
|
data/lib/mongoose/column.rb
CHANGED
@@ -50,12 +50,6 @@ class Column < BaseColumn
|
|
50
50
|
@idx = LinearSearch.new(self)
|
51
51
|
end
|
52
52
|
|
53
|
-
def store(key, value)
|
54
|
-
end
|
55
|
-
|
56
|
-
def remove(key, value)
|
57
|
-
end
|
58
|
-
|
59
53
|
def >(other)
|
60
54
|
@tbl_class.query << [@idx, :>, other]
|
61
55
|
end
|
@@ -110,6 +104,9 @@ class IndexedColumn < BaseColumn
|
|
110
104
|
rebuild_index_file if index_file_out_of_date?
|
111
105
|
end
|
112
106
|
|
107
|
+
#-----------------------------------------------------------------------------
|
108
|
+
# init_index
|
109
|
+
#-----------------------------------------------------------------------------
|
113
110
|
def init_index
|
114
111
|
if File.exists?(@index_file_name) and not index_file_out_of_date?
|
115
112
|
rebuild_index_from_index_file
|
@@ -149,12 +146,12 @@ end
|
|
149
146
|
#-------------------------------------------------------------------------------
|
150
147
|
class SkipListIndexColumn < IndexedColumn
|
151
148
|
def initialize(tbl_class, name, col_def)
|
152
|
-
@idx = SkipList.new
|
149
|
+
@idx = SkipList.new(self)
|
153
150
|
super
|
154
151
|
end
|
155
152
|
|
156
153
|
def clear_index
|
157
|
-
@idx = SkipList.new
|
154
|
+
@idx = SkipList.new(self)
|
158
155
|
end
|
159
156
|
|
160
157
|
def >(other)
|
@@ -190,9 +187,7 @@ class SkipListIndexColumn < IndexedColumn
|
|
190
187
|
end
|
191
188
|
|
192
189
|
def rebuild_index_file
|
193
|
-
with_index_file('w')
|
194
|
-
@idx.each { |k,v| fptr.write(Marshal.dump([k, v])) }
|
195
|
-
end
|
190
|
+
with_index_file('w') { |fptr| fptr.write(Marshal.dump(@idx.dump_to_hash)) }
|
196
191
|
end
|
197
192
|
|
198
193
|
def rebuild_index_from_table
|
@@ -206,15 +201,7 @@ class SkipListIndexColumn < IndexedColumn
|
|
206
201
|
|
207
202
|
def rebuild_index_from_index_file
|
208
203
|
clear_index
|
209
|
-
|
210
|
-
with_index_file do |fptr|
|
211
|
-
begin
|
212
|
-
while true
|
213
|
-
@idx.load(*Marshal.load(fptr))
|
214
|
-
end
|
215
|
-
rescue EOFError
|
216
|
-
end
|
217
|
-
end
|
204
|
+
with_index_file { |fptr| @idx.load_from_hash(Marshal.load(fptr)) }
|
218
205
|
end
|
219
206
|
|
220
207
|
def add_index_rec(key, value)
|
@@ -235,7 +222,6 @@ class IDColumn < IndexedColumn
|
|
235
222
|
|
236
223
|
def_delegator(:@idx, :[], :[])
|
237
224
|
def_delegator(:@idx, :keys, :keys)
|
238
|
-
# def_delegator(:@idx, :delete, :remove)
|
239
225
|
|
240
226
|
def initialize(tbl_class, name, col_def)
|
241
227
|
@idx = {}
|
@@ -247,31 +233,17 @@ class IDColumn < IndexedColumn
|
|
247
233
|
end
|
248
234
|
|
249
235
|
def rebuild_index_file
|
250
|
-
with_index_file('w')
|
251
|
-
# @idx.each_pair { |k,v| fptr.write(Marshal.dump([k, v])) }
|
252
|
-
fptr.write(Marshal.dump(@idx))
|
253
|
-
end
|
236
|
+
with_index_file('w') { |fptr| fptr.write(Marshal.dump(@idx)) }
|
254
237
|
end
|
255
238
|
|
256
239
|
def rebuild_index_from_table
|
257
240
|
clear_index
|
258
|
-
@tbl_class.get_all_recs
|
259
|
-
add_index_rec(rec[0], fpos)
|
260
|
-
end
|
241
|
+
@tbl_class.get_all_recs { |rec, fpos| add_index_rec(rec[0], fpos) }
|
261
242
|
end
|
262
243
|
|
263
244
|
def rebuild_index_from_index_file
|
264
245
|
clear_index
|
265
|
-
|
266
|
-
with_index_file do |fptr|
|
267
|
-
# begin
|
268
|
-
# while true
|
269
|
-
# add_index_rec(*Marshal.load(fptr))
|
270
|
-
# end
|
271
|
-
# rescue EOFError
|
272
|
-
# end
|
273
|
-
@idx = Marshal.load(fptr)
|
274
|
-
end
|
246
|
+
with_index_file { |fptr| @idx = Marshal.load(fptr) }
|
275
247
|
end
|
276
248
|
|
277
249
|
def add_index_rec(id, fpos)
|
data/lib/mongoose/database.rb
CHANGED
@@ -37,7 +37,7 @@ class Database
|
|
37
37
|
def create_table(table_name)
|
38
38
|
raise "Table already exists!" if table_exists?(table_name)
|
39
39
|
|
40
|
-
class_name =
|
40
|
+
class_name = Util.us_case_to_class_case(table_name)
|
41
41
|
|
42
42
|
tbl_header = {}
|
43
43
|
tbl_header[:table_name] = table_name
|
@@ -64,7 +64,7 @@ class Database
|
|
64
64
|
# drop_table
|
65
65
|
#-----------------------------------------------------------------------------
|
66
66
|
def drop_table(table_name)
|
67
|
-
class_name =
|
67
|
+
class_name = Util.us_case_to_class_case(table_name)
|
68
68
|
|
69
69
|
@tables[Object.const_get(class_name)][:columns].each do |c|
|
70
70
|
if c.indexed?
|
@@ -92,22 +92,13 @@ class Database
|
|
92
92
|
# init_table
|
93
93
|
#-----------------------------------------------------------------------------
|
94
94
|
def init_table(table_name)
|
95
|
-
class_name =
|
95
|
+
class_name = Util.us_case_to_class_case(table_name)
|
96
96
|
|
97
97
|
@tables[Object.full_const_get(class_name)] = { :class_name => class_name,
|
98
|
-
:table_name => table_name, :columns => [], :query => []
|
99
|
-
:last_id_used => nil, :deleted_recs_counter => nil }
|
98
|
+
:table_name => table_name, :columns => [], :query => [] }
|
100
99
|
|
101
100
|
Object.full_const_get(class_name).init_table
|
102
101
|
end
|
103
|
-
|
104
|
-
#-----------------------------------------------------------------------------
|
105
|
-
# get_class_name_from_table_name
|
106
|
-
#-----------------------------------------------------------------------------
|
107
|
-
def get_class_name_from_table_name(table_name)
|
108
|
-
return table_name.to_s.split('_').inject('') { |full_name, word|
|
109
|
-
full_name + word.capitalize }
|
110
|
-
end
|
111
102
|
end
|
112
103
|
|
113
104
|
end
|
data/lib/mongoose/skiplist.rb
CHANGED
@@ -1,412 +1,483 @@
|
|
1
|
-
require 'pp'
|
2
|
-
|
3
1
|
module Mongoose
|
4
2
|
|
5
3
|
#---------------------------------------------------------------------------
|
6
4
|
# SkipList Class
|
7
5
|
#---------------------------------------------------------------------------
|
8
6
|
class SkipList
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
7
|
+
MAX_LEVEL = 31
|
8
|
+
OPTIMAL_PROBABILITY = 0.25
|
9
|
+
|
10
|
+
attr_reader :size
|
11
|
+
|
12
|
+
#-----------------------------------------------------------------------
|
13
|
+
# initialize
|
14
|
+
#-----------------------------------------------------------------------
|
15
|
+
def initialize(col)
|
16
|
+
@col = col
|
17
|
+
@size = 0
|
18
|
+
|
19
|
+
# Create header and footer nodes.
|
20
|
+
@footer = FooterNode.new
|
21
|
+
@header = HeaderNode.new
|
22
|
+
|
23
|
+
# Point all header.forward references to footer node.
|
24
|
+
0.upto(MAX_LEVEL) { |i| @header.forward[i] = @footer }
|
25
|
+
|
26
|
+
# This attribute will hold the actual level of the skip list.
|
27
|
+
@level = 0
|
28
|
+
end
|
29
|
+
|
30
|
+
#-----------------------------------------------------------------------
|
31
|
+
# store
|
32
|
+
#-----------------------------------------------------------------------
|
33
|
+
#++
|
34
|
+
# If key not found, will insert new record, otherwise will update value
|
35
|
+
# of existing record.
|
36
|
+
#
|
37
|
+
def store(search_key, new_value)
|
38
|
+
# This array will be used to determine which records need their
|
39
|
+
# forward array re-adjusted after a new node is created.
|
40
|
+
update = []
|
41
|
+
|
42
|
+
# Start off at the header node.
|
43
|
+
x = @header
|
44
|
+
|
45
|
+
# Starting at the current highest level of the skip list, walk from
|
46
|
+
# left to right at that level, until you see that the next node's
|
47
|
+
# key is greater than the key you are searching for. When this
|
48
|
+
# happend, you want to add the current node you are on to the list
|
49
|
+
# of nodes that need to have their forward arrays updated. Next,
|
50
|
+
# you want to drop down a level on the current node and start
|
51
|
+
# walking forward again, until you again see that the next node's
|
52
|
+
# key is bigger than the search key. In this way, you are walking
|
53
|
+
# through the skip list, constantly moving to the right and moving
|
54
|
+
# down, until you reach level 0 and are on the node whose key is
|
55
|
+
# either equal to the search key (in which case an update will take
|
56
|
+
# place), or the highest key in the list that is still lower than
|
57
|
+
# the search key (in which case, you have found the place to do an
|
58
|
+
# insert).
|
59
|
+
@level.downto(0) do |i|
|
60
|
+
while x.forward[i].key < search_key
|
61
|
+
x = x.forward[i]
|
62
|
+
end
|
63
|
+
update[i] = x
|
33
64
|
end
|
34
65
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
# of
|
41
|
-
#
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
while x.forward[i].key < search_key
|
66
|
-
x = x.forward[i]
|
67
|
-
end
|
68
|
-
update[i] = x
|
69
|
-
end
|
70
|
-
|
71
|
-
x = x.forward[0]
|
72
|
-
|
73
|
-
# If the search key was found, simply update the value of the node.
|
74
|
-
if x.key == search_key
|
75
|
-
x.value << new_value unless x.value.include?(new_value)
|
76
|
-
# If this is an insert, determine the number of levels it will have
|
77
|
-
# using a random number. This is what keeps the skip list balanced.
|
78
|
-
else
|
79
|
-
lvl = random_level
|
80
|
-
|
81
|
-
# If the new level is higher than the actual current level, we
|
82
|
-
# need to make sure that the header node gets updated at these
|
83
|
-
# levels. Then, we set the actual current level equal to the
|
84
|
-
# new level.
|
85
|
-
if lvl > @level
|
86
|
-
(@level + 1).upto(lvl) { |i| update[i] = @header }
|
87
|
-
@level = lvl
|
88
|
-
end
|
89
|
-
|
90
|
-
# Create a new node.
|
91
|
-
x = Node.new(lvl, search_key, [new_value])
|
92
|
-
|
93
|
-
# Now, we need to update all of the nodes that will be affected
|
94
|
-
# by the insertion of the new node. These are nodes whose
|
95
|
-
# forward array either will point to the new node and the new
|
96
|
-
# node itself.
|
97
|
-
0.upto(lvl) do |i|
|
98
|
-
x.forward[i] = update[i].forward[i]
|
99
|
-
update[i].forward[i] = x
|
100
|
-
end
|
66
|
+
x = x.forward[0]
|
67
|
+
|
68
|
+
# If the search key was found, simply update the value of the node.
|
69
|
+
if x.key == search_key
|
70
|
+
x.value << new_value unless x.value.include?(new_value)
|
71
|
+
# If this is an insert, determine the number of levels it will have
|
72
|
+
# using a random number. This is what keeps the skip list balanced.
|
73
|
+
else
|
74
|
+
lvl = random_level
|
75
|
+
|
76
|
+
# If the new level is higher than the actual current level, we
|
77
|
+
# need to make sure that the header node gets updated at these
|
78
|
+
# levels. Then, we set the actual current level equal to the
|
79
|
+
# new level.
|
80
|
+
if lvl > @level
|
81
|
+
(@level + 1).upto(lvl) { |i| update[i] = @header }
|
82
|
+
@level = lvl
|
83
|
+
end
|
84
|
+
|
85
|
+
# Create a new node.
|
86
|
+
x = Node.new(lvl, search_key, [new_value])
|
87
|
+
|
88
|
+
# Now, we need to update all of the nodes that will be affected
|
89
|
+
# by the insertion of the new node. These are nodes whose
|
90
|
+
# forward array either will point to the new node and the new
|
91
|
+
# node itself.
|
92
|
+
0.upto(lvl) do |i|
|
93
|
+
x.forward[i] = update[i].forward[i]
|
94
|
+
update[i].forward[i] = x
|
95
|
+
end
|
101
96
|
|
102
|
-
|
103
|
-
|
104
|
-
end
|
97
|
+
# Increment the size attribute by one.
|
98
|
+
@size += 1
|
105
99
|
end
|
100
|
+
end
|
106
101
|
|
107
|
-
|
108
|
-
|
102
|
+
#-----------------------------------------------------------------------
|
103
|
+
# remove
|
104
|
+
#-----------------------------------------------------------------------
|
105
|
+
def remove(search_key, value)
|
106
|
+
update = []
|
109
107
|
|
110
|
-
|
108
|
+
x = @header
|
111
109
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
x = x.forward[0]
|
120
|
-
|
121
|
-
if x.key == search_key
|
122
|
-
x.value.delete(value)
|
110
|
+
@level.downto(0) do |i|
|
111
|
+
while x.forward[i].key < search_key
|
112
|
+
x = x.forward[i]
|
113
|
+
end
|
114
|
+
update[i] = x
|
115
|
+
end
|
123
116
|
|
124
|
-
|
125
|
-
0.upto(@level) do |i|
|
126
|
-
break unless update[i].forward[i] == x
|
127
|
-
update[i].forward[i] = x.forward[i]
|
128
|
-
end
|
117
|
+
x = x.forward[0]
|
129
118
|
|
130
|
-
|
131
|
-
|
132
|
-
end
|
119
|
+
if x.key == search_key
|
120
|
+
x.value.delete(value)
|
133
121
|
|
134
|
-
|
135
|
-
|
122
|
+
if x.value.empty?
|
123
|
+
0.upto(@level) do |i|
|
124
|
+
break unless update[i].forward[i] == x
|
125
|
+
update[i].forward[i] = x.forward[i]
|
136
126
|
end
|
137
|
-
end
|
138
|
-
|
139
|
-
def search(search_key)
|
140
|
-
result = []
|
141
|
-
x = @header
|
142
127
|
|
143
|
-
@level
|
144
|
-
|
145
|
-
x = x.forward[i]
|
146
|
-
end
|
128
|
+
while @level > 0 and @header.forward[@level] == @footer
|
129
|
+
@level -= 1
|
147
130
|
end
|
148
131
|
|
149
|
-
|
150
|
-
|
151
|
-
return x.value if x.key == search_key
|
132
|
+
@size -= 1
|
133
|
+
end
|
152
134
|
end
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
135
|
+
end
|
136
|
+
|
137
|
+
#-----------------------------------------------------------------------
|
138
|
+
# search
|
139
|
+
#-----------------------------------------------------------------------
|
140
|
+
def search(search_key)
|
141
|
+
result = []
|
142
|
+
x = @header
|
143
|
+
|
144
|
+
@level.downto(0) do |i|
|
145
|
+
while x.forward[i].key < search_key
|
146
|
+
x = x.forward[i]
|
147
|
+
end
|
160
148
|
end
|
161
149
|
|
162
|
-
|
163
|
-
return search(other)
|
164
|
-
end
|
165
|
-
|
166
|
-
def >(search_key)
|
167
|
-
result = []
|
168
|
-
x = @header
|
169
|
-
|
170
|
-
@level.downto(0) do |i|
|
171
|
-
while x.forward[i].key < search_key
|
172
|
-
x = x.forward[i]
|
173
|
-
end
|
174
|
-
end
|
175
|
-
|
176
|
-
x = x.forward[0]
|
177
|
-
|
178
|
-
x = x.forward[0] if x.key == search_key
|
150
|
+
x = x.forward[0]
|
179
151
|
|
180
|
-
|
181
|
-
|
182
|
-
x = x.forward[0]
|
183
|
-
end
|
152
|
+
return x.value if x.key == search_key
|
153
|
+
end
|
184
154
|
|
185
|
-
|
155
|
+
#-----------------------------------------------------------------------
|
156
|
+
# one_of
|
157
|
+
#-----------------------------------------------------------------------
|
158
|
+
def one_of(*other)
|
159
|
+
result = []
|
160
|
+
other.each do |o|
|
161
|
+
result.concat(search(o))
|
162
|
+
end
|
163
|
+
return result
|
164
|
+
end
|
165
|
+
|
166
|
+
#-----------------------------------------------------------------------
|
167
|
+
# ==
|
168
|
+
#-----------------------------------------------------------------------
|
169
|
+
def ==(other)
|
170
|
+
search(other)
|
171
|
+
end
|
172
|
+
|
173
|
+
#-----------------------------------------------------------------------
|
174
|
+
# >
|
175
|
+
#-----------------------------------------------------------------------
|
176
|
+
def >(search_key)
|
177
|
+
result = []
|
178
|
+
x = @header
|
179
|
+
|
180
|
+
@level.downto(0) do |i|
|
181
|
+
while x.forward[i].key < search_key
|
182
|
+
x = x.forward[i]
|
183
|
+
end
|
186
184
|
end
|
187
185
|
|
188
|
-
|
189
|
-
|
190
|
-
x = @header
|
191
|
-
|
192
|
-
@level.downto(0) do |i|
|
193
|
-
while x.forward[i].key < search_key
|
194
|
-
x = x.forward[i]
|
195
|
-
end
|
196
|
-
end
|
197
|
-
|
198
|
-
x = x.forward[0]
|
186
|
+
x = x.forward[0]
|
187
|
+
x = x.forward[0] if x.key == search_key
|
199
188
|
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
end
|
204
|
-
|
205
|
-
return result
|
189
|
+
while x != @footer
|
190
|
+
result.concat(x.value)
|
191
|
+
x = x.forward[0]
|
206
192
|
end
|
207
193
|
|
208
|
-
|
209
|
-
|
210
|
-
x = @header
|
211
|
-
|
212
|
-
x = x.forward[0]
|
194
|
+
result
|
195
|
+
end
|
213
196
|
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
197
|
+
#-----------------------------------------------------------------------
|
198
|
+
# >=
|
199
|
+
#-----------------------------------------------------------------------
|
200
|
+
def >=(search_key)
|
201
|
+
result = []
|
202
|
+
x = @header
|
218
203
|
|
219
|
-
|
204
|
+
@level.downto(0) do |i|
|
205
|
+
while x.forward[i].key < search_key
|
206
|
+
x = x.forward[i]
|
207
|
+
end
|
220
208
|
end
|
221
|
-
|
222
|
-
def <=(search_key)
|
223
|
-
result = []
|
224
|
-
x = @header
|
225
209
|
|
226
|
-
|
227
|
-
|
228
|
-
while x != @footer and x.key <= search_key
|
229
|
-
result.concat(x.value)
|
230
|
-
x = x.forward[0]
|
231
|
-
end
|
210
|
+
x = x.forward[0]
|
232
211
|
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
def [](search_key)
|
237
|
-
return search(search_key)
|
212
|
+
while x != @footer
|
213
|
+
result.concat(x.value)
|
214
|
+
x = x.forward[0]
|
238
215
|
end
|
239
216
|
|
240
|
-
|
241
|
-
|
242
|
-
result = []
|
243
|
-
x = @header
|
217
|
+
result
|
218
|
+
end
|
244
219
|
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
x = x.forward[0]
|
220
|
+
#-----------------------------------------------------------------------
|
221
|
+
# <
|
222
|
+
#-----------------------------------------------------------------------
|
223
|
+
def <(search_key)
|
224
|
+
result = []
|
225
|
+
x = @header
|
252
226
|
|
253
|
-
|
227
|
+
x = x.forward[0]
|
254
228
|
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
end
|
259
|
-
|
260
|
-
result = result.concat(x.value) if x.key == search_end and end_inclusive
|
261
|
-
return result
|
229
|
+
while x != @footer and x.key < search_key
|
230
|
+
result.concat(x.value)
|
231
|
+
x = x.forward[0]
|
262
232
|
end
|
263
233
|
|
264
|
-
|
265
|
-
|
234
|
+
result
|
235
|
+
end
|
266
236
|
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
237
|
+
#-----------------------------------------------------------------------
|
238
|
+
# <=
|
239
|
+
#-----------------------------------------------------------------------
|
240
|
+
def <=(search_key)
|
241
|
+
result = []
|
242
|
+
x = @header
|
243
|
+
|
244
|
+
x = x.forward[0]
|
245
|
+
|
246
|
+
while x != @footer and x.key <= search_key
|
247
|
+
result.concat(x.value)
|
248
|
+
x = x.forward[0]
|
271
249
|
end
|
272
250
|
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
end
|
251
|
+
result
|
252
|
+
end
|
253
|
+
|
254
|
+
#-----------------------------------------------------------------------
|
255
|
+
# []
|
256
|
+
#-----------------------------------------------------------------------
|
257
|
+
def [](search_key)
|
258
|
+
search(search_key)
|
259
|
+
end
|
260
|
+
|
261
|
+
#-----------------------------------------------------------------------
|
262
|
+
# between
|
263
|
+
#-----------------------------------------------------------------------
|
264
|
+
def between(search_start, search_end, start_inclusive=false,
|
265
|
+
end_inclusive=false)
|
266
|
+
result = []
|
267
|
+
x = @header
|
268
|
+
|
269
|
+
@level.downto(0) do |i|
|
270
|
+
while x.forward[i].key < search_start
|
271
|
+
x = x.forward[i]
|
272
|
+
end
|
273
|
+
end
|
297
274
|
|
298
|
-
|
275
|
+
x = x.forward[0]
|
276
|
+
x = x.forward[0] if x.key == search_start and not start_inclusive
|
299
277
|
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
if lvl > @level
|
305
|
-
(@level + 1).upto(lvl) { |i| update[i] = @header }
|
306
|
-
@level = lvl
|
307
|
-
end
|
278
|
+
while x != @footer and x.key < search_end
|
279
|
+
result.concat(x.value)
|
280
|
+
x = x.forward[0]
|
281
|
+
end
|
308
282
|
|
309
|
-
|
310
|
-
|
283
|
+
result = result.concat(x.value) if x.key == search_end and end_inclusive
|
284
|
+
result
|
285
|
+
end
|
311
286
|
|
312
|
-
|
313
|
-
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
|
321
|
-
|
322
|
-
|
287
|
+
#-----------------------------------------------------------------------
|
288
|
+
# each
|
289
|
+
#-----------------------------------------------------------------------
|
290
|
+
def each
|
291
|
+
x = @header.forward[0]
|
292
|
+
|
293
|
+
while x != @footer
|
294
|
+
yield x.key, x.value
|
295
|
+
x = x.forward[0]
|
296
|
+
end
|
297
|
+
end
|
298
|
+
|
299
|
+
#-----------------------------------------------------------------------
|
300
|
+
# load_from_hash
|
301
|
+
#-----------------------------------------------------------------------
|
302
|
+
def load_from_hash(sl_hash)
|
303
|
+
# Create array to hold last node that was at that level, while working
|
304
|
+
# backwards.
|
305
|
+
levels = []
|
306
|
+
0.upto(MAX_LEVEL) { |i| levels << @footer }
|
307
|
+
|
308
|
+
x = nil
|
309
|
+
lvl = nil
|
310
|
+
|
311
|
+
# Loop through keys in reverse order...
|
312
|
+
sl_hash.keys.sort.reverse.each do |key|
|
313
|
+
lvl, value = sl_hash[key]
|
314
|
+
|
315
|
+
# Update skiplist level to be same as highest level yet found in keys.
|
316
|
+
@level = lvl if lvl > @level
|
317
|
+
|
318
|
+
# Create node with values from hash.
|
319
|
+
x = Node.new(lvl, key, value)
|
320
|
+
|
321
|
+
# Now, for each level of node, point its forward reference to the last
|
322
|
+
# node that occupied that level (remember we are working backwards).
|
323
|
+
0.upto(lvl) do |i|
|
324
|
+
x.forward[i] = levels[i]
|
325
|
+
# Now, we want to make current node the last node that occupied that
|
326
|
+
# level.
|
327
|
+
levels[i] = x
|
328
|
+
end
|
329
|
+
|
330
|
+
# Now, make sure that, for now, the header node points to this node,
|
331
|
+
# since it is the lowest node, at least until we read the next hash key.
|
332
|
+
0.upto(lvl) { |i| @header.forward[i] = x }
|
333
|
+
end
|
334
|
+
end
|
335
|
+
|
336
|
+
#-----------------------------------------------------------------------
|
337
|
+
# dump_to_hash
|
338
|
+
#-----------------------------------------------------------------------
|
339
|
+
def dump_to_hash
|
340
|
+
sl_hash = {}
|
341
|
+
|
342
|
+
x = @header.forward[0]
|
343
|
+
|
344
|
+
while x != @footer
|
345
|
+
sl_hash[x.key] = [x.lvl, x.value]
|
346
|
+
x = x.forward[0]
|
323
347
|
end
|
324
348
|
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
|
349
|
+
sl_hash
|
350
|
+
end
|
351
|
+
|
352
|
+
#-----------------------------------------------------------------------
|
353
|
+
# dump
|
354
|
+
#-----------------------------------------------------------------------
|
355
|
+
def dump
|
356
|
+
File.open(@col.index_file_name + '.dump', 'w') do |fptr|
|
357
|
+
x = @header
|
358
|
+
fptr.write("---------------- Header -----------------------------\n")
|
359
|
+
x.forward.each_with_index do |f,i|
|
360
|
+
fptr.write("**** Forward entry %d ****\n" % i)
|
361
|
+
fptr.write("Key: " + f.key.inspect + "\n")
|
362
|
+
fptr.write("Value: " + f.value.inspect + "\n") unless \
|
363
|
+
f.is_a?(FooterNode)
|
364
|
+
end
|
365
|
+
fptr.write("---------------- End Header -------------------------\n")
|
366
|
+
|
367
|
+
while not x.forward[0].is_a?(FooterNode)
|
368
|
+
x = x.forward[0]
|
369
|
+
fptr.write("---------------- Node -------------------------\n")
|
370
|
+
fptr.write("Key: " + x.key.inspect + "\n")
|
371
|
+
fptr.write("Value: " + x.value.inspect + "\n")
|
372
|
+
fptr.write("Level: " + x.lvl.inspect + "\n")
|
329
373
|
x.forward.each_with_index do |f,i|
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
puts '---------------- End Header -------------------------'
|
335
|
-
|
336
|
-
while not x.forward[0].is_a?(FooterNode)
|
337
|
-
x = x.forward[0]
|
338
|
-
puts '---------------- Node -------------------------'
|
339
|
-
puts x
|
340
|
-
puts x.key
|
341
|
-
puts x.value
|
342
|
-
puts x.lvl
|
343
|
-
x.forward.each_with_index do |f,i|
|
344
|
-
puts '**** Forward entry %d ****' % i
|
345
|
-
puts f.key
|
346
|
-
puts f.value unless f.is_a?(FooterNode)
|
347
|
-
end
|
348
|
-
puts '---------------- End Node ---------------------'
|
374
|
+
fptr.write("**** Forward entry %d ****\n" % i)
|
375
|
+
fptr.write("Key: " + f.key.inspect + "\n")
|
376
|
+
fptr.write("Value: " + f.value.inspect + "\n") unless \
|
377
|
+
f.is_a?(FooterNode)
|
349
378
|
end
|
350
|
-
|
351
|
-
|
352
|
-
|
353
|
-
|
379
|
+
fptr.write("---------------- End Node ---------------------\n")
|
380
|
+
end
|
381
|
+
|
382
|
+
fptr.write("--------------------- Footer ---------------------\n")
|
383
|
+
fptr.write(x.forward[0].inspect + "\n")
|
384
|
+
fptr.write("--------------------- End Footer -----------------\n")
|
385
|
+
end
|
386
|
+
end
|
387
|
+
|
388
|
+
#-----------------------------------------------------------------------
|
389
|
+
# PRIVATE METHODS
|
390
|
+
#-----------------------------------------------------------------------
|
391
|
+
private
|
392
|
+
|
393
|
+
#-----------------------------------------------------------------------
|
394
|
+
# random_level
|
395
|
+
#-----------------------------------------------------------------------
|
396
|
+
def random_level
|
397
|
+
lvl = 0
|
398
|
+
while rand < OPTIMAL_PROBABILITY and lvl < MAX_LEVEL
|
399
|
+
lvl += 1
|
354
400
|
end
|
401
|
+
lvl
|
402
|
+
end
|
403
|
+
end
|
355
404
|
|
356
|
-
private
|
357
405
|
|
358
|
-
|
359
|
-
|
406
|
+
#---------------------------------------------------------------------------
|
407
|
+
# HeaderNode Class
|
408
|
+
#---------------------------------------------------------------------------
|
409
|
+
class HeaderNode
|
410
|
+
attr_accessor :forward
|
411
|
+
attr_reader :key
|
360
412
|
|
361
|
-
|
362
|
-
|
363
|
-
|
413
|
+
def initialize
|
414
|
+
@forward = []
|
415
|
+
@key = HeaderKey.new
|
416
|
+
end
|
364
417
|
|
365
|
-
|
366
|
-
|
418
|
+
def inspect
|
419
|
+
self.class.to_s
|
420
|
+
end
|
367
421
|
end
|
368
422
|
|
369
|
-
class HeaderNode
|
370
|
-
attr_accessor :forward
|
371
|
-
attr_reader :key
|
372
|
-
|
373
|
-
def initialize
|
374
|
-
@forward = []
|
375
|
-
@key = HeaderKey.new
|
376
|
-
end
|
377
|
-
end
|
378
423
|
|
424
|
+
#---------------------------------------------------------------------------
|
425
|
+
# HeaderKey Class
|
426
|
+
#---------------------------------------------------------------------------
|
379
427
|
class HeaderKey
|
380
|
-
|
381
|
-
|
382
|
-
|
428
|
+
def ==(other)
|
429
|
+
false
|
430
|
+
end
|
383
431
|
end
|
384
432
|
|
433
|
+
|
434
|
+
#---------------------------------------------------------------------------
|
435
|
+
# FooterNode Class
|
436
|
+
#---------------------------------------------------------------------------
|
385
437
|
class FooterNode
|
386
|
-
|
438
|
+
attr_reader :key
|
387
439
|
|
388
|
-
|
389
|
-
|
390
|
-
|
440
|
+
def initialize
|
441
|
+
@key = FooterKey.new
|
442
|
+
end
|
443
|
+
|
444
|
+
def inspect
|
445
|
+
self.class.to_s
|
446
|
+
end
|
391
447
|
end
|
392
448
|
|
449
|
+
|
450
|
+
#---------------------------------------------------------------------------
|
451
|
+
# FooterKey Class
|
452
|
+
#---------------------------------------------------------------------------
|
393
453
|
class FooterKey
|
394
|
-
|
395
|
-
|
396
|
-
|
454
|
+
def inspect
|
455
|
+
self.class.to_s
|
456
|
+
end
|
457
|
+
|
458
|
+
def <(other)
|
459
|
+
false
|
460
|
+
end
|
397
461
|
end
|
398
462
|
|
399
463
|
|
464
|
+
#---------------------------------------------------------------------------
|
465
|
+
# Node Class
|
466
|
+
#---------------------------------------------------------------------------
|
400
467
|
class Node
|
401
|
-
|
402
|
-
|
403
|
-
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
468
|
+
attr_accessor :forward, :value
|
469
|
+
attr_reader :key, :lvl
|
470
|
+
|
471
|
+
def initialize(lvl, key, value)
|
472
|
+
@lvl = lvl
|
473
|
+
@forward = []
|
474
|
+
@key = key
|
475
|
+
@value = value
|
476
|
+
end
|
477
|
+
|
478
|
+
def inspect
|
479
|
+
self.class.to_s
|
480
|
+
end
|
410
481
|
end
|
411
482
|
|
412
483
|
end
|