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,11 @@
1
+ package main
2
+
3
+ import (
4
+ "os"
5
+
6
+ acceleratorRuntime "github.com/aldanedev-create/rubydb/accelerator/internal/runtime"
7
+ )
8
+
9
+ func main() {
10
+ acceleratorRuntime.Run(os.Stdin, os.Stdout)
11
+ }
@@ -0,0 +1,3 @@
1
+ module github.com/aldanedev-create/rubydb/accelerator
2
+
3
+ go 1.23
@@ -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
+ }