Skip to content

Commit 3a6c585

Browse files
authored
Merge pull request #202 from Apostlex0/feature/chart-histogram
Port ChartHistogram from FireFly Core (#196)
2 parents db56dd8 + f1de4ea commit 3a6c585

4 files changed

Lines changed: 547 additions & 0 deletions

File tree

pkg/dbsql/config.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ const (
3737
SQLConfMaxIdleConns = "maxIdleConns"
3838
// SQLConfMaxConnLifetime maximum connections to the database
3939
SQLConfMaxConnLifetime = "maxConnLifetime"
40+
// SQLConfHistogramsMaxChartRows maximum rows to fetch
41+
SQLConfHistogramsMaxChartRows = "histograms.maxChartRows"
4042
)
4143

4244
const (
@@ -51,4 +53,5 @@ func (s *Database) InitConfig(provider Provider, config config.Section) {
5153
config.AddKnownKey(SQLConfMaxConnIdleTime, "1m")
5254
config.AddKnownKey(SQLConfMaxIdleConns) // defaults to the max connections
5355
config.AddKnownKey(SQLConfMaxConnLifetime)
56+
config.AddKnownKey(SQLConfHistogramsMaxChartRows, 100) // as per ff core
5457
}

pkg/dbsql/histogram_sql.go

Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
// Copyright © 2025 Kaleido, Inc.
2+
//
3+
// SPDX-License-Identifier: Apache-2.0
4+
//
5+
// Licensed under the Apache License, Version 2.0 (the "License");
6+
// you may not use this file except in compliance with the License.
7+
// You may obtain a copy of the License at
8+
//
9+
// http://www.apache.org/licenses/LICENSE-2.0
10+
//
11+
// Unless required by applicable law or agreed to in writing, software
12+
// distributed under the License is distributed on an "AS IS" BASIS,
13+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
// See the License for the specific language governing permissions and
15+
// limitations under the License.
16+
17+
package dbsql
18+
19+
import (
20+
"context"
21+
"database/sql"
22+
"strconv"
23+
24+
sq "github.com/Masterminds/squirrel"
25+
"github.com/hyperledger/firefly-common/pkg/config"
26+
"github.com/hyperledger/firefly-common/pkg/fftypes"
27+
"github.com/hyperledger/firefly-common/pkg/i18n"
28+
)
29+
30+
// GetChartHistogram executes a collection of queries (one per interval) and builds
31+
// a histogram response for the specified table and time intervals.
32+
func (s *Database) GetChartHistogram(
33+
ctx context.Context,
34+
tableName string,
35+
timestampColumn string,
36+
typeColumn string,
37+
namespaceColumn string,
38+
namespaceValue string,
39+
intervals []fftypes.ChartHistogramInterval,
40+
) ([]*fftypes.ChartHistogram, error) {
41+
42+
maxRows := config.GetUint64(SQLConfHistogramsMaxChartRows)
43+
44+
// check if we have a type column for grouping
45+
hasTypeColumn := typeColumn != ""
46+
47+
// Build qs for each interval
48+
queries := s.buildHistogramQueries(
49+
tableName, timestampColumn, typeColumn,
50+
namespaceColumn, namespaceValue,
51+
intervals, maxRows,
52+
)
53+
54+
histogramList := []*fftypes.ChartHistogram{}
55+
56+
for i, query := range queries {
57+
rows, _, err := s.Query(ctx, tableName, query)
58+
if err != nil {
59+
return nil, err
60+
}
61+
defer rows.Close()
62+
data, total, err := s.processHistogramRows(ctx, tableName, rows, hasTypeColumn)
63+
if err != nil {
64+
return nil, err
65+
}
66+
67+
// Build hist bucket
68+
histBucket := &fftypes.ChartHistogram{
69+
Count: strconv.FormatUint(total, 10),
70+
Timestamp: intervals[i].StartTime,
71+
Types: []*fftypes.ChartHistogramType{},
72+
IsCapped: total == maxRows,
73+
}
74+
75+
// Add type counts if applicable
76+
if hasTypeColumn {
77+
for t, c := range data {
78+
histBucket.Types = append(histBucket.Types,
79+
&fftypes.ChartHistogramType{
80+
Count: strconv.Itoa(c),
81+
Type: t,
82+
})
83+
}
84+
}
85+
86+
histogramList = append(histogramList, histBucket)
87+
}
88+
89+
return histogramList, nil
90+
}
91+
92+
// buildHistogramQueries constructs SQL queries for each time interval.
93+
// each query selects data within the interval's time range, optionally
94+
// filtered by namespace, and limited to maxRows.
95+
func (s *Database) buildHistogramQueries(
96+
tableName string,
97+
timestampColumn string,
98+
typeColumn string,
99+
namespaceColumn string,
100+
namespaceValue string,
101+
intervals []fftypes.ChartHistogramInterval,
102+
maxRows uint64,
103+
) []sq.SelectBuilder {
104+
105+
queries := []sq.SelectBuilder{}
106+
107+
// Determine columns to select
108+
cols := []string{timestampColumn}
109+
if typeColumn != "" {
110+
cols = append(cols, typeColumn)
111+
}
112+
113+
for _, i := range intervals {
114+
whereClause := sq.And{
115+
sq.GtOrEq{timestampColumn: i.StartTime},
116+
sq.Lt{timestampColumn: i.EndTime},
117+
}
118+
119+
// namespace filter
120+
if namespaceColumn != "" && namespaceValue != "" {
121+
whereClause = append(whereClause, sq.Eq{namespaceColumn: namespaceValue})
122+
}
123+
124+
// Build query with PlaceholderFormat applied
125+
query := sq.Select(cols...).
126+
From(tableName).
127+
Where(whereClause).
128+
OrderBy(timestampColumn).
129+
Limit(maxRows).
130+
PlaceholderFormat(s.features.PlaceholderFormat)
131+
132+
queries = append(queries, query)
133+
}
134+
135+
return queries
136+
}
137+
138+
// processHistogramRows scans SQL result rows and builds histogram data
139+
// If hasTypeColumn is true, it groups counts by type else it just
140+
// counts total rows.
141+
func (s *Database) processHistogramRows(
142+
ctx context.Context,
143+
tableName string,
144+
rows *sql.Rows,
145+
hasTypeColumn bool,
146+
) (map[string]int, uint64, error) {
147+
148+
total := uint64(0)
149+
150+
if !hasTypeColumn {
151+
// counting rows
152+
for rows.Next() {
153+
var timestamp interface{}
154+
if err := rows.Scan(&timestamp); err != nil {
155+
return nil, 0, i18n.NewError(ctx, i18n.MsgDBReadErr, tableName)
156+
}
157+
total++
158+
}
159+
return map[string]int{}, total, nil
160+
}
161+
162+
// Count by type
163+
typeMap := map[string]int{}
164+
for rows.Next() {
165+
var timestamp interface{}
166+
var typeStr string
167+
if err := rows.Scan(&timestamp, &typeStr); err != nil {
168+
return nil, 0, i18n.NewError(ctx, i18n.MsgDBReadErr, tableName)
169+
}
170+
171+
typeMap[typeStr]++
172+
total++
173+
}
174+
175+
return typeMap, total, nil
176+
}

0 commit comments

Comments
 (0)