rubydb 0.1.5 → 0.1.6
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 +4 -4
- data/.gitignore +4 -0
- data/CHANGELOG.md +14 -0
- data/Gemfile.lock +1 -1
- data/README.md +296 -227
- data/Rakefile +6 -1
- data/accelerator/bin/SHA256SUMS +6 -0
- data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
- data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
- data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
- data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
- data/accelerator/go.mod +3 -0
- data/accelerator/internal/execution/aggregate.go +94 -0
- data/accelerator/internal/execution/distinct.go +22 -0
- data/accelerator/internal/execution/filter.go +73 -0
- data/accelerator/internal/execution/join.go +79 -0
- data/accelerator/internal/execution/operators.go +167 -0
- data/accelerator/internal/execution/scan.go +20 -0
- data/accelerator/internal/execution/sort.go +62 -0
- data/accelerator/internal/execution/types.go +136 -0
- data/accelerator/internal/execution/value.go +67 -0
- data/accelerator/internal/memory/arena.go +47 -0
- data/accelerator/internal/memory/reuse.go +22 -0
- data/accelerator/internal/metrics/registry.go +67 -0
- data/accelerator/internal/parallel/bounded_queue.go +56 -0
- data/accelerator/internal/parallel/scheduler.go +47 -0
- data/accelerator/internal/parallel/worker_pool.go +53 -0
- data/accelerator/internal/protocol/cancellation.go +48 -0
- data/accelerator/internal/protocol/columnar.go +263 -0
- data/accelerator/internal/protocol/frame.go +187 -0
- data/accelerator/internal/runtime/worker.go +521 -0
- data/accelerator/internal/storage/page_reader.go +81 -0
- data/accelerator/internal/storage/snapshot_scan.go +539 -0
- data/accelerator/internal/wal/checksum.go +13 -0
- data/accelerator/internal/wal/compression.go +41 -0
- data/accelerator/internal/wal/group_commit.go +24 -0
- data/accelerator/internal/wal/record_encoder.go +40 -0
- data/adapters/activerecord/README.md +8 -3
- data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +50 -42
- data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
- data/docs/README.md +3 -1
- data/docs/architecture/go-accelerator.md +179 -0
- data/docs/cli.md +24 -0
- data/docs/contributing/benchmarking.md +16 -0
- data/docs/developer/local-development.md +32 -0
- data/docs/release.md +2 -2
- data/lessons/02-local-development.md +2 -2
- data/lessons/04-rails-complex-apps.md +2 -2
- data/lessons/05-rubydb-production-server.md +2 -2
- data/lessons/07-hybrid-microservices.md +175 -90
- data/lessons/10-release-readiness.md +184 -117
- data/lessons/11-community-adapter.md +323 -0
- data/lessons/12-rails-ecommerce-pressure.md +263 -0
- data/lib/rubydb/accelerator/client.rb +451 -0
- data/lib/rubydb/accelerator/error.rb +22 -0
- data/lib/rubydb/accelerator/manager.rb +606 -0
- data/lib/rubydb/accelerator.rb +13 -0
- data/lib/rubydb/cli/application.rb +6 -1
- data/lib/rubydb/cli/commands/accelerator.rb +72 -0
- data/lib/rubydb/cli/commands/doctor.rb +3 -0
- data/lib/rubydb/client/client.rb +7 -0
- data/lib/rubydb/client/connection.rb +15 -0
- data/lib/rubydb/client/result.rb +5 -1
- data/lib/rubydb/configuration/defaults.rb +12 -0
- data/lib/rubydb/configuration/validation.rb +8 -1
- data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
- data/lib/rubydb/execution/cost_model.rb +72 -0
- data/lib/rubydb/execution/executor.rb +373 -11
- data/lib/rubydb/execution/operator_selection.rb +57 -0
- data/lib/rubydb/execution/physical_plan.rb +47 -0
- data/lib/rubydb/execution/planner.rb +12 -46
- data/lib/rubydb/execution/sort_executor.rb +22 -8
- data/lib/rubydb/indexes/btree.rb +31 -2
- data/lib/rubydb/rubydb.rb +7 -1
- data/lib/rubydb/server/session.rb +45 -0
- data/lib/rubydb/storage/engine.rb +74 -12
- data/lib/rubydb/storage/snapshot_reader.rb +167 -0
- data/lib/rubydb/version.rb +1 -1
- data/lib/rubydb/wal/archive.rb +17 -0
- data/lib/rubydb/wal/wal.rb +1 -0
- data/rubydb.gemspec +12 -2
- data/scripts/build_accelerator +49 -0
- data/scripts/release +34 -4
- data/scripts/replication_failover_drill +2 -2
- metadata +49 -1
|
@@ -97,11 +97,11 @@ module RubyDB
|
|
|
97
97
|
comparison = 0
|
|
98
98
|
order_by.each do |order|
|
|
99
99
|
col = order.column.to_s
|
|
100
|
-
val_a = a
|
|
101
|
-
val_b = b
|
|
100
|
+
val_a = order_value(a, col)
|
|
101
|
+
val_b = order_value(b, col)
|
|
102
102
|
|
|
103
103
|
comparison = compare_values(val_a, val_b)
|
|
104
|
-
comparison = -comparison if order.direction
|
|
104
|
+
comparison = -comparison if order.direction.to_s.casecmp?("desc")
|
|
105
105
|
break unless comparison == 0
|
|
106
106
|
end
|
|
107
107
|
comparison
|
|
@@ -112,9 +112,11 @@ module RubyDB
|
|
|
112
112
|
return 0 if a.nil? && b.nil?
|
|
113
113
|
return -1 if a.nil?
|
|
114
114
|
return 1 if b.nil?
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
115
|
+
|
|
116
|
+
comparison = a <=> b
|
|
117
|
+
return comparison unless comparison.nil?
|
|
118
|
+
|
|
119
|
+
a.to_s <=> b.to_s
|
|
118
120
|
end
|
|
119
121
|
|
|
120
122
|
def external_sort(rows, order_by, temp_dir)
|
|
@@ -166,14 +168,26 @@ module RubyDB
|
|
|
166
168
|
def compare_rows(a, b, order_by)
|
|
167
169
|
order_by.each do |order|
|
|
168
170
|
col = order.column.to_s
|
|
169
|
-
val_a = a
|
|
170
|
-
val_b = b
|
|
171
|
+
val_a = order_value(a, col)
|
|
172
|
+
val_b = order_value(b, col)
|
|
171
173
|
comparison = compare_values(val_a, val_b)
|
|
174
|
+
comparison = -comparison if order.direction.to_s.casecmp?("desc")
|
|
172
175
|
return comparison unless comparison == 0
|
|
173
176
|
end
|
|
174
177
|
0
|
|
175
178
|
end
|
|
176
179
|
|
|
180
|
+
def order_value(row, column)
|
|
181
|
+
return row[column] if row.key?(column)
|
|
182
|
+
return row[column.to_sym] if row.key?(column.to_sym)
|
|
183
|
+
|
|
184
|
+
short_column = column.split(".").last
|
|
185
|
+
return row[short_column] if row.key?(short_column)
|
|
186
|
+
return row[short_column.to_sym] if row.key?(short_column.to_sym)
|
|
187
|
+
|
|
188
|
+
nil
|
|
189
|
+
end
|
|
190
|
+
|
|
177
191
|
attr_reader :stats
|
|
178
192
|
end
|
|
179
193
|
end
|
data/lib/rubydb/indexes/btree.rb
CHANGED
|
@@ -133,6 +133,27 @@ module RubyDB
|
|
|
133
133
|
})
|
|
134
134
|
end
|
|
135
135
|
|
|
136
|
+
# Return a stable, ordered copy of the in-memory B-tree entries. Indexes
|
|
137
|
+
# are rebuilt from metadata on open, so this immutable value is the
|
|
138
|
+
# correct hand-off to a storage snapshot; it is not a second persisted
|
|
139
|
+
# index format.
|
|
140
|
+
def snapshot_entries
|
|
141
|
+
@lock.synchronize do
|
|
142
|
+
return [] unless @root
|
|
143
|
+
|
|
144
|
+
leaf = @root
|
|
145
|
+
leaf = leaf.children.first until leaf.is_leaf
|
|
146
|
+
entries = []
|
|
147
|
+
while leaf
|
|
148
|
+
leaf.keys.each_with_index do |key, index|
|
|
149
|
+
entries << {key: key, value: leaf.values[index]}
|
|
150
|
+
end
|
|
151
|
+
leaf = leaf.next_leaf
|
|
152
|
+
end
|
|
153
|
+
entries
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
136
157
|
private
|
|
137
158
|
|
|
138
159
|
def initialize_root
|
|
@@ -151,9 +172,17 @@ module RubyDB
|
|
|
151
172
|
def extract_key(row)
|
|
152
173
|
if @columns.size == 1
|
|
153
174
|
column = @columns.first
|
|
154
|
-
|
|
175
|
+
return row[column] if row.key?(column)
|
|
176
|
+
return row[column.to_s] if row.key?(column.to_s)
|
|
177
|
+
|
|
178
|
+
row[column.to_sym] if column.respond_to?(:to_sym)
|
|
155
179
|
else
|
|
156
|
-
@columns.map
|
|
180
|
+
@columns.map do |column|
|
|
181
|
+
next row[column] if row.key?(column)
|
|
182
|
+
next row[column.to_s] if row.key?(column.to_s)
|
|
183
|
+
|
|
184
|
+
column.respond_to?(:to_sym) ? row[column.to_sym] : nil
|
|
185
|
+
end
|
|
157
186
|
end
|
|
158
187
|
end
|
|
159
188
|
|
data/lib/rubydb/rubydb.rb
CHANGED
|
@@ -12,6 +12,7 @@ require "set"
|
|
|
12
12
|
require_relative "version"
|
|
13
13
|
require_relative "constants"
|
|
14
14
|
require_relative "build_info"
|
|
15
|
+
require_relative "accelerator"
|
|
15
16
|
|
|
16
17
|
require_relative "errors/error"
|
|
17
18
|
require_relative "errors/connection_error"
|
|
@@ -77,8 +78,12 @@ require_relative "sql/planner/binder"
|
|
|
77
78
|
require_relative "sql/planner/analyzer"
|
|
78
79
|
require_relative "sql/planner/type_checker"
|
|
79
80
|
|
|
80
|
-
require_relative "execution/executor"
|
|
81
81
|
require_relative "execution/plan"
|
|
82
|
+
require_relative "execution/physical_plan"
|
|
83
|
+
require_relative "execution/cost_model"
|
|
84
|
+
require_relative "execution/operator_selection"
|
|
85
|
+
require_relative "execution/accelerator_dispatch"
|
|
86
|
+
require_relative "execution/executor"
|
|
82
87
|
require_relative "execution/planner"
|
|
83
88
|
require_relative "execution/optimizer"
|
|
84
89
|
require_relative "execution/expression"
|
|
@@ -302,6 +307,7 @@ require_relative "cli/commands/diff"
|
|
|
302
307
|
require_relative "cli/commands/inspect"
|
|
303
308
|
require_relative "cli/commands/vacuum"
|
|
304
309
|
require_relative "cli/commands/doctor"
|
|
310
|
+
require_relative "cli/commands/accelerator"
|
|
305
311
|
|
|
306
312
|
module RubyDB
|
|
307
313
|
# Main database class
|
|
@@ -80,6 +80,8 @@ module RubyDB
|
|
|
80
80
|
case request[:type]
|
|
81
81
|
when "query"
|
|
82
82
|
process_query(request[:sql], request[:params] || [], request[:deadline_at], cancellation)
|
|
83
|
+
when "metadata"
|
|
84
|
+
process_metadata(request[:table])
|
|
83
85
|
when "prepare"
|
|
84
86
|
process_prepare(request[:sql])
|
|
85
87
|
when "execute"
|
|
@@ -195,6 +197,49 @@ module RubyDB
|
|
|
195
197
|
}
|
|
196
198
|
end
|
|
197
199
|
|
|
200
|
+
def process_metadata(table_name = nil)
|
|
201
|
+
engine = @config[:engine]
|
|
202
|
+
raise RubyDB::ServerError, "Session has no database engine" unless engine
|
|
203
|
+
|
|
204
|
+
if table_name.nil? || table_name.to_s.empty?
|
|
205
|
+
return {
|
|
206
|
+
success: true,
|
|
207
|
+
type: "metadata_result",
|
|
208
|
+
metadata: {tables: engine.list_tables.map(&:to_s)},
|
|
209
|
+
timestamp: Time.now.iso8601
|
|
210
|
+
}
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
table_key = table_name.to_s
|
|
214
|
+
metadata = engine.table_metadata[table_key] || engine.table_metadata[table_key.to_sym]
|
|
215
|
+
raise RubyDB::DatabaseError, "Table '#{table_name}' does not exist" unless metadata
|
|
216
|
+
|
|
217
|
+
columns = engine.table_columns(table_key).map do |column|
|
|
218
|
+
{
|
|
219
|
+
name: column.name.to_s,
|
|
220
|
+
type: column.type.to_s,
|
|
221
|
+
nullable: column.nullable?,
|
|
222
|
+
primary_key: column.primary_key?,
|
|
223
|
+
default: column.has_default? ? column.default : nil
|
|
224
|
+
}
|
|
225
|
+
end
|
|
226
|
+
indexes = engine.index_manager.get_indexes_for_table(table_key).map do |index|
|
|
227
|
+
{name: index.name.to_s, unique: index.unique, columns: index.columns.map(&:to_s)}
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
{
|
|
231
|
+
success: true,
|
|
232
|
+
type: "metadata_result",
|
|
233
|
+
metadata: {
|
|
234
|
+
tables: [table_key],
|
|
235
|
+
columns: columns,
|
|
236
|
+
indexes: indexes,
|
|
237
|
+
constraints: metadata[:constraints] || []
|
|
238
|
+
},
|
|
239
|
+
timestamp: Time.now.iso8601
|
|
240
|
+
}
|
|
241
|
+
end
|
|
242
|
+
|
|
198
243
|
def process_prepare(sql)
|
|
199
244
|
permission_error = authorize_sql(sql)
|
|
200
245
|
return permission_error if permission_error
|
|
@@ -5,6 +5,7 @@ require "json"
|
|
|
5
5
|
require "securerandom"
|
|
6
6
|
require "fileutils"
|
|
7
7
|
require "monitor"
|
|
8
|
+
require_relative "../accelerator"
|
|
8
9
|
|
|
9
10
|
# Storage components
|
|
10
11
|
require_relative "database_lock"
|
|
@@ -17,6 +18,7 @@ require_relative "tuple"
|
|
|
17
18
|
require_relative "serializer"
|
|
18
19
|
require_relative "deserializer"
|
|
19
20
|
require_relative "visibility_map"
|
|
21
|
+
require_relative "snapshot_reader"
|
|
20
22
|
require_relative "../mvcc/version_store"
|
|
21
23
|
require_relative "storage_layout"
|
|
22
24
|
require_relative "../indexes/index"
|
|
@@ -53,12 +55,13 @@ module RubyDB
|
|
|
53
55
|
NULL_BITMAP_FLAG = 0x02
|
|
54
56
|
VARIABLE_LENGTH_PREFIXES_FLAG = 0x04
|
|
55
57
|
|
|
56
|
-
attr_reader :storage_manager, :buffer_pool, :page_manager, :catalog, :path, :last_commit_ack
|
|
57
|
-
attr_reader :table_metadata, :transaction_manager, :wal, :crash_recovery, :version_store, :index_manager
|
|
58
|
+
attr_reader :storage_manager, :buffer_pool, :page_manager, :catalog, :path, :last_commit_ack, :config, :accelerator
|
|
59
|
+
attr_reader :table_metadata, :transaction_manager, :wal, :crash_recovery, :version_store, :index_manager, :snapshot_reader
|
|
58
60
|
|
|
59
61
|
def initialize(path, config = {})
|
|
60
62
|
@path = path
|
|
61
63
|
@config = config
|
|
64
|
+
@accelerator = RubyDB::Accelerator::Client.new(config[:accelerator] || {})
|
|
62
65
|
@database_lock = DatabaseLock.new(path)
|
|
63
66
|
@database_lock.acquire!
|
|
64
67
|
@catalog = config[:catalog] || Catalog::Catalog.new
|
|
@@ -117,7 +120,7 @@ module RubyDB
|
|
|
117
120
|
|
|
118
121
|
# Initialize WAL
|
|
119
122
|
wal_dir = config[:wal_dir] || "#{path}.wal"
|
|
120
|
-
@wal = WAL::WAL.new(wal_dir, recovery: false) # Defer recovery until after metadata load
|
|
123
|
+
@wal = WAL::WAL.new(wal_dir, config.merge(recovery: false)) # Defer recovery until after metadata load
|
|
121
124
|
@wal.attach_engine(self)
|
|
122
125
|
|
|
123
126
|
# Initialize crash recovery
|
|
@@ -131,6 +134,7 @@ module RubyDB
|
|
|
131
134
|
|
|
132
135
|
# Build/load indexes only after metadata and crash recovery are ready.
|
|
133
136
|
@index_manager = Indexes::IndexManager.new(self)
|
|
137
|
+
@snapshot_reader = SnapshotReader.new(self)
|
|
134
138
|
|
|
135
139
|
# Start cleanup thread if configured
|
|
136
140
|
start_cleanup_thread if config[:auto_cleanup] != false
|
|
@@ -168,6 +172,31 @@ module RubyDB
|
|
|
168
172
|
@storage_manager.free_page(page_number)
|
|
169
173
|
end
|
|
170
174
|
|
|
175
|
+
# Yield a stable, immutable database snapshot to the accelerator. A
|
|
176
|
+
# transaction or an active MVCC writer makes the snapshot ineligible;
|
|
177
|
+
# callers use the normal Ruby path in that case.
|
|
178
|
+
def with_accelerator_snapshot
|
|
179
|
+
@lock.synchronize do
|
|
180
|
+
return nil unless accelerator_snapshot_eligible?
|
|
181
|
+
|
|
182
|
+
@storage_manager.flush
|
|
183
|
+
@visibility_map.flush
|
|
184
|
+
save_table_metadata
|
|
185
|
+
@snapshot_reader.with_snapshot { |snapshot| yield snapshot }
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def accelerator_snapshot_hidden_row_ids
|
|
190
|
+
@visibility_map.visibility_info.each_with_object([]) do |(row_id, info), hidden|
|
|
191
|
+
state = (info[:state] || info["state"]).to_s.to_sym
|
|
192
|
+
hidden << row_id.to_i unless %i[visible committed].include?(state)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
def table_pages_for_snapshot(table_name)
|
|
197
|
+
@table_pages[resolve_table_name(table_name)] || []
|
|
198
|
+
end
|
|
199
|
+
|
|
171
200
|
# Record operations
|
|
172
201
|
def read_record(page_number, offset, length)
|
|
173
202
|
@storage_manager.read_record(page_number, offset, length)
|
|
@@ -1483,6 +1512,14 @@ module RubyDB
|
|
|
1483
1512
|
current_transaction_manager && current_transaction_manager[:active]
|
|
1484
1513
|
end
|
|
1485
1514
|
|
|
1515
|
+
def accelerator_snapshot_eligible?
|
|
1516
|
+
return false unless @is_open
|
|
1517
|
+
return false if in_transaction?
|
|
1518
|
+
return false unless @visibility_map.active_transaction_count.zero?
|
|
1519
|
+
|
|
1520
|
+
current_transaction_id.to_i.zero?
|
|
1521
|
+
end
|
|
1522
|
+
|
|
1486
1523
|
def create_savepoint(name)
|
|
1487
1524
|
@lock.synchronize do
|
|
1488
1525
|
tx = current_transaction_manager
|
|
@@ -1613,6 +1650,8 @@ module RubyDB
|
|
|
1613
1650
|
return unless @is_open
|
|
1614
1651
|
|
|
1615
1652
|
stop_cleanup_thread
|
|
1653
|
+
@accelerator&.close
|
|
1654
|
+
@snapshot_reader&.close
|
|
1616
1655
|
|
|
1617
1656
|
# Flush WAL first (ensures all mutations are recorded)
|
|
1618
1657
|
if @wal
|
|
@@ -1653,6 +1692,7 @@ module RubyDB
|
|
|
1653
1692
|
transaction_rollback: @stats[:transaction_rollback],
|
|
1654
1693
|
wal_writes: @stats[:wal_writes],
|
|
1655
1694
|
crash_recoveries: @stats[:crash_recoveries],
|
|
1695
|
+
accelerator: @accelerator&.stats,
|
|
1656
1696
|
last_maintenance_error: @last_maintenance_error,
|
|
1657
1697
|
is_open: @is_open,
|
|
1658
1698
|
visibility_rows: @visibility_map.visibility_info.size,
|
|
@@ -2092,7 +2132,7 @@ module RubyDB
|
|
|
2092
2132
|
end
|
|
2093
2133
|
|
|
2094
2134
|
def validate_referential_update!(table_name, old_row, new_row)
|
|
2095
|
-
return true if old_row
|
|
2135
|
+
return true if referential_row_values_equal?(table_name, old_row, new_row)
|
|
2096
2136
|
|
|
2097
2137
|
@table_metadata.each do |child_table, child_metadata|
|
|
2098
2138
|
(child_metadata[:constraints] || []).each do |raw_definition|
|
|
@@ -2102,13 +2142,13 @@ module RubyDB
|
|
|
2102
2142
|
|
|
2103
2143
|
child_columns = Array(definition[:columns]).map(&:to_sym)
|
|
2104
2144
|
parent_columns = Array(definition[:reference_columns] || :id).map(&:to_sym)
|
|
2105
|
-
next unless parent_columns.any? { |column| old_row
|
|
2145
|
+
next unless parent_columns.any? { |column| row_value(old_row, column) != row_value(new_row, column) }
|
|
2106
2146
|
|
|
2107
2147
|
child_rows = select_rows(child_table, child_metadata[:columns], visibility_check: false)
|
|
2108
2148
|
referenced = child_rows.any? do |child_row|
|
|
2109
2149
|
child_columns.each_with_index.all? do |child_column, index|
|
|
2110
|
-
child_value = child_row
|
|
2111
|
-
parent_value = old_row
|
|
2150
|
+
child_value = row_value(child_row, child_column)
|
|
2151
|
+
parent_value = row_value(old_row, parent_columns[index])
|
|
2112
2152
|
!child_value.nil? && child_value == parent_value
|
|
2113
2153
|
end
|
|
2114
2154
|
end
|
|
@@ -2134,7 +2174,7 @@ module RubyDB
|
|
|
2134
2174
|
end
|
|
2135
2175
|
|
|
2136
2176
|
def apply_referential_update!(table_name, old_row, new_row)
|
|
2137
|
-
return true if old_row
|
|
2177
|
+
return true if referential_row_values_equal?(table_name, old_row, new_row)
|
|
2138
2178
|
|
|
2139
2179
|
@table_metadata.each do |child_table, child_metadata|
|
|
2140
2180
|
(child_metadata[:constraints] || []).each do |raw_definition|
|
|
@@ -2144,12 +2184,12 @@ module RubyDB
|
|
|
2144
2184
|
|
|
2145
2185
|
child_columns = Array(definition[:columns]).map(&:to_sym)
|
|
2146
2186
|
parent_columns = Array(definition[:reference_columns] || :id).map(&:to_sym)
|
|
2147
|
-
next unless parent_columns.any? { |column| old_row
|
|
2187
|
+
next unless parent_columns.any? { |column| row_value(old_row, column) != row_value(new_row, column) }
|
|
2148
2188
|
child_rows = select_rows(child_table, child_metadata[:columns], visibility_check: false)
|
|
2149
2189
|
matching_rows = child_rows.select do |child_row|
|
|
2150
2190
|
child_columns.each_with_index.all? do |child_column, index|
|
|
2151
|
-
child_value = child_row
|
|
2152
|
-
parent_value = old_row
|
|
2191
|
+
child_value = row_value(child_row, child_column)
|
|
2192
|
+
parent_value = row_value(old_row, parent_columns[index])
|
|
2153
2193
|
!child_value.nil? && child_value == parent_value
|
|
2154
2194
|
end
|
|
2155
2195
|
end
|
|
@@ -2160,7 +2200,7 @@ module RubyDB
|
|
|
2160
2200
|
child_row_id = child_row[:_row_id] || child_row["_row_id"]
|
|
2161
2201
|
updates = case action
|
|
2162
2202
|
when "cascade"
|
|
2163
|
-
child_columns.each_with_index.to_h { |column, index| [column, new_row
|
|
2203
|
+
child_columns.each_with_index.to_h { |column, index| [column, row_value(new_row, parent_columns[index])] }
|
|
2164
2204
|
when "set_null"
|
|
2165
2205
|
child_columns.to_h { |column| [column, nil] }
|
|
2166
2206
|
when "set_default"
|
|
@@ -2178,6 +2218,28 @@ module RubyDB
|
|
|
2178
2218
|
true
|
|
2179
2219
|
end
|
|
2180
2220
|
|
|
2221
|
+
# Rows can cross the storage, SQL, WAL, and recovery boundaries with
|
|
2222
|
+
# symbol or string keys. Referential actions compare column values, not
|
|
2223
|
+
# the Ruby key representation. Without this normalization, updating a
|
|
2224
|
+
# non-key parent column could look like a primary-key change during WAL
|
|
2225
|
+
# replay and incorrectly fail ON UPDATE RESTRICT.
|
|
2226
|
+
def row_value(row, column)
|
|
2227
|
+
return row[column] if row.key?(column)
|
|
2228
|
+
return row[column.to_s] if row.key?(column.to_s)
|
|
2229
|
+
|
|
2230
|
+
nil
|
|
2231
|
+
end
|
|
2232
|
+
|
|
2233
|
+
def referential_row_values_equal?(table_name, old_row, new_row)
|
|
2234
|
+
columns = @table_metadata[table_name.to_s] || @table_metadata[table_name.to_sym]
|
|
2235
|
+
column_definitions = columns && columns[:columns]
|
|
2236
|
+
return old_row == new_row if column_definitions.nil?
|
|
2237
|
+
|
|
2238
|
+
column_definitions.all? do |column|
|
|
2239
|
+
row_value(old_row, column.name.to_sym) == row_value(new_row, column.name.to_sym)
|
|
2240
|
+
end
|
|
2241
|
+
end
|
|
2242
|
+
|
|
2181
2243
|
def cache_hit_rate
|
|
2182
2244
|
total = @stats[:cache_hits] + @stats[:cache_misses]
|
|
2183
2245
|
return 0.0 if total == 0
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "json"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
module RubyDB
|
|
9
|
+
module Storage
|
|
10
|
+
# Creates a validated snapshot manifest for the Go accelerator. The
|
|
11
|
+
# default is a lock-protected read of the already-flushed live file;
|
|
12
|
+
# operators can opt into a detached copy when they need one. Ruby remains
|
|
13
|
+
# responsible for locking, WAL durability, catalog state, and MVCC
|
|
14
|
+
# eligibility.
|
|
15
|
+
class SnapshotReader
|
|
16
|
+
FORMAT_VERSION = 1
|
|
17
|
+
|
|
18
|
+
def initialize(engine)
|
|
19
|
+
@engine = engine
|
|
20
|
+
@directory = "#{engine.path}.accelerator-snapshots"
|
|
21
|
+
@lock = Mutex.new
|
|
22
|
+
@active_paths = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def with_snapshot
|
|
26
|
+
snapshot = create
|
|
27
|
+
begin
|
|
28
|
+
yield snapshot
|
|
29
|
+
ensure
|
|
30
|
+
release(snapshot)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def close
|
|
35
|
+
@lock.synchronize do
|
|
36
|
+
@active_paths.keys.each { |path| delete_file(path) }
|
|
37
|
+
@active_paths.clear
|
|
38
|
+
Dir.rmdir(@directory) if Dir.exist?(@directory) && Dir.empty?(@directory)
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def create
|
|
45
|
+
source = File.expand_path(@engine.path)
|
|
46
|
+
raise StorageError, "Cannot create accelerator snapshot: database file is missing" unless File.file?(source)
|
|
47
|
+
|
|
48
|
+
# Engine#with_accelerator_snapshot holds the engine lock for the whole
|
|
49
|
+
# yield, including the Go read. In that guarded section the live file
|
|
50
|
+
# is already flushed and no Ruby writer can mutate it, so copying the
|
|
51
|
+
# complete database before every read only adds latency and doubles
|
|
52
|
+
# the storage I/O. Keep an opt-out copy mode for operators that need a
|
|
53
|
+
# detached file, but make the lock-protected direct path the default.
|
|
54
|
+
return direct_manifest(source) if direct_snapshot?
|
|
55
|
+
|
|
56
|
+
FileUtils.mkdir_p(@directory)
|
|
57
|
+
destination = File.join(@directory, "snapshot-#{Process.pid}-#{SecureRandom.hex(12)}.db")
|
|
58
|
+
FileUtils.copy_file(source, destination, true)
|
|
59
|
+
File.open(destination, "r+b") do |file|
|
|
60
|
+
file.flush
|
|
61
|
+
file.fsync
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
snapshot = manifest_for(
|
|
65
|
+
destination,
|
|
66
|
+
snapshot_id: File.basename(destination, ".db"),
|
|
67
|
+
file_sha256: Digest::SHA256.file(destination).hexdigest,
|
|
68
|
+
temporary: true
|
|
69
|
+
)
|
|
70
|
+
@lock.synchronize { @active_paths[destination] = true }
|
|
71
|
+
snapshot
|
|
72
|
+
# Remove a partially copied snapshot even when the caller is interrupted.
|
|
73
|
+
# rubocop:disable Lint/RescueException
|
|
74
|
+
rescue Exception
|
|
75
|
+
delete_file(destination) if destination
|
|
76
|
+
raise
|
|
77
|
+
# rubocop:enable Lint/RescueException
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def direct_manifest(source)
|
|
81
|
+
manifest_for(
|
|
82
|
+
source,
|
|
83
|
+
snapshot_id: "live-#{Process.pid}-#{SecureRandom.hex(8)}",
|
|
84
|
+
file_sha256: nil,
|
|
85
|
+
temporary: false
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def manifest_for(path, snapshot_id:, file_sha256:, temporary:)
|
|
90
|
+
{
|
|
91
|
+
format_version: FORMAT_VERSION,
|
|
92
|
+
snapshot_id: snapshot_id,
|
|
93
|
+
snapshot_path: File.expand_path(path),
|
|
94
|
+
page_size: @engine.storage_manager.page_size,
|
|
95
|
+
page_count: @engine.storage_manager.file_manager.num_pages,
|
|
96
|
+
file_sha256: file_sha256,
|
|
97
|
+
tables: table_manifest,
|
|
98
|
+
hidden_row_ids: @engine.accelerator_snapshot_hidden_row_ids,
|
|
99
|
+
temporary: temporary
|
|
100
|
+
}
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def direct_snapshot?
|
|
104
|
+
accelerator_config = @engine.config[:accelerator] || @engine.config["accelerator"] || {}
|
|
105
|
+
configured = accelerator_config[:direct_snapshot] || accelerator_config["direct_snapshot"]
|
|
106
|
+
configured = ENV.fetch("RUBYDB_ACCELERATOR_DIRECT_SNAPSHOT", "on") if configured.nil?
|
|
107
|
+
!%w[off false 0].include?(configured.to_s.downcase)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def table_manifest
|
|
111
|
+
@engine.table_metadata.each_with_object({}) do |(table_name, metadata), tables|
|
|
112
|
+
columns = Array(metadata[:columns]).map do |column|
|
|
113
|
+
{
|
|
114
|
+
name: column.name.to_s,
|
|
115
|
+
type: column.type_class.to_s,
|
|
116
|
+
nullable: column.nullable?,
|
|
117
|
+
default: json_value(column.default)
|
|
118
|
+
}
|
|
119
|
+
end
|
|
120
|
+
indexes = Array(@engine.index_manager&.get_indexes_for_table(table_name)).filter_map do |index|
|
|
121
|
+
next unless index.type.to_sym == :btree && index.respond_to?(:snapshot_entries)
|
|
122
|
+
|
|
123
|
+
{
|
|
124
|
+
name: index.name.to_s,
|
|
125
|
+
type: index.type.to_s,
|
|
126
|
+
columns: index.columns.map(&:to_s),
|
|
127
|
+
unique: index.unique,
|
|
128
|
+
entries: index.snapshot_entries.map do |entry|
|
|
129
|
+
{key: json_value(entry[:key]), row_id: entry[:value].to_i}
|
|
130
|
+
end
|
|
131
|
+
}
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
tables[table_name.to_s] = {
|
|
135
|
+
pages: Array(@engine.table_pages_for_snapshot(table_name)).map(&:to_i),
|
|
136
|
+
columns: columns,
|
|
137
|
+
indexes: indexes,
|
|
138
|
+
row_count: metadata[:row_count].to_i
|
|
139
|
+
}
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def json_value(value)
|
|
144
|
+
JSON.parse(JSON.generate(value))
|
|
145
|
+
rescue
|
|
146
|
+
nil
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def release(snapshot)
|
|
150
|
+
path = snapshot && snapshot[:snapshot_path]
|
|
151
|
+
return unless path && snapshot[:temporary]
|
|
152
|
+
|
|
153
|
+
@lock.synchronize do
|
|
154
|
+
@active_paths.delete(path)
|
|
155
|
+
delete_file(path)
|
|
156
|
+
Dir.rmdir(@directory) if Dir.exist?(@directory) && Dir.empty?(@directory)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def delete_file(path)
|
|
161
|
+
File.delete(path) if path && File.file?(path)
|
|
162
|
+
rescue SystemCallError
|
|
163
|
+
nil
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
data/lib/rubydb/version.rb
CHANGED
data/lib/rubydb/wal/archive.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require "fileutils"
|
|
4
4
|
require "zlib"
|
|
5
5
|
require "time"
|
|
6
|
+
require_relative "../accelerator"
|
|
6
7
|
|
|
7
8
|
module RubyDB
|
|
8
9
|
module WAL
|
|
@@ -14,6 +15,7 @@ module RubyDB
|
|
|
14
15
|
@wal_dir = wal_dir
|
|
15
16
|
@archive_dir = archive_dir || File.join(wal_dir, "archive")
|
|
16
17
|
@config = config
|
|
18
|
+
@accelerator = RubyDB::Accelerator::Client.new(config[:accelerator] || {})
|
|
17
19
|
@compression = config[:compression] || true
|
|
18
20
|
@max_archive_size = config[:max_size] || 10 * 1024 * 1024 * 1024 # 10GB
|
|
19
21
|
@retention_days = config[:retention_days] || 30
|
|
@@ -170,9 +172,19 @@ module RubyDB
|
|
|
170
172
|
end
|
|
171
173
|
end
|
|
172
174
|
|
|
175
|
+
def close
|
|
176
|
+
@accelerator&.close
|
|
177
|
+
true
|
|
178
|
+
end
|
|
179
|
+
|
|
173
180
|
private
|
|
174
181
|
|
|
175
182
|
def compress_file(source, destination)
|
|
183
|
+
if (compressed = @accelerator.gzip(File.binread(source), level: @config[:compression_level]))
|
|
184
|
+
File.binwrite(destination, compressed)
|
|
185
|
+
return
|
|
186
|
+
end
|
|
187
|
+
|
|
176
188
|
File.open(source, "rb") do |input|
|
|
177
189
|
Zlib::GzipWriter.open(destination) do |output|
|
|
178
190
|
output.write(input.read)
|
|
@@ -181,6 +193,11 @@ module RubyDB
|
|
|
181
193
|
end
|
|
182
194
|
|
|
183
195
|
def decompress_file(source, destination)
|
|
196
|
+
if (decompressed = @accelerator.gunzip(File.binread(source)))
|
|
197
|
+
File.binwrite(destination, decompressed)
|
|
198
|
+
return
|
|
199
|
+
end
|
|
200
|
+
|
|
184
201
|
Zlib::GzipReader.open(source) do |input|
|
|
185
202
|
File.binwrite(destination, input.read)
|
|
186
203
|
end
|
data/lib/rubydb/wal/wal.rb
CHANGED
data/rubydb.gemspec
CHANGED
|
@@ -42,8 +42,18 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
# Specify which files should be added to the gem when it is released.
|
|
43
43
|
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
|
44
44
|
spec.files = Dir.chdir(__dir__) do
|
|
45
|
-
`git ls-files -z`.split("\x0")
|
|
46
|
-
|
|
45
|
+
tracked_files = `git ls-files -z`.split("\x0")
|
|
46
|
+
accelerator_files = Dir[
|
|
47
|
+
"accelerator/bin/rubydb-accelerator-*",
|
|
48
|
+
"accelerator/bin/SHA256SUMS",
|
|
49
|
+
"accelerator/cmd/**/*.go",
|
|
50
|
+
"accelerator/internal/**/*.go",
|
|
51
|
+
"accelerator/go.mod"
|
|
52
|
+
]
|
|
53
|
+
runtime_files = Dir["lib/rubydb/accelerator.rb", "lib/rubydb/accelerator/**/*.rb", "lib/rubydb/cli/commands/accelerator.rb", "lib/rubydb/storage/snapshot_reader.rb"]
|
|
54
|
+
physical_execution_files = Dir["lib/rubydb/execution/physical_plan.rb", "lib/rubydb/execution/cost_model.rb", "lib/rubydb/execution/operator_selection.rb", "lib/rubydb/execution/accelerator_dispatch.rb"]
|
|
55
|
+
(tracked_files + accelerator_files + runtime_files + physical_execution_files).uniq.select { |f| File.file?(f) }.reject do |f|
|
|
56
|
+
f.match?(%r{^(test|spec|features|benchmarks|fuzz|chaos|examples|adapters/python)/}) || f.end_with?("_test.go")
|
|
47
57
|
end
|
|
48
58
|
end
|
|
49
59
|
spec.bindir = "exe"
|