lithos 0.1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +28 -0
- data/LICENSE.txt +21 -0
- data/README.md +101 -0
- data/ext/lithos/extconf.rb +20 -0
- data/ext/lithos/lithos.cpp +1026 -0
- data/lib/lithos/version.rb +5 -0
- data/lib/lithos.rb +170 -0
- metadata +119 -0
data/lib/lithos.rb
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lithos/version"
|
|
4
|
+
require "lithos/lithos" # native extension: defines Lithos::DB + Lithos::Error
|
|
5
|
+
|
|
6
|
+
# lithos — a small embedded, ordered, crash-safe key-value store.
|
|
7
|
+
#
|
|
8
|
+
# Lithos.open("data/db") do |db|
|
|
9
|
+
# db["alpha"] = "1"
|
|
10
|
+
# db.put("\x00bin\xff", "raw") # binary keys/values welcome
|
|
11
|
+
# db["alpha"] # => "1"
|
|
12
|
+
# db.scan(gte: "a", lt: "m") { |k, v| } # ordered range
|
|
13
|
+
# end # auto-closed (flushes + fsyncs)
|
|
14
|
+
module Lithos
|
|
15
|
+
# Open a store at `path` (a directory, created if missing). Options:
|
|
16
|
+
# sync: durably fsync each write before it returns (default true)
|
|
17
|
+
# memtable_bytes: flush the in-memory table to an SSTable past this size
|
|
18
|
+
#
|
|
19
|
+
# With a block: yields the store, closes it when the block returns (even on
|
|
20
|
+
# error), and returns the block's value. Without a block: returns the store,
|
|
21
|
+
# and the caller must #close it.
|
|
22
|
+
def self.open(path, **opts)
|
|
23
|
+
db = DB.new(path, **opts)
|
|
24
|
+
return db unless block_given?
|
|
25
|
+
|
|
26
|
+
begin
|
|
27
|
+
yield db
|
|
28
|
+
ensure
|
|
29
|
+
db.close
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class DB
|
|
34
|
+
include Enumerable
|
|
35
|
+
|
|
36
|
+
# --- mutators (forbidden while an each/scan is in progress) --------------
|
|
37
|
+
# The merge iterator holds raw pointers into memory-mapped SSTables and live
|
|
38
|
+
# memtable iterators; mutating mid-scan would free them underneath it. We
|
|
39
|
+
# refuse here with a plain Ruby exception (which unwinds safely), rather than
|
|
40
|
+
# let it reach the C engine during iteration.
|
|
41
|
+
|
|
42
|
+
def put(key, value)
|
|
43
|
+
forbid_during_iteration!
|
|
44
|
+
_put(key, value)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def delete(key)
|
|
48
|
+
forbid_during_iteration!
|
|
49
|
+
_delete(key)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def flush
|
|
53
|
+
forbid_during_iteration!
|
|
54
|
+
_flush
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def compact
|
|
58
|
+
forbid_during_iteration!
|
|
59
|
+
_compact
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def close
|
|
63
|
+
forbid_during_iteration!
|
|
64
|
+
_close
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Hash-like access. `db[k]` => value or nil; `db[k] = v` stores v.
|
|
68
|
+
def [](key)
|
|
69
|
+
get(key)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def []=(key, value)
|
|
73
|
+
put(key, value)
|
|
74
|
+
value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Like Hash#fetch: return the value, else the block result, else the default,
|
|
78
|
+
# else raise KeyError. (Stored values are never nil, so nil == absent.)
|
|
79
|
+
def fetch(key, *default)
|
|
80
|
+
if default.size > 1
|
|
81
|
+
raise ArgumentError, "wrong number of arguments (given #{default.size + 1}, expected 1..2)"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
value = get(key)
|
|
85
|
+
return value unless value.nil?
|
|
86
|
+
return yield(key) if block_given?
|
|
87
|
+
return default.first unless default.empty?
|
|
88
|
+
|
|
89
|
+
raise KeyError, "key not found: #{key.inspect}"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
alias include? key?
|
|
93
|
+
alias has_key? key?
|
|
94
|
+
alias member? key?
|
|
95
|
+
|
|
96
|
+
# Iterate all pairs in ascending (unsigned-byte) key order.
|
|
97
|
+
def each(&block)
|
|
98
|
+
return enum_for(:each) unless block
|
|
99
|
+
|
|
100
|
+
bounded_each(nil, false, nil, false, &block)
|
|
101
|
+
end
|
|
102
|
+
alias each_pair each
|
|
103
|
+
|
|
104
|
+
# Iterate keys only, ascending.
|
|
105
|
+
def each_key
|
|
106
|
+
return enum_for(:each_key) unless block_given?
|
|
107
|
+
|
|
108
|
+
bounded_each(nil, false, nil, false) { |k, _| yield k }
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# Ordered range scan. Bounds are optional; gt/lt are exclusive, gte/lte
|
|
112
|
+
# inclusive. Returns an Enumerator without a block.
|
|
113
|
+
def scan(gte: nil, gt: nil, lte: nil, lt: nil, &block)
|
|
114
|
+
lower, lower_incl = gt ? [gt, false] : (gte ? [gte, true] : [nil, false])
|
|
115
|
+
upper, upper_incl = lt ? [lt, false] : (lte ? [lte, true] : [nil, false])
|
|
116
|
+
bounded_each(lower, lower_incl, upper, upper_incl, &block)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Half-open range [from, to) in ascending order.
|
|
120
|
+
def range(from, to, &block)
|
|
121
|
+
bounded_each(from, true, to, false, &block)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Number of live keys. O(n) — counts via an ordered scan.
|
|
125
|
+
def size
|
|
126
|
+
n = 0
|
|
127
|
+
each_key { n += 1 }
|
|
128
|
+
n
|
|
129
|
+
end
|
|
130
|
+
alias length size
|
|
131
|
+
|
|
132
|
+
def empty?
|
|
133
|
+
each { return false }
|
|
134
|
+
true
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def to_h
|
|
138
|
+
h = {}
|
|
139
|
+
each { |k, v| h[k] = v }
|
|
140
|
+
h
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def inspect
|
|
144
|
+
"#<Lithos::DB path=#{path.inspect}#{closed? ? ' (closed)' : ''}>"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
private
|
|
148
|
+
|
|
149
|
+
def forbid_during_iteration!
|
|
150
|
+
return unless (@iterating || 0).positive?
|
|
151
|
+
|
|
152
|
+
raise Error, "lithos: cannot modify the store during iteration"
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
# Shared range driver; positional args so enum_for forwards cleanly. The
|
|
156
|
+
# @iterating depth lets reads nest while mutations are blocked, and the
|
|
157
|
+
# ensure guarantees the count is restored even on break/exception.
|
|
158
|
+
def bounded_each(lower, lower_incl, upper, upper_incl, &block)
|
|
159
|
+
return enum_for(:bounded_each, lower, lower_incl, upper, upper_incl) unless block
|
|
160
|
+
|
|
161
|
+
@iterating = (@iterating || 0) + 1
|
|
162
|
+
begin
|
|
163
|
+
_each_range(lower, lower_incl, upper, upper_incl, &block)
|
|
164
|
+
ensure
|
|
165
|
+
@iterating -= 1
|
|
166
|
+
end
|
|
167
|
+
self
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lithos
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- ned
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rake
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '13.0'
|
|
19
|
+
type: :development
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '13.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rake-compiler
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '1.2'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '1.2'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: minitest
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '5.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '5.0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: vcvars
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - "~>"
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0.1'
|
|
61
|
+
- - ">="
|
|
62
|
+
- !ruby/object:Gem::Version
|
|
63
|
+
version: 0.1.1
|
|
64
|
+
type: :development
|
|
65
|
+
prerelease: false
|
|
66
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
67
|
+
requirements:
|
|
68
|
+
- - "~>"
|
|
69
|
+
- !ruby/object:Gem::Version
|
|
70
|
+
version: '0.1'
|
|
71
|
+
- - ">="
|
|
72
|
+
- !ruby/object:Gem::Version
|
|
73
|
+
version: 0.1.1
|
|
74
|
+
description: |
|
|
75
|
+
lithos is a self-contained embedded key-value store written from scratch as a
|
|
76
|
+
native extension — no external database dependency. It uses a log-structured
|
|
77
|
+
merge (LSM) tree: a write-ahead log makes every write durable, an in-memory
|
|
78
|
+
sorted memtable flushes to immutable SSTables (with bloom filters), and
|
|
79
|
+
compaction merges them. Keys and values are arbitrary binary strings; keys
|
|
80
|
+
are kept in sorted order so you get ordered iteration and range scans, plus
|
|
81
|
+
crash recovery via WAL replay. Windows MSVC (mswin) Ruby only.
|
|
82
|
+
executables: []
|
|
83
|
+
extensions:
|
|
84
|
+
- ext/lithos/extconf.rb
|
|
85
|
+
extra_rdoc_files: []
|
|
86
|
+
files:
|
|
87
|
+
- CHANGELOG.md
|
|
88
|
+
- LICENSE.txt
|
|
89
|
+
- README.md
|
|
90
|
+
- ext/lithos/extconf.rb
|
|
91
|
+
- ext/lithos/lithos.cpp
|
|
92
|
+
- lib/lithos.rb
|
|
93
|
+
- lib/lithos/version.rb
|
|
94
|
+
homepage: https://github.com/main-path/lithos
|
|
95
|
+
licenses:
|
|
96
|
+
- MIT
|
|
97
|
+
metadata:
|
|
98
|
+
homepage_uri: https://github.com/main-path/lithos
|
|
99
|
+
changelog_uri: https://github.com/main-path/lithos/blob/main/CHANGELOG.md
|
|
100
|
+
bug_tracker_uri: https://github.com/main-path/lithos/issues
|
|
101
|
+
rubygems_mfa_required: 'true'
|
|
102
|
+
rdoc_options: []
|
|
103
|
+
require_paths:
|
|
104
|
+
- lib
|
|
105
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '3.0'
|
|
110
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
111
|
+
requirements:
|
|
112
|
+
- - ">="
|
|
113
|
+
- !ruby/object:Gem::Version
|
|
114
|
+
version: '0'
|
|
115
|
+
requirements: []
|
|
116
|
+
rubygems_version: 3.6.9
|
|
117
|
+
specification_version: 4
|
|
118
|
+
summary: A small embedded, ordered, crash-safe key-value store (LSM-tree) for Ruby.
|
|
119
|
+
test_files: []
|