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.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -0
  3. data/CHANGELOG.md +14 -0
  4. data/Gemfile.lock +1 -1
  5. data/README.md +296 -227
  6. data/Rakefile +6 -1
  7. data/accelerator/bin/SHA256SUMS +6 -0
  8. data/accelerator/bin/rubydb-accelerator-darwin-amd64 +0 -0
  9. data/accelerator/bin/rubydb-accelerator-darwin-arm64 +0 -0
  10. data/accelerator/bin/rubydb-accelerator-linux-amd64 +0 -0
  11. data/accelerator/bin/rubydb-accelerator-linux-arm64 +0 -0
  12. data/accelerator/bin/rubydb-accelerator-windows-amd64.exe +0 -0
  13. data/accelerator/bin/rubydb-accelerator-windows-arm64.exe +0 -0
  14. data/accelerator/cmd/rubydb-accelerator/main.go +11 -0
  15. data/accelerator/go.mod +3 -0
  16. data/accelerator/internal/execution/aggregate.go +94 -0
  17. data/accelerator/internal/execution/distinct.go +22 -0
  18. data/accelerator/internal/execution/filter.go +73 -0
  19. data/accelerator/internal/execution/join.go +79 -0
  20. data/accelerator/internal/execution/operators.go +167 -0
  21. data/accelerator/internal/execution/scan.go +20 -0
  22. data/accelerator/internal/execution/sort.go +62 -0
  23. data/accelerator/internal/execution/types.go +136 -0
  24. data/accelerator/internal/execution/value.go +67 -0
  25. data/accelerator/internal/memory/arena.go +47 -0
  26. data/accelerator/internal/memory/reuse.go +22 -0
  27. data/accelerator/internal/metrics/registry.go +67 -0
  28. data/accelerator/internal/parallel/bounded_queue.go +56 -0
  29. data/accelerator/internal/parallel/scheduler.go +47 -0
  30. data/accelerator/internal/parallel/worker_pool.go +53 -0
  31. data/accelerator/internal/protocol/cancellation.go +48 -0
  32. data/accelerator/internal/protocol/columnar.go +263 -0
  33. data/accelerator/internal/protocol/frame.go +187 -0
  34. data/accelerator/internal/runtime/worker.go +521 -0
  35. data/accelerator/internal/storage/page_reader.go +81 -0
  36. data/accelerator/internal/storage/snapshot_scan.go +539 -0
  37. data/accelerator/internal/wal/checksum.go +13 -0
  38. data/accelerator/internal/wal/compression.go +41 -0
  39. data/accelerator/internal/wal/group_commit.go +24 -0
  40. data/accelerator/internal/wal/record_encoder.go +40 -0
  41. data/adapters/activerecord/README.md +8 -3
  42. data/adapters/activerecord/lib/active_record/connection_adapters/rubydb_adapter.rb +50 -42
  43. data/adapters/activerecord/rubydb-activerecord.gemspec +1 -1
  44. data/docs/README.md +3 -1
  45. data/docs/architecture/go-accelerator.md +179 -0
  46. data/docs/cli.md +24 -0
  47. data/docs/contributing/benchmarking.md +16 -0
  48. data/docs/developer/local-development.md +32 -0
  49. data/docs/release.md +2 -2
  50. data/lessons/02-local-development.md +2 -2
  51. data/lessons/04-rails-complex-apps.md +2 -2
  52. data/lessons/05-rubydb-production-server.md +2 -2
  53. data/lessons/07-hybrid-microservices.md +175 -90
  54. data/lessons/10-release-readiness.md +184 -117
  55. data/lessons/11-community-adapter.md +323 -0
  56. data/lessons/12-rails-ecommerce-pressure.md +263 -0
  57. data/lib/rubydb/accelerator/client.rb +451 -0
  58. data/lib/rubydb/accelerator/error.rb +22 -0
  59. data/lib/rubydb/accelerator/manager.rb +606 -0
  60. data/lib/rubydb/accelerator.rb +13 -0
  61. data/lib/rubydb/cli/application.rb +6 -1
  62. data/lib/rubydb/cli/commands/accelerator.rb +72 -0
  63. data/lib/rubydb/cli/commands/doctor.rb +3 -0
  64. data/lib/rubydb/client/client.rb +7 -0
  65. data/lib/rubydb/client/connection.rb +15 -0
  66. data/lib/rubydb/client/result.rb +5 -1
  67. data/lib/rubydb/configuration/defaults.rb +12 -0
  68. data/lib/rubydb/configuration/validation.rb +8 -1
  69. data/lib/rubydb/execution/accelerator_dispatch.rb +30 -0
  70. data/lib/rubydb/execution/cost_model.rb +72 -0
  71. data/lib/rubydb/execution/executor.rb +373 -11
  72. data/lib/rubydb/execution/operator_selection.rb +57 -0
  73. data/lib/rubydb/execution/physical_plan.rb +47 -0
  74. data/lib/rubydb/execution/planner.rb +12 -46
  75. data/lib/rubydb/execution/sort_executor.rb +22 -8
  76. data/lib/rubydb/indexes/btree.rb +31 -2
  77. data/lib/rubydb/rubydb.rb +7 -1
  78. data/lib/rubydb/server/session.rb +45 -0
  79. data/lib/rubydb/storage/engine.rb +74 -12
  80. data/lib/rubydb/storage/snapshot_reader.rb +167 -0
  81. data/lib/rubydb/version.rb +1 -1
  82. data/lib/rubydb/wal/archive.rb +17 -0
  83. data/lib/rubydb/wal/wal.rb +1 -0
  84. data/rubydb.gemspec +12 -2
  85. data/scripts/build_accelerator +49 -0
  86. data/scripts/release +34 -4
  87. data/scripts/replication_failover_drill +2 -2
  88. metadata +49 -1
@@ -0,0 +1,187 @@
1
+ package protocol
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "encoding/json"
7
+ "errors"
8
+ "fmt"
9
+ "io"
10
+ )
11
+
12
+ const (
13
+ Version = 1
14
+ BinaryVersion = 1
15
+ MaxFrameSize = 16 * 1024 * 1024
16
+
17
+ BinaryRequestKind byte = 1
18
+ BinaryResponseKind byte = 2
19
+ BinaryRowsType byte = 1
20
+ BinaryJoinType byte = 2
21
+ BinarySnapshotScanType byte = 3
22
+ )
23
+
24
+ var BinaryMagic = []byte("RDBB")
25
+
26
+ var ErrFrameTooLarge = errors.New("frame too large")
27
+
28
+ type BinaryFrame struct {
29
+ Kind byte
30
+ TypeID byte
31
+ Status byte
32
+ ID string
33
+ Payload []byte
34
+ }
35
+
36
+ func ReadMessage(reader *bufio.Reader) (*BinaryFrame, []byte, error) {
37
+ prefix, err := reader.Peek(len(BinaryMagic))
38
+ if err != nil {
39
+ return nil, nil, err
40
+ }
41
+ if bytes.Equal(prefix, BinaryMagic) {
42
+ frame, err := ReadBinaryFrame(reader)
43
+ return frame, nil, err
44
+ }
45
+ frame, err := ReadFrame(reader)
46
+ return nil, frame, err
47
+ }
48
+
49
+ func ReadBinaryFrame(reader *bufio.Reader) (*BinaryFrame, error) {
50
+ header := make([]byte, 14)
51
+ if _, err := io.ReadFull(reader, header); err != nil {
52
+ return nil, err
53
+ }
54
+ if !bytes.Equal(header[:4], BinaryMagic) || header[4] != BinaryVersion {
55
+ return nil, errors.New("invalid binary frame")
56
+ }
57
+ idLength := int(binaryLittleEndian16(header[8:10]))
58
+ payloadLength := int(binaryLittleEndian32(header[10:14]))
59
+ if payloadLength > MaxFrameSize || idLength > 1024 || payloadLength+idLength > MaxFrameSize {
60
+ return nil, ErrFrameTooLarge
61
+ }
62
+ body := make([]byte, idLength+payloadLength)
63
+ if _, err := io.ReadFull(reader, body); err != nil {
64
+ return nil, err
65
+ }
66
+ return &BinaryFrame{
67
+ Kind: header[5],
68
+ TypeID: header[6],
69
+ Status: header[7],
70
+ ID: string(body[:idLength]),
71
+ Payload: body[idLength:],
72
+ }, nil
73
+ }
74
+
75
+ func WriteBinaryResponse(writer *bufio.Writer, requestFrame BinaryFrame, payload []byte, message, code string) error {
76
+ status := byte(0)
77
+ if message != "" {
78
+ status = 1
79
+ payload = MustJSON(map[string]string{"error": message, "code": code})
80
+ }
81
+ if len(requestFrame.ID) > 1024 || len(payload)+len(requestFrame.ID) > MaxFrameSize {
82
+ return ErrFrameTooLarge
83
+ }
84
+ header := make([]byte, 14)
85
+ copy(header[:4], BinaryMagic)
86
+ header[4] = BinaryVersion
87
+ header[5] = BinaryResponseKind
88
+ header[6] = requestFrame.TypeID
89
+ header[7] = status
90
+ putBinaryLittleEndian16(header[8:10], uint16(len(requestFrame.ID)))
91
+ putBinaryLittleEndian32(header[10:14], uint32(len(payload)))
92
+ if _, err := writer.Write(header); err != nil {
93
+ return err
94
+ }
95
+ if _, err := writer.WriteString(requestFrame.ID); err != nil {
96
+ return err
97
+ }
98
+ if _, err := writer.Write(payload); err != nil {
99
+ return err
100
+ }
101
+ return writer.Flush()
102
+ }
103
+
104
+ func ReadFrame(reader *bufio.Reader) ([]byte, error) {
105
+ frame := make([]byte, 0, 1024)
106
+ for {
107
+ chunk, err := reader.ReadSlice('\n')
108
+ if len(frame)+len(chunk) > MaxFrameSize {
109
+ return nil, ErrFrameTooLarge
110
+ }
111
+ frame = append(frame, chunk...)
112
+ if err == bufio.ErrBufferFull {
113
+ continue
114
+ }
115
+ if err != nil {
116
+ return nil, err
117
+ }
118
+ return frame, nil
119
+ }
120
+ }
121
+
122
+ func WriteJSONResponse(writer *bufio.Writer, value interface{}) error {
123
+ data, err := json.Marshal(value)
124
+ if err != nil {
125
+ return err
126
+ }
127
+ if len(data)+1 > MaxFrameSize {
128
+ return fmt.Errorf("response exceeds maximum frame size")
129
+ }
130
+ if _, err = writer.Write(append(data, '\n')); err != nil {
131
+ return err
132
+ }
133
+ return writer.Flush()
134
+ }
135
+
136
+ func MustJSON(value interface{}) []byte {
137
+ data, _ := json.Marshal(value)
138
+ return data
139
+ }
140
+
141
+ type Reader struct {
142
+ data []byte
143
+ offset int
144
+ }
145
+
146
+ func NewReader(data []byte) *Reader { return &Reader{data: data} }
147
+
148
+ func (reader *Reader) Remaining() int { return len(reader.data) - reader.offset }
149
+
150
+ func (reader *Reader) Bytes() []byte { return reader.data[reader.offset:] }
151
+
152
+ func (reader *Reader) Take(length int) ([]byte, error) {
153
+ if length < 0 || reader.Remaining() < length {
154
+ return nil, errors.New("binary value is truncated")
155
+ }
156
+ value := reader.data[reader.offset : reader.offset+length]
157
+ reader.offset += length
158
+ return value, nil
159
+ }
160
+
161
+ func (reader *Reader) Uint32() (uint32, error) {
162
+ value, err := reader.Take(4)
163
+ if err != nil {
164
+ return 0, err
165
+ }
166
+ return binaryLittleEndian32(value), nil
167
+ }
168
+
169
+ func binaryLittleEndian16(data []byte) uint16 {
170
+ return uint16(data[0]) | uint16(data[1])<<8
171
+ }
172
+
173
+ func binaryLittleEndian32(data []byte) uint32 {
174
+ return uint32(data[0]) | uint32(data[1])<<8 | uint32(data[2])<<16 | uint32(data[3])<<24
175
+ }
176
+
177
+ func putBinaryLittleEndian16(data []byte, value uint16) {
178
+ data[0] = byte(value)
179
+ data[1] = byte(value >> 8)
180
+ }
181
+
182
+ func putBinaryLittleEndian32(data []byte, value uint32) {
183
+ data[0] = byte(value)
184
+ data[1] = byte(value >> 8)
185
+ data[2] = byte(value >> 16)
186
+ data[3] = byte(value >> 24)
187
+ }
@@ -0,0 +1,521 @@
1
+ package runtime
2
+
3
+ import (
4
+ "bufio"
5
+ "bytes"
6
+ "compress/gzip"
7
+ "context"
8
+ "encoding/base64"
9
+ "encoding/json"
10
+ "errors"
11
+ "io"
12
+ "runtime"
13
+ "sync"
14
+ "time"
15
+
16
+ "github.com/aldanedev-create/rubydb/accelerator/internal/execution"
17
+ "github.com/aldanedev-create/rubydb/accelerator/internal/metrics"
18
+ "github.com/aldanedev-create/rubydb/accelerator/internal/protocol"
19
+ "github.com/aldanedev-create/rubydb/accelerator/internal/storage"
20
+ "github.com/aldanedev-create/rubydb/accelerator/internal/wal"
21
+ )
22
+
23
+ type request struct {
24
+ ID string `json:"id"`
25
+ Type string `json:"type"`
26
+ Payload json.RawMessage `json:"payload"`
27
+ }
28
+
29
+ type response struct {
30
+ ID string `json:"id"`
31
+ Type string `json:"type"`
32
+ Success bool `json:"success"`
33
+ More bool `json:"more,omitempty"`
34
+ Payload interface{} `json:"payload,omitempty"`
35
+ Error string `json:"error,omitempty"`
36
+ Code string `json:"code,omitempty"`
37
+ }
38
+
39
+ var operationMetrics = metrics.NewRegistry()
40
+
41
+ func Run(input io.Reader, output io.Writer) {
42
+ reader := bufio.NewReaderSize(input, 64*1024)
43
+ writer := bufio.NewWriterSize(output, 64*1024)
44
+ var writeMu sync.Mutex
45
+ registry := protocol.NewCancellationRegistry()
46
+ workerLimit := runtime.GOMAXPROCS(0) * 2
47
+ // Keep a small amount of parallelism available even on development
48
+ // machines where Go reports only one or two logical processors. The Ruby
49
+ // client multiplexes requests over one worker, so rejecting every request
50
+ // above GOMAXPROCS*2 would turn normal connection-pool bursts into false
51
+ // accelerator failures. The limit is still bounded and remains a safety
52
+ // valve for large requests.
53
+ if workerLimit < 8 {
54
+ workerLimit = 8
55
+ }
56
+ active := make(chan struct{}, workerLimit)
57
+ defer writer.Flush()
58
+ for {
59
+ binaryFrame, frame, err := protocol.ReadMessage(reader)
60
+ if err != nil {
61
+ if errors.Is(err, io.EOF) {
62
+ return
63
+ }
64
+ if errors.Is(err, protocol.ErrFrameTooLarge) {
65
+ _ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "request exceeds maximum frame size", Code: "frame_too_large"})
66
+ return
67
+ }
68
+ _ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "invalid request frame", Code: "invalid_frame"})
69
+ return
70
+ }
71
+ if binaryFrame != nil {
72
+ frame := *binaryFrame
73
+ select {
74
+ case active <- struct{}{}:
75
+ default:
76
+ writeMu.Lock()
77
+ _ = protocol.WriteBinaryResponse(writer, frame, nil, "accelerator worker capacity is exhausted", "resource_limit")
78
+ writeMu.Unlock()
79
+ continue
80
+ }
81
+ ctx := registry.Begin(context.Background(), frame.ID)
82
+ go func() {
83
+ defer func() { <-active }()
84
+ defer registry.Finish(frame.ID)
85
+ payload, message, code := handleBinaryContext(ctx, frame)
86
+ writeMu.Lock()
87
+ defer writeMu.Unlock()
88
+ _ = protocol.WriteBinaryResponse(writer, frame, payload, message, code)
89
+ }()
90
+ continue
91
+ }
92
+ var req request
93
+ decoder := json.NewDecoder(bytes.NewReader(bytes.TrimSpace(frame)))
94
+ decoder.UseNumber()
95
+ if err := decoder.Decode(&req); err != nil || req.ID == "" || req.Type == "" {
96
+ _ = protocol.WriteJSONResponse(writer, response{Success: false, Type: "error", Error: "invalid request frame", Code: "invalid_frame"})
97
+ return
98
+ }
99
+ if req.Type == "cancel" {
100
+ var payload struct {
101
+ TargetID string `json:"target_id"`
102
+ }
103
+ if err := json.Unmarshal(req.Payload, &payload); err != nil || payload.TargetID == "" {
104
+ writeMu.Lock()
105
+ _ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Success: false, Type: "error", Error: "invalid cancellation payload", Code: "invalid_payload"})
106
+ writeMu.Unlock()
107
+ continue
108
+ }
109
+ cancelled := registry.Cancel(payload.TargetID)
110
+ writeMu.Lock()
111
+ _ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Type: "cancel_response", Success: true, Payload: map[string]interface{}{"target_id": payload.TargetID, "cancelled": cancelled}})
112
+ writeMu.Unlock()
113
+ continue
114
+ }
115
+ if req.Type == "terminate" {
116
+ res := handleContext(context.Background(), req)
117
+ res.ID = req.ID
118
+ writeMu.Lock()
119
+ err := protocol.WriteJSONResponse(writer, res)
120
+ writeMu.Unlock()
121
+ if err != nil {
122
+ return
123
+ }
124
+ return
125
+ }
126
+ select {
127
+ case active <- struct{}{}:
128
+ default:
129
+ writeMu.Lock()
130
+ _ = protocol.WriteJSONResponse(writer, response{ID: req.ID, Type: "error", Success: false, Error: "accelerator worker capacity is exhausted", Code: "resource_limit"})
131
+ writeMu.Unlock()
132
+ continue
133
+ }
134
+ ctx := registry.Begin(context.Background(), req.ID)
135
+ go func(req request, ctx context.Context) {
136
+ defer func() { <-active }()
137
+ defer registry.Finish(req.ID)
138
+ started := time.Now()
139
+ if req.Type == "rows_pipeline_stream" {
140
+ streamRowsPipeline(ctx, req, writer, &writeMu)
141
+ operationMetrics.Observe(req.Type, time.Since(started))
142
+ return
143
+ }
144
+ res := handleContext(ctx, req)
145
+ operationMetrics.Observe(req.Type, time.Since(started))
146
+ res.ID = req.ID
147
+ writeMu.Lock()
148
+ defer writeMu.Unlock()
149
+ _ = protocol.WriteJSONResponse(writer, res)
150
+ }(req, ctx)
151
+ }
152
+ }
153
+
154
+ func handle(req request) response {
155
+ return handleContext(context.Background(), req)
156
+ }
157
+
158
+ func handleContext(ctx context.Context, req request) response {
159
+ select {
160
+ case <-ctx.Done():
161
+ return failure(ctx.Err().Error(), "cancelled")
162
+ default:
163
+ }
164
+ switch req.Type {
165
+ case "handshake":
166
+ return response{Success: true, Type: "handshake_response", Payload: map[string]interface{}{
167
+ "protocol_version": protocolVersion,
168
+ "accelerator_version": "0.1.0",
169
+ "runtime": "go",
170
+ "capabilities": capabilities(),
171
+ }}
172
+ case "ping":
173
+ return response{Success: true, Type: "pong", Payload: map[string]interface{}{
174
+ "protocol_version": protocolVersion,
175
+ "capabilities": capabilities(),
176
+ }}
177
+ case "stats":
178
+ return response{Success: true, Type: "stats_response", Payload: operationMetrics.Snapshot()}
179
+ case "sha256":
180
+ return handleSHA256(req.Payload)
181
+ case "gzip":
182
+ return handleGzip(req.Payload, true)
183
+ case "gunzip":
184
+ return handleGzip(req.Payload, false)
185
+ case "rows_pipeline":
186
+ return handleRowsPipelineContext(ctx, req.Payload)
187
+ case "wal_batch":
188
+ return handleWALBatch(req.Payload)
189
+ case "terminate":
190
+ return response{Success: true, Type: "terminate_response"}
191
+ default:
192
+ return failure("unsupported accelerator operation", "unsupported_operation")
193
+ }
194
+ }
195
+
196
+ func capabilities() []string {
197
+ return []string{"sha256", "gzip", "wal_batch", "rows_pipeline", "rows_pipeline_stream", "rows_pipeline_binary", "hash_join", "merge_join", "snapshot_scan", "snapshot_index_scan", "columnar_batches", "projection", "distinct", "window_functions", "cancellation", "multiplexed_requests", "bounded_concurrency", "resource_limits", "metrics"}
198
+ }
199
+
200
+ func handleBinary(frame protocol.BinaryFrame) ([]byte, string, string) {
201
+ return handleBinaryContext(context.Background(), frame)
202
+ }
203
+
204
+ func handleBinaryContext(ctx context.Context, frame protocol.BinaryFrame) ([]byte, string, string) {
205
+ if frame.Kind != protocol.BinaryRequestKind || (frame.TypeID != protocol.BinaryRowsType && frame.TypeID != protocol.BinaryJoinType && frame.TypeID != protocol.BinarySnapshotScanType) {
206
+ return nil, "unsupported binary accelerator operation", "unsupported_operation"
207
+ }
208
+ if frame.TypeID == protocol.BinarySnapshotScanType {
209
+ var request storage.SnapshotScanRequest
210
+ decoder := json.NewDecoder(bytes.NewReader(frame.Payload))
211
+ decoder.UseNumber()
212
+ if err := decoder.Decode(&request); err != nil {
213
+ return nil, "snapshot scan request is invalid", "invalid_payload"
214
+ }
215
+ result, err := storage.ExecuteSnapshotScanContext(ctx, request)
216
+ if err != nil {
217
+ return nil, err.Error(), "snapshot_scan_failed"
218
+ }
219
+ return encodeBinaryResultWithTypes(result.Rows, nil, result.ColumnTypes)
220
+ }
221
+ if frame.TypeID == protocol.BinaryJoinType {
222
+ return handleBinaryJoin(frame.Payload)
223
+ }
224
+ payload, err := decodeBinaryRowsRequest(frame.Payload)
225
+ if err != nil {
226
+ return nil, err.Error(), "invalid_payload"
227
+ }
228
+ result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
229
+ if err != nil {
230
+ return nil, err.Error(), "execution_error"
231
+ }
232
+ return encodeBinaryResult(result.Rows, result.Aggregates)
233
+ }
234
+
235
+ func encodeBinaryResult(rows, aggregates []map[string]interface{}) ([]byte, string, string) {
236
+ return encodeBinaryResultWithTypes(rows, aggregates, nil)
237
+ }
238
+
239
+ func encodeBinaryResultWithTypes(rows, aggregates []map[string]interface{}, columnTypes map[string]string) ([]byte, string, string) {
240
+ rowBatch, err := protocol.EncodeRows(rows)
241
+ if err != nil {
242
+ return nil, err.Error(), "encoding_error"
243
+ }
244
+ aggregateBatch, err := protocol.EncodeRows(aggregates)
245
+ if err != nil {
246
+ return nil, err.Error(), "encoding_error"
247
+ }
248
+ metadata, _ := json.Marshal(map[string]interface{}{"row_count": len(rows), "has_aggregates": len(aggregates) > 0, "row_batch_bytes": len(rowBatch), "aggregate_batch_bytes": len(aggregateBatch), "column_types": columnTypes})
249
+ var payload bytes.Buffer
250
+ writeUint32(&payload, uint32(len(metadata)))
251
+ payload.Write(metadata)
252
+ writeUint32(&payload, uint32(len(rowBatch)))
253
+ payload.Write(rowBatch)
254
+ writeUint32(&payload, uint32(len(aggregateBatch)))
255
+ payload.Write(aggregateBatch)
256
+ return payload.Bytes(), "", ""
257
+ }
258
+
259
+ func decodeBinaryRowsRequest(payload []byte) (execution.RowsRequest, error) {
260
+ reader := protocol.NewReader(payload)
261
+ metadataLength, err := reader.Uint32()
262
+ if err != nil {
263
+ return execution.RowsRequest{}, errors.New("binary rows request is truncated")
264
+ }
265
+ metadata, err := reader.Take(int(metadataLength))
266
+ if err != nil {
267
+ return execution.RowsRequest{}, errors.New("binary rows request metadata is invalid")
268
+ }
269
+ var request execution.RowsRequest
270
+ decoder := json.NewDecoder(bytes.NewReader(metadata))
271
+ decoder.UseNumber()
272
+ if err := decoder.Decode(&request); err != nil {
273
+ return execution.RowsRequest{}, errors.New("binary rows request metadata is invalid")
274
+ }
275
+ rows, err := protocol.DecodeRows(reader.Bytes())
276
+ if err != nil {
277
+ return execution.RowsRequest{}, err
278
+ }
279
+ request.Rows = rows
280
+ return request, nil
281
+ }
282
+
283
+ func handleBinaryJoin(payload []byte) ([]byte, string, string) {
284
+ reader := protocol.NewReader(payload)
285
+ metadataLength, err := reader.Uint32()
286
+ if err != nil {
287
+ return nil, "binary join request is truncated", "invalid_payload"
288
+ }
289
+ metadata, err := reader.Take(int(metadataLength))
290
+ if err != nil {
291
+ return nil, "binary join metadata is invalid", "invalid_payload"
292
+ }
293
+ var specification struct {
294
+ LeftKey string `json:"left_key"`
295
+ RightKey string `json:"right_key"`
296
+ JoinType string `json:"join_type"`
297
+ Algorithm string `json:"algorithm"`
298
+ }
299
+ if err := json.Unmarshal(metadata, &specification); err != nil || specification.LeftKey == "" || specification.RightKey == "" {
300
+ return nil, "binary join metadata is invalid", "invalid_payload"
301
+ }
302
+ leftLength, err := reader.Uint32()
303
+ if err != nil {
304
+ return nil, "binary join request is truncated", "invalid_payload"
305
+ }
306
+ left, err := reader.Take(int(leftLength))
307
+ if err != nil {
308
+ return nil, "binary join left batch is invalid", "invalid_payload"
309
+ }
310
+ rightLength, err := reader.Uint32()
311
+ if err != nil {
312
+ return nil, "binary join request is truncated", "invalid_payload"
313
+ }
314
+ right, err := reader.Take(int(rightLength))
315
+ if err != nil || reader.Remaining() != 0 {
316
+ return nil, "binary join right batch is invalid", "invalid_payload"
317
+ }
318
+ if specification.JoinType != "" && !equalFold(specification.JoinType, "inner") {
319
+ return nil, "only inner joins are supported by the accelerator", "unsupported_operation"
320
+ }
321
+ leftRows, err := protocol.DecodeRows(left)
322
+ if err != nil {
323
+ return nil, err.Error(), "invalid_payload"
324
+ }
325
+ rightRows, err := protocol.DecodeRows(right)
326
+ if err != nil {
327
+ return nil, err.Error(), "invalid_payload"
328
+ }
329
+ joined := execution.HashJoinRows(leftRows, rightRows, specification.LeftKey, specification.RightKey)
330
+ if equalFold(specification.Algorithm, "merge") {
331
+ joined = execution.MergeJoinRows(leftRows, rightRows, specification.LeftKey, specification.RightKey)
332
+ }
333
+ return encodeBinaryResult(joined, nil)
334
+ }
335
+
336
+ func handleSHA256(raw json.RawMessage) response {
337
+ var payload struct {
338
+ DataBase64 string `json:"data_base64"`
339
+ }
340
+ if err := json.Unmarshal(raw, &payload); err != nil {
341
+ return failure("invalid sha256 payload", "invalid_payload")
342
+ }
343
+ data, err := base64.StdEncoding.DecodeString(payload.DataBase64)
344
+ if err != nil {
345
+ return failure("data_base64 is invalid", "invalid_payload")
346
+ }
347
+ return response{Success: true, Type: "sha256_response", Payload: map[string]string{"digest": wal.SHA256Hex(data)}}
348
+ }
349
+
350
+ func handleGzip(raw json.RawMessage, compress bool) response {
351
+ var payload struct {
352
+ DataBase64 string `json:"data_base64"`
353
+ Level int `json:"level,omitempty"`
354
+ }
355
+ if err := json.Unmarshal(raw, &payload); err != nil {
356
+ return failure("invalid compression payload", "invalid_payload")
357
+ }
358
+ data, err := base64.StdEncoding.DecodeString(payload.DataBase64)
359
+ if err != nil {
360
+ return failure("data_base64 is invalid", "invalid_payload")
361
+ }
362
+ if compress {
363
+ level := payload.Level
364
+ if level == 0 {
365
+ level = gzip.DefaultCompression
366
+ }
367
+ compressed, err := wal.Gzip(data, level)
368
+ if err != nil {
369
+ return failure("gzip operation failed", "compression_error")
370
+ }
371
+ return response{Success: true, Type: "gzip_response", Payload: map[string]interface{}{"data_base64": base64.StdEncoding.EncodeToString(compressed), "input_bytes": len(data), "output_bytes": len(compressed)}}
372
+ }
373
+ output, err := wal.Gunzip(data, protocol.MaxFrameSize)
374
+ if err != nil {
375
+ return failure("gunzip operation failed", "compression_error")
376
+ }
377
+ return response{Success: true, Type: "gunzip_response", Payload: map[string]interface{}{"data_base64": base64.StdEncoding.EncodeToString(output), "output_bytes": len(output)}}
378
+ }
379
+
380
+ func handleRowsPipeline(raw json.RawMessage) response {
381
+ return handleRowsPipelineContext(context.Background(), raw)
382
+ }
383
+
384
+ func handleRowsPipelineContext(ctx context.Context, raw json.RawMessage) response {
385
+ var payload execution.RowsRequest
386
+ decoder := json.NewDecoder(bytes.NewReader(raw))
387
+ decoder.UseNumber()
388
+ if err := decoder.Decode(&payload); err != nil {
389
+ return failure("invalid rows pipeline payload", "invalid_payload")
390
+ }
391
+ result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
392
+ if err != nil {
393
+ code := "execution_error"
394
+ if errors.Is(err, context.Canceled) {
395
+ code = "cancelled"
396
+ }
397
+ return failure(err.Error(), code)
398
+ }
399
+ return response{Success: true, Type: "rows_pipeline_response", Payload: result.AsMap()}
400
+ }
401
+
402
+ func streamRowsPipeline(ctx context.Context, req request, writer *bufio.Writer, writeMu *sync.Mutex) {
403
+ var payload execution.RowsRequest
404
+ decoder := json.NewDecoder(bytes.NewReader(req.Payload))
405
+ decoder.UseNumber()
406
+ if err := decoder.Decode(&payload); err != nil {
407
+ writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: "invalid rows pipeline payload", Code: "invalid_payload"})
408
+ return
409
+ }
410
+ result, err := execution.ExecuteRowsPipelineContext(ctx, payload)
411
+ if err != nil {
412
+ code := "execution_error"
413
+ if errors.Is(err, context.Canceled) {
414
+ code = "cancelled"
415
+ }
416
+ writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: err.Error(), Code: code})
417
+ return
418
+ }
419
+ batchSize := payload.BatchSize
420
+ if batchSize < 1 {
421
+ batchSize = 1024
422
+ }
423
+ if len(result.Rows) == 0 {
424
+ writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: true, Payload: map[string]interface{}{"rows": []map[string]interface{}{}, "row_count": 0}, More: false})
425
+ return
426
+ }
427
+ for start := 0; start < len(result.Rows); start += batchSize {
428
+ select {
429
+ case <-ctx.Done():
430
+ writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: false, Error: ctx.Err().Error(), Code: "cancelled"})
431
+ return
432
+ default:
433
+ }
434
+ end := start + batchSize
435
+ if end > len(result.Rows) {
436
+ end = len(result.Rows)
437
+ }
438
+ payload := map[string]interface{}{"rows": result.Rows[start:end], "row_count": end - start}
439
+ if end == len(result.Rows) && len(result.Aggregates) > 0 {
440
+ payload["aggregates"] = result.Aggregates
441
+ }
442
+ writeStreamResponse(writer, writeMu, response{ID: req.ID, Type: "rows_pipeline_batch", Success: true, Payload: payload, More: end < len(result.Rows)})
443
+ }
444
+ }
445
+
446
+ func writeStreamResponse(writer *bufio.Writer, writeMu *sync.Mutex, result response) {
447
+ writeMu.Lock()
448
+ defer writeMu.Unlock()
449
+ _ = protocol.WriteJSONResponse(writer, result)
450
+ }
451
+
452
+ func handleWALBatch(raw json.RawMessage) response {
453
+ var payload struct {
454
+ Records []struct {
455
+ Kind uint8 `json:"kind"`
456
+ LSN uint64 `json:"lsn"`
457
+ Transaction uint64 `json:"transaction"`
458
+ PayloadBase64 string `json:"payload_base64"`
459
+ } `json:"records"`
460
+ Compress bool `json:"compress,omitempty"`
461
+ }
462
+ if err := json.Unmarshal(raw, &payload); err != nil || len(payload.Records) == 0 {
463
+ return failure("invalid WAL batch payload", "invalid_payload")
464
+ }
465
+ encoded := make([][]byte, 0, len(payload.Records))
466
+ for _, record := range payload.Records {
467
+ data, err := base64.StdEncoding.DecodeString(record.PayloadBase64)
468
+ if err != nil {
469
+ return failure("WAL payload is not valid base64", "invalid_payload")
470
+ }
471
+ encoded = append(encoded, wal.EncodeRecord(record.Kind, record.LSN, record.Transaction, data))
472
+ }
473
+ joined := bytes.Join(encoded, nil)
474
+ result := map[string]interface{}{
475
+ "record_count": len(encoded),
476
+ "bytes": len(joined),
477
+ "checksum": wal.SHA256Hex(joined),
478
+ "records_base64": base64.StdEncoding.EncodeToString(joined),
479
+ }
480
+ if payload.Compress {
481
+ compressed, err := wal.Gzip(joined, gzip.DefaultCompression)
482
+ if err != nil {
483
+ return failure("WAL compression failed", "compression_error")
484
+ }
485
+ result["compressed_base64"] = base64.StdEncoding.EncodeToString(compressed)
486
+ result["compressed_bytes"] = len(compressed)
487
+ }
488
+ return response{Success: true, Type: "wal_batch_response", Payload: result}
489
+ }
490
+
491
+ func writeUint32(output *bytes.Buffer, value uint32) {
492
+ output.WriteByte(byte(value))
493
+ output.WriteByte(byte(value >> 8))
494
+ output.WriteByte(byte(value >> 16))
495
+ output.WriteByte(byte(value >> 24))
496
+ }
497
+
498
+ func equalFold(left, right string) bool {
499
+ if len(left) != len(right) {
500
+ return false
501
+ }
502
+ for index := range left {
503
+ l, r := left[index], right[index]
504
+ if l >= 'A' && l <= 'Z' {
505
+ l += 'a' - 'A'
506
+ }
507
+ if r >= 'A' && r <= 'Z' {
508
+ r += 'a' - 'A'
509
+ }
510
+ if l != r {
511
+ return false
512
+ }
513
+ }
514
+ return true
515
+ }
516
+
517
+ func failure(message, code string) response {
518
+ return response{Success: false, Type: "error", Error: message, Code: code}
519
+ }
520
+
521
+ const protocolVersion = protocol.Version