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
data/accelerator/go.mod
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"strings"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func AggregateRows(rows []map[string]interface{}, groupBy []string, definitions []Aggregate) []map[string]interface{} {
|
|
9
|
+
groups := map[string][]map[string]interface{}{}
|
|
10
|
+
keys := map[string][]interface{}{}
|
|
11
|
+
order := make([]string, 0)
|
|
12
|
+
if len(groupBy) == 0 {
|
|
13
|
+
groups["_"] = rows
|
|
14
|
+
order = append(order, "_")
|
|
15
|
+
} else {
|
|
16
|
+
for _, row := range rows {
|
|
17
|
+
values := make([]interface{}, len(groupBy))
|
|
18
|
+
for i, column := range groupBy {
|
|
19
|
+
values[i] = row[column]
|
|
20
|
+
}
|
|
21
|
+
keyBytes, _ := json.Marshal(values)
|
|
22
|
+
key := string(keyBytes)
|
|
23
|
+
if _, exists := groups[key]; !exists {
|
|
24
|
+
order = append(order, key)
|
|
25
|
+
}
|
|
26
|
+
groups[key] = append(groups[key], row)
|
|
27
|
+
keys[key] = values
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
result := make([]map[string]interface{}, 0, len(groups))
|
|
31
|
+
for _, key := range order {
|
|
32
|
+
group := groups[key]
|
|
33
|
+
row := map[string]interface{}{}
|
|
34
|
+
for i, column := range groupBy {
|
|
35
|
+
row[column] = keys[key][i]
|
|
36
|
+
}
|
|
37
|
+
for _, definition := range definitions {
|
|
38
|
+
name := definition.Alias
|
|
39
|
+
if name == "" {
|
|
40
|
+
name = strings.ToLower(definition.Function) + "(" + definition.Column + ")"
|
|
41
|
+
}
|
|
42
|
+
row[name] = AggregateValue(group, definition)
|
|
43
|
+
}
|
|
44
|
+
result = append(result, row)
|
|
45
|
+
}
|
|
46
|
+
return result
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
func AggregateValue(rows []map[string]interface{}, definition Aggregate) interface{} {
|
|
50
|
+
values := make([]interface{}, 0, len(rows))
|
|
51
|
+
for _, row := range rows {
|
|
52
|
+
value := row[definition.Column]
|
|
53
|
+
if value != nil {
|
|
54
|
+
values = append(values, value)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
switch strings.ToUpper(definition.Function) {
|
|
58
|
+
case "COUNT":
|
|
59
|
+
if definition.Column == "*" {
|
|
60
|
+
return len(rows)
|
|
61
|
+
}
|
|
62
|
+
return len(values)
|
|
63
|
+
case "SUM", "AVG":
|
|
64
|
+
var sum float64
|
|
65
|
+
for _, value := range values {
|
|
66
|
+
number, ok := NumberValue(value)
|
|
67
|
+
if !ok {
|
|
68
|
+
return nil
|
|
69
|
+
}
|
|
70
|
+
sum += number
|
|
71
|
+
}
|
|
72
|
+
if strings.EqualFold(definition.Function, "AVG") {
|
|
73
|
+
if len(values) == 0 {
|
|
74
|
+
return nil
|
|
75
|
+
}
|
|
76
|
+
return sum / float64(len(values))
|
|
77
|
+
}
|
|
78
|
+
return sum
|
|
79
|
+
case "MIN", "MAX":
|
|
80
|
+
if len(values) == 0 {
|
|
81
|
+
return nil
|
|
82
|
+
}
|
|
83
|
+
best := values[0]
|
|
84
|
+
for _, value := range values[1:] {
|
|
85
|
+
comparison := Compare(value, best)
|
|
86
|
+
if (strings.EqualFold(definition.Function, "MIN") && comparison < 0) || (strings.EqualFold(definition.Function, "MAX") && comparison > 0) {
|
|
87
|
+
best = value
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return best
|
|
91
|
+
default:
|
|
92
|
+
return nil
|
|
93
|
+
}
|
|
94
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import "encoding/json"
|
|
4
|
+
|
|
5
|
+
func DistinctRows(rows []map[string]interface{}, columns []string) []map[string]interface{} {
|
|
6
|
+
seen := make(map[string]struct{}, len(rows))
|
|
7
|
+
result := make([]map[string]interface{}, 0, len(rows))
|
|
8
|
+
for _, row := range rows {
|
|
9
|
+
values := make([]interface{}, len(columns))
|
|
10
|
+
for index, column := range columns {
|
|
11
|
+
values[index] = row[column]
|
|
12
|
+
}
|
|
13
|
+
keyBytes, _ := json.Marshal(values)
|
|
14
|
+
key := string(keyBytes)
|
|
15
|
+
if _, exists := seen[key]; exists {
|
|
16
|
+
continue
|
|
17
|
+
}
|
|
18
|
+
seen[key] = struct{}{}
|
|
19
|
+
result = append(result, row)
|
|
20
|
+
}
|
|
21
|
+
return result
|
|
22
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"fmt"
|
|
5
|
+
"regexp"
|
|
6
|
+
"strings"
|
|
7
|
+
|
|
8
|
+
"github.com/aldanedev-create/rubydb/accelerator/internal/parallel"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
func FilterRows(rows []map[string]interface{}, condition Filter) []map[string]interface{} {
|
|
12
|
+
if len(rows) < 4096 {
|
|
13
|
+
filtered := make([]map[string]interface{}, 0, len(rows))
|
|
14
|
+
for _, row := range rows {
|
|
15
|
+
if Matches(row[condition.Column], condition.Operator, condition.Value) {
|
|
16
|
+
filtered = append(filtered, row)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return filtered
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
chunks := make([][]map[string]interface{}, parallel.ChunkCount(len(rows)))
|
|
23
|
+
parallel.ForEachChunk(len(rows), func(index, start, end int) {
|
|
24
|
+
filtered := make([]map[string]interface{}, 0, end-start)
|
|
25
|
+
for _, row := range rows[start:end] {
|
|
26
|
+
if Matches(row[condition.Column], condition.Operator, condition.Value) {
|
|
27
|
+
filtered = append(filtered, row)
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
chunks[index] = filtered
|
|
31
|
+
})
|
|
32
|
+
filtered := make([]map[string]interface{}, 0, len(rows))
|
|
33
|
+
for _, chunk := range chunks {
|
|
34
|
+
filtered = append(filtered, chunk...)
|
|
35
|
+
}
|
|
36
|
+
return filtered
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
func Matches(actual interface{}, operator string, expected interface{}) bool {
|
|
40
|
+
if strings.EqualFold(operator, "is_null") {
|
|
41
|
+
return actual == nil
|
|
42
|
+
}
|
|
43
|
+
if strings.EqualFold(operator, "is_not_null") {
|
|
44
|
+
return actual != nil
|
|
45
|
+
}
|
|
46
|
+
if actual == nil || expected == nil {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
if strings.EqualFold(operator, "like") {
|
|
50
|
+
pattern := regexp.QuoteMeta(fmt.Sprint(expected))
|
|
51
|
+
pattern = strings.ReplaceAll(pattern, "%", ".*")
|
|
52
|
+
pattern = strings.ReplaceAll(pattern, "_", ".")
|
|
53
|
+
matched, _ := regexp.MatchString("(?i)^"+pattern+"$", fmt.Sprint(actual))
|
|
54
|
+
return matched
|
|
55
|
+
}
|
|
56
|
+
comparison := Compare(actual, expected)
|
|
57
|
+
switch strings.ToLower(operator) {
|
|
58
|
+
case "eq":
|
|
59
|
+
return comparison == 0
|
|
60
|
+
case "ne":
|
|
61
|
+
return comparison != 0
|
|
62
|
+
case "lt":
|
|
63
|
+
return comparison < 0
|
|
64
|
+
case "lte":
|
|
65
|
+
return comparison <= 0
|
|
66
|
+
case "gt":
|
|
67
|
+
return comparison > 0
|
|
68
|
+
case "gte":
|
|
69
|
+
return comparison >= 0
|
|
70
|
+
default:
|
|
71
|
+
return false
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"sort"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
func HashJoinRows(leftRows, rightRows []map[string]interface{}, leftKey, rightKey string) []map[string]interface{} {
|
|
9
|
+
index := make(map[string][]map[string]interface{}, len(rightRows))
|
|
10
|
+
for _, row := range rightRows {
|
|
11
|
+
key := JoinKey(row[rightKey])
|
|
12
|
+
index[key] = append(index[key], row)
|
|
13
|
+
}
|
|
14
|
+
rows := make([]map[string]interface{}, 0)
|
|
15
|
+
for _, left := range leftRows {
|
|
16
|
+
for _, right := range index[JoinKey(left[leftKey])] {
|
|
17
|
+
merged := make(map[string]interface{}, len(left)+len(right))
|
|
18
|
+
for key, value := range right {
|
|
19
|
+
merged[key] = value
|
|
20
|
+
}
|
|
21
|
+
for key, value := range left {
|
|
22
|
+
merged[key] = value
|
|
23
|
+
}
|
|
24
|
+
rows = append(rows, merged)
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return rows
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MergeJoinRows is used when both inputs are already ordered by their join
|
|
31
|
+
// keys. It avoids allocating a hash table for large, sorted relations.
|
|
32
|
+
func MergeJoinRows(leftRows, rightRows []map[string]interface{}, leftKey, rightKey string) []map[string]interface{} {
|
|
33
|
+
left := append([]map[string]interface{}(nil), leftRows...)
|
|
34
|
+
right := append([]map[string]interface{}(nil), rightRows...)
|
|
35
|
+
sort.SliceStable(left, func(i, j int) bool { return Compare(left[i][leftKey], left[j][leftKey]) < 0 })
|
|
36
|
+
sort.SliceStable(right, func(i, j int) bool { return Compare(right[i][rightKey], right[j][rightKey]) < 0 })
|
|
37
|
+
result := make([]map[string]interface{}, 0)
|
|
38
|
+
for i, j := 0, 0; i < len(left) && j < len(right); {
|
|
39
|
+
comparison := Compare(left[i][leftKey], right[j][rightKey])
|
|
40
|
+
if comparison < 0 {
|
|
41
|
+
i++
|
|
42
|
+
continue
|
|
43
|
+
}
|
|
44
|
+
if comparison > 0 {
|
|
45
|
+
j++
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
leftEnd, rightEnd := i, j
|
|
49
|
+
for leftEnd < len(left) && Compare(left[leftEnd][leftKey], left[i][leftKey]) == 0 {
|
|
50
|
+
leftEnd++
|
|
51
|
+
}
|
|
52
|
+
for rightEnd < len(right) && Compare(right[rightEnd][rightKey], right[j][rightKey]) == 0 {
|
|
53
|
+
rightEnd++
|
|
54
|
+
}
|
|
55
|
+
for leftIndex := i; leftIndex < leftEnd; leftIndex++ {
|
|
56
|
+
for rightIndex := j; rightIndex < rightEnd; rightIndex++ {
|
|
57
|
+
result = append(result, mergeRows(left[leftIndex], right[rightIndex]))
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
i, j = leftEnd, rightEnd
|
|
61
|
+
}
|
|
62
|
+
return result
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
func mergeRows(left, right map[string]interface{}) map[string]interface{} {
|
|
66
|
+
merged := make(map[string]interface{}, len(left)+len(right))
|
|
67
|
+
for key, value := range right {
|
|
68
|
+
merged[key] = value
|
|
69
|
+
}
|
|
70
|
+
for key, value := range left {
|
|
71
|
+
merged[key] = value
|
|
72
|
+
}
|
|
73
|
+
return merged
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
func JoinKey(value interface{}) string {
|
|
77
|
+
encoded, _ := json.Marshal(value)
|
|
78
|
+
return string(encoded)
|
|
79
|
+
}
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"fmt"
|
|
6
|
+
"sort"
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
func ProjectRows(rows []map[string]interface{}, columns []string) []map[string]interface{} {
|
|
10
|
+
result := make([]map[string]interface{}, 0, len(rows))
|
|
11
|
+
for _, row := range rows {
|
|
12
|
+
projected := make(map[string]interface{}, len(columns))
|
|
13
|
+
for _, column := range columns {
|
|
14
|
+
projected[column] = row[column]
|
|
15
|
+
}
|
|
16
|
+
result = append(result, projected)
|
|
17
|
+
}
|
|
18
|
+
return result
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func SortRowsContext(ctx context.Context, rows []map[string]interface{}, orderBy []Order) error {
|
|
22
|
+
cancelled := false
|
|
23
|
+
sort.SliceStable(rows, func(left, right int) bool {
|
|
24
|
+
select {
|
|
25
|
+
case <-ctx.Done():
|
|
26
|
+
cancelled = true
|
|
27
|
+
return false
|
|
28
|
+
default:
|
|
29
|
+
}
|
|
30
|
+
for _, item := range orderBy {
|
|
31
|
+
comparison := Compare(rows[left][item.Column], rows[right][item.Column])
|
|
32
|
+
if comparison == 0 {
|
|
33
|
+
continue
|
|
34
|
+
}
|
|
35
|
+
if equalFold(item.Direction, "desc") {
|
|
36
|
+
return comparison > 0
|
|
37
|
+
}
|
|
38
|
+
return comparison < 0
|
|
39
|
+
}
|
|
40
|
+
return false
|
|
41
|
+
})
|
|
42
|
+
if cancelled {
|
|
43
|
+
return ctx.Err()
|
|
44
|
+
}
|
|
45
|
+
return nil
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
func ApplyWindowsContext(ctx context.Context, rows []map[string]interface{}, windows []Window) ([]map[string]interface{}, error) {
|
|
49
|
+
for _, definition := range windows {
|
|
50
|
+
if err := applyWindowContext(ctx, rows, definition); err != nil {
|
|
51
|
+
return nil, err
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return rows, nil
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func applyWindowContext(ctx context.Context, rows []map[string]interface{}, definition Window) error {
|
|
58
|
+
partitions := map[string][]map[string]interface{}{}
|
|
59
|
+
for _, row := range rows {
|
|
60
|
+
select {
|
|
61
|
+
case <-ctx.Done():
|
|
62
|
+
return ctx.Err()
|
|
63
|
+
default:
|
|
64
|
+
}
|
|
65
|
+
values := make([]interface{}, len(definition.PartitionBy))
|
|
66
|
+
for index, column := range definition.PartitionBy {
|
|
67
|
+
values[index] = row[column]
|
|
68
|
+
}
|
|
69
|
+
key := JoinKey(values)
|
|
70
|
+
partitions[key] = append(partitions[key], row)
|
|
71
|
+
}
|
|
72
|
+
for _, partition := range partitions {
|
|
73
|
+
if err := SortRowsContext(ctx, partition, definition.OrderBy); err != nil {
|
|
74
|
+
return err
|
|
75
|
+
}
|
|
76
|
+
for index, row := range partition {
|
|
77
|
+
select {
|
|
78
|
+
case <-ctx.Done():
|
|
79
|
+
return ctx.Err()
|
|
80
|
+
default:
|
|
81
|
+
}
|
|
82
|
+
value, err := windowValue(definition, partition, index)
|
|
83
|
+
if err != nil {
|
|
84
|
+
return err
|
|
85
|
+
}
|
|
86
|
+
alias := definition.Alias
|
|
87
|
+
if alias == "" {
|
|
88
|
+
alias = definition.Function
|
|
89
|
+
}
|
|
90
|
+
row[alias] = value
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
func windowValue(definition Window, partition []map[string]interface{}, index int) (interface{}, error) {
|
|
97
|
+
switch normalizeFunction(definition.Function) {
|
|
98
|
+
case "ROW_NUMBER":
|
|
99
|
+
return int64(index + 1), nil
|
|
100
|
+
case "RANK":
|
|
101
|
+
if index == 0 || len(definition.OrderBy) == 0 {
|
|
102
|
+
return int64(index + 1), nil
|
|
103
|
+
}
|
|
104
|
+
rank := 1
|
|
105
|
+
for previous := 1; previous <= index; previous++ {
|
|
106
|
+
if compareOrderRow(partition[previous-1], partition[previous], definition.OrderBy) != 0 {
|
|
107
|
+
rank = previous + 1
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return int64(rank), nil
|
|
111
|
+
case "DENSE_RANK":
|
|
112
|
+
rank := 1
|
|
113
|
+
for previous := 1; previous <= index; previous++ {
|
|
114
|
+
if compareOrderRow(partition[previous-1], partition[previous], definition.OrderBy) != 0 {
|
|
115
|
+
rank++
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return int64(rank), nil
|
|
119
|
+
case "LAG", "LEAD":
|
|
120
|
+
delta := -1
|
|
121
|
+
if normalizeFunction(definition.Function) == "LEAD" {
|
|
122
|
+
delta = 1
|
|
123
|
+
}
|
|
124
|
+
position := index + delta
|
|
125
|
+
if position < 0 || position >= len(partition) {
|
|
126
|
+
return nil, nil
|
|
127
|
+
}
|
|
128
|
+
return partition[position][definition.Column], nil
|
|
129
|
+
case "COUNT":
|
|
130
|
+
count := int64(0)
|
|
131
|
+
for _, row := range partition {
|
|
132
|
+
if definition.Column == "*" || row[definition.Column] != nil {
|
|
133
|
+
count++
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return count, nil
|
|
137
|
+
case "SUM", "AVG", "MIN", "MAX":
|
|
138
|
+
return AggregateValue(partition, Aggregate{Function: definition.Function, Column: definition.Column}), nil
|
|
139
|
+
default:
|
|
140
|
+
return nil, fmt.Errorf("unsupported window function %q", definition.Function)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
func compareOrderRow(left, right map[string]interface{}, orderBy []Order) int {
|
|
145
|
+
for _, order := range orderBy {
|
|
146
|
+
comparison := Compare(left[order.Column], right[order.Column])
|
|
147
|
+
if comparison == 0 {
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
if equalFold(order.Direction, "desc") {
|
|
151
|
+
return -comparison
|
|
152
|
+
}
|
|
153
|
+
return comparison
|
|
154
|
+
}
|
|
155
|
+
return 0
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
func normalizeFunction(value string) string {
|
|
159
|
+
result := make([]byte, 0, len(value))
|
|
160
|
+
for _, character := range value {
|
|
161
|
+
if character >= 'a' && character <= 'z' {
|
|
162
|
+
character -= 'a' - 'A'
|
|
163
|
+
}
|
|
164
|
+
result = append(result, byte(character))
|
|
165
|
+
}
|
|
166
|
+
return string(result)
|
|
167
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import "context"
|
|
4
|
+
|
|
5
|
+
// FilterRowsContext is the cancellation-aware form used by server workers.
|
|
6
|
+
// The non-context API remains intentionally small for the stdio protocol.
|
|
7
|
+
func FilterRowsContext(ctx context.Context, rows []map[string]interface{}, condition Filter) ([]map[string]interface{}, error) {
|
|
8
|
+
filtered := make([]map[string]interface{}, 0, len(rows))
|
|
9
|
+
for _, row := range rows {
|
|
10
|
+
select {
|
|
11
|
+
case <-ctx.Done():
|
|
12
|
+
return nil, ctx.Err()
|
|
13
|
+
default:
|
|
14
|
+
}
|
|
15
|
+
if Matches(row[condition.Column], condition.Operator, condition.Value) {
|
|
16
|
+
filtered = append(filtered, row)
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return filtered, nil
|
|
20
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import "sort"
|
|
4
|
+
|
|
5
|
+
func SortRows(rows []map[string]interface{}, orderBy []Order) {
|
|
6
|
+
sort.SliceStable(rows, func(i, j int) bool {
|
|
7
|
+
for _, item := range orderBy {
|
|
8
|
+
comparison := Compare(rows[i][item.Column], rows[j][item.Column])
|
|
9
|
+
if comparison == 0 {
|
|
10
|
+
continue
|
|
11
|
+
}
|
|
12
|
+
if equalFold(item.Direction, "desc") {
|
|
13
|
+
return comparison > 0
|
|
14
|
+
}
|
|
15
|
+
return comparison < 0
|
|
16
|
+
}
|
|
17
|
+
return false
|
|
18
|
+
})
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
func ApplyWindow(rows []map[string]interface{}, offset int, limit *int) ([]map[string]interface{}, error) {
|
|
22
|
+
if offset < 0 {
|
|
23
|
+
return nil, ErrInvalidWindow("offset cannot be negative")
|
|
24
|
+
}
|
|
25
|
+
if limit != nil && *limit < 0 {
|
|
26
|
+
return nil, ErrInvalidWindow("limit cannot be negative")
|
|
27
|
+
}
|
|
28
|
+
if offset >= len(rows) {
|
|
29
|
+
return []map[string]interface{}{}, nil
|
|
30
|
+
}
|
|
31
|
+
if offset > 0 {
|
|
32
|
+
rows = rows[offset:]
|
|
33
|
+
}
|
|
34
|
+
if limit != nil && *limit < len(rows) {
|
|
35
|
+
rows = rows[:*limit]
|
|
36
|
+
}
|
|
37
|
+
return rows, nil
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
type invalidWindowError string
|
|
41
|
+
|
|
42
|
+
func (err invalidWindowError) Error() string { return string(err) }
|
|
43
|
+
func ErrInvalidWindow(message string) error { return invalidWindowError(message) }
|
|
44
|
+
|
|
45
|
+
func equalFold(left, right string) bool {
|
|
46
|
+
if len(left) != len(right) {
|
|
47
|
+
return false
|
|
48
|
+
}
|
|
49
|
+
for index := range left {
|
|
50
|
+
l, r := left[index], right[index]
|
|
51
|
+
if l >= 'A' && l <= 'Z' {
|
|
52
|
+
l += 'a' - 'A'
|
|
53
|
+
}
|
|
54
|
+
if r >= 'A' && r <= 'Z' {
|
|
55
|
+
r += 'a' - 'A'
|
|
56
|
+
}
|
|
57
|
+
if l != r {
|
|
58
|
+
return false
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
return true
|
|
62
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
package execution
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"context"
|
|
5
|
+
"encoding/json"
|
|
6
|
+
)
|
|
7
|
+
|
|
8
|
+
type RowsRequest struct {
|
|
9
|
+
Rows []map[string]interface{} `json:"rows"`
|
|
10
|
+
Filters []Filter `json:"filters,omitempty"`
|
|
11
|
+
OrderBy []Order `json:"order_by,omitempty"`
|
|
12
|
+
GroupBy []string `json:"group_by,omitempty"`
|
|
13
|
+
Aggregate []Aggregate `json:"aggregates,omitempty"`
|
|
14
|
+
Having []Filter `json:"having,omitempty"`
|
|
15
|
+
Projection []string `json:"projection,omitempty"`
|
|
16
|
+
Distinct bool `json:"distinct,omitempty"`
|
|
17
|
+
DistinctColumns []string `json:"distinct_columns,omitempty"`
|
|
18
|
+
Windows []Window `json:"windows,omitempty"`
|
|
19
|
+
BatchSize int `json:"batch_size,omitempty"`
|
|
20
|
+
Limit *int `json:"limit,omitempty"`
|
|
21
|
+
Offset int `json:"offset,omitempty"`
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
type Filter struct {
|
|
25
|
+
Column string `json:"column"`
|
|
26
|
+
Operator string `json:"operator"`
|
|
27
|
+
Value interface{} `json:"value,omitempty"`
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
type Order struct {
|
|
31
|
+
Column string `json:"column"`
|
|
32
|
+
Direction string `json:"direction,omitempty"`
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
type Aggregate struct {
|
|
36
|
+
Function string `json:"function"`
|
|
37
|
+
Column string `json:"column,omitempty"`
|
|
38
|
+
Alias string `json:"alias,omitempty"`
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Window describes the portable window subset used by the accelerator. Ruby
|
|
42
|
+
// remains the authority for parsing and deciding whether this representation
|
|
43
|
+
// is semantically safe to delegate.
|
|
44
|
+
type Window struct {
|
|
45
|
+
Function string `json:"function"`
|
|
46
|
+
Column string `json:"column,omitempty"`
|
|
47
|
+
Alias string `json:"alias,omitempty"`
|
|
48
|
+
PartitionBy []string `json:"partition_by,omitempty"`
|
|
49
|
+
OrderBy []Order `json:"order_by,omitempty"`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
type RowsResult struct {
|
|
53
|
+
Rows []map[string]interface{}
|
|
54
|
+
Aggregates []map[string]interface{}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
func (result RowsResult) AsMap() map[string]interface{} {
|
|
58
|
+
payload := map[string]interface{}{
|
|
59
|
+
"rows": result.Rows,
|
|
60
|
+
"row_count": len(result.Rows),
|
|
61
|
+
}
|
|
62
|
+
if len(result.Aggregates) > 0 {
|
|
63
|
+
payload["aggregates"] = result.Aggregates
|
|
64
|
+
}
|
|
65
|
+
return payload
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
func ExecuteRowsPipeline(payload RowsRequest) (RowsResult, error) {
|
|
69
|
+
return ExecuteRowsPipelineContext(context.Background(), payload)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
func ExecuteRowsPipelineContext(ctx context.Context, payload RowsRequest) (RowsResult, error) {
|
|
73
|
+
rows := payload.Rows
|
|
74
|
+
for _, condition := range payload.Filters {
|
|
75
|
+
filtered, err := FilterRowsContext(ctx, rows, condition)
|
|
76
|
+
if err != nil {
|
|
77
|
+
return RowsResult{}, err
|
|
78
|
+
}
|
|
79
|
+
rows = filtered
|
|
80
|
+
}
|
|
81
|
+
if len(payload.OrderBy) > 0 {
|
|
82
|
+
if err := SortRowsContext(ctx, rows, payload.OrderBy); err != nil {
|
|
83
|
+
return RowsResult{}, err
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
result := RowsResult{}
|
|
87
|
+
var aggregateRows []map[string]interface{}
|
|
88
|
+
if len(payload.Aggregate) > 0 {
|
|
89
|
+
aggregateRows = AggregateRows(rows, payload.GroupBy, payload.Aggregate)
|
|
90
|
+
for _, condition := range payload.Having {
|
|
91
|
+
filtered, err := FilterRowsContext(ctx, aggregateRows, condition)
|
|
92
|
+
if err != nil {
|
|
93
|
+
return RowsResult{}, err
|
|
94
|
+
}
|
|
95
|
+
aggregateRows = filtered
|
|
96
|
+
}
|
|
97
|
+
if len(payload.Projection) > 0 {
|
|
98
|
+
rows = aggregateRows
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if payload.Distinct {
|
|
102
|
+
columns := payload.DistinctColumns
|
|
103
|
+
if len(columns) == 0 {
|
|
104
|
+
columns = payload.Projection
|
|
105
|
+
}
|
|
106
|
+
if len(columns) == 0 && len(rows) > 0 {
|
|
107
|
+
for column := range rows[0] {
|
|
108
|
+
columns = append(columns, column)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
rows = DistinctRows(rows, columns)
|
|
112
|
+
}
|
|
113
|
+
if len(payload.Projection) > 0 {
|
|
114
|
+
rows = ProjectRows(rows, payload.Projection)
|
|
115
|
+
}
|
|
116
|
+
if len(payload.Windows) > 0 {
|
|
117
|
+
var err error
|
|
118
|
+
rows, err = ApplyWindowsContext(ctx, rows, payload.Windows)
|
|
119
|
+
if err != nil {
|
|
120
|
+
return RowsResult{}, err
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rows, err := ApplyWindow(rows, payload.Offset, payload.Limit)
|
|
124
|
+
if err != nil {
|
|
125
|
+
return RowsResult{}, err
|
|
126
|
+
}
|
|
127
|
+
result.Rows = rows
|
|
128
|
+
if len(payload.Aggregate) > 0 {
|
|
129
|
+
result.Aggregates = aggregateRows
|
|
130
|
+
}
|
|
131
|
+
return result, nil
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
func MarshalRowsResult(result RowsResult) ([]byte, error) {
|
|
135
|
+
return json.Marshal(result.AsMap())
|
|
136
|
+
}
|