Skip to content
This repository was archived by the owner on Mar 4, 2025. It is now read-only.

Commit fe31f98

Browse files
committed
common, live, webui: Add very basic data view page for live databases
1 parent 472483e commit fe31f98

File tree

9 files changed

+1092
-284
lines changed

9 files changed

+1092
-284
lines changed

common/live.go

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,11 @@ type LiveDBQueryResponse struct {
6464

6565
// LiveDBRequest holds the fields used for sending requests to our AMQP backend
6666
type LiveDBRequest struct {
67-
Operation string `json:"operation"`
68-
DBOwner string `json:"dbowner"`
69-
DBName string `json:"dbname"`
70-
Query string `json:"query"`
71-
RequestingUser string `json:"requesting_user"`
67+
Operation string `json:"operation"`
68+
DBOwner string `json:"dbowner"`
69+
DBName string `json:"dbname"`
70+
Data interface{} `json:"data,omitempty"`
71+
RequestingUser string `json:"requesting_user"`
7272
}
7373

7474
// LiveDBResponse holds the fields used for receiving (non-query) responses from our AMQP backend
@@ -78,6 +78,26 @@ type LiveDBResponse struct {
7878
Error string `json:"error"`
7979
}
8080

81+
// LiveDBRowsRequest holds the data used when making an AMQP rows request
82+
type LiveDBRowsRequest struct {
83+
DbTable string `json:"db_table"`
84+
SortCol string `json:"sort_col"`
85+
SortDir string `json:"sort_dir"`
86+
CommitID string `json:"commit_id"`
87+
RowOffset int `json:"row_offset"`
88+
MaxRows int `json:"max_rows"`
89+
}
90+
91+
// LiveDBRowsResponse holds the fields used for receiving database page row responses from our AMQP backend
92+
type LiveDBRowsResponse struct {
93+
Node string `json:"node"`
94+
DatabaseSize int64 `json:"database_size"`
95+
DefaultTable string `json:"default_table"`
96+
RowData SQLiteRecordSet `json:"row_data"`
97+
Tables []string `json:"tables"`
98+
Error string `json:"error"`
99+
}
100+
81101
// LiveDBs is used for general purpose holding of details about live databases
82102
type LiveDBs struct {
83103
DBOwner string `json:"owner_name"`
@@ -292,7 +312,7 @@ func MQCreateResponse(msg amqp.Delivery, channel *amqp.Channel, nodeName, result
292312
}
293313

294314
// MQRequest is the main function used for sending requests to our AMQP backend
295-
func MQRequest(channel *amqp.Channel, queue, operation, requestingUser, dbOwner, dbName, query string) (result []byte, err error) {
315+
func MQRequest(channel *amqp.Channel, queue, operation, requestingUser, dbOwner, dbName string, data interface{}) (result []byte, err error) {
296316
// Create a temporary AMQP queue for receiving the response
297317
var q amqp.Queue
298318
q, err = channel.QueueDeclare("", false, false, true, false, nil)
@@ -305,7 +325,7 @@ func MQRequest(channel *amqp.Channel, queue, operation, requestingUser, dbOwner,
305325
Operation: operation,
306326
DBOwner: dbOwner,
307327
DBName: dbName,
308-
Query: query,
328+
Data: data,
309329
RequestingUser: requestingUser,
310330
}
311331
var z []byte

common/memcache.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ func SetUserStatusUpdates(userName string, numUpdates int) error {
229229
return nil
230230
}
231231

232-
// TableRowsCacheKey generates a predictable cache key for SQLite row data
232+
// TableRowsCacheKey generates a predictable cache key for SQLite row data. ONLY for standard databases
233233
func TableRowsCacheKey(prefix string, loggedInUser string, dbOwner string, dbFolder string, dbName string, commitID string, dbTable string, rows int) string {
234234
var cacheString string
235235
if strings.ToLower(loggedInUser) == strings.ToLower(dbOwner) {

common/sqlite.go

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1142,6 +1142,135 @@ func SQLiteSanityCheck(fileName string) (tables []string, err error) {
11421142
return
11431143
}
11441144

1145+
// SQLiteReadDatabasePage opens a SQLite database (locally) and returns a "page" of rows from it, for display in the
1146+
// database view page. Note that the dbSize return value is only set for live databases.
1147+
func SQLiteReadDatabasePage(bucket, id, loggedInUser, dbOwner, dbName, dbTable, sortCol, sortDir, commitID string, rowOffset, maxRows int, isLive bool) (tables []string, defaultTable string, rowData SQLiteRecordSet, dbSize int64, err error) {
1148+
dbFolder := "/"
1149+
1150+
// Get a handle from Minio for the database object
1151+
var sdb *sqlite.Conn
1152+
if isLive {
1153+
// Open live database file
1154+
sdb, err = OpenSQLiteDatabaseLive(Conf.Live.StorageDir, dbOwner, dbName)
1155+
if err != nil {
1156+
return
1157+
}
1158+
1159+
// We also return the file size for live database files
1160+
var z os.FileInfo
1161+
z, err = os.Stat(filepath.Join(Conf.Live.StorageDir, dbOwner, dbName, "live.sqlite"))
1162+
if err != nil {
1163+
return
1164+
}
1165+
dbSize = z.Size()
1166+
} else {
1167+
// Open standard database
1168+
sdb, err = OpenSQLiteDatabase(bucket, id)
1169+
if err != nil {
1170+
return
1171+
}
1172+
}
1173+
defer sdb.Close()
1174+
1175+
// Retrieve the list of tables and views in the database
1176+
tables, err = TablesAndViews(sdb, dbName)
1177+
if err != nil {
1178+
return
1179+
}
1180+
1181+
// If a specific table or view was requested, check that it's present
1182+
if dbTable != "" {
1183+
tablePresent := false
1184+
for _, tbl := range tables {
1185+
if tbl == dbTable {
1186+
tablePresent = true
1187+
}
1188+
}
1189+
if tablePresent == false {
1190+
// The requested table or view doesn't exist in the database, so pick one of the tables that does
1191+
for _, t := range tables {
1192+
err = ValidatePGTable(t)
1193+
if err == nil {
1194+
// Validation passed, so use this table or view
1195+
dbTable = t
1196+
defaultTable = t
1197+
break
1198+
}
1199+
}
1200+
}
1201+
}
1202+
1203+
// If a specific table wasn't requested, use the first table in the database that passes validation
1204+
if dbTable == "" {
1205+
for _, i := range tables {
1206+
if i != "" {
1207+
err = ValidatePGTable(i)
1208+
if err == nil {
1209+
// The database table name is acceptable, so use it
1210+
dbTable = i
1211+
break
1212+
}
1213+
}
1214+
}
1215+
}
1216+
1217+
// If a sort column was requested, verify it exists
1218+
if sortCol != "" {
1219+
var colList []sqlite.Column
1220+
colList, err = sdb.Columns("", dbTable)
1221+
if err != nil {
1222+
log.Printf("Error when reading column names for table '%s': %v\n", SanitiseLogString(dbTable), err.Error())
1223+
return
1224+
}
1225+
colExists := false
1226+
for _, j := range colList {
1227+
if j.Name == sortCol {
1228+
colExists = true
1229+
}
1230+
}
1231+
if colExists == false {
1232+
// The requested sort column doesn't exist, so we fall back to no sorting
1233+
sortCol = ""
1234+
}
1235+
}
1236+
1237+
// Validate the table name, just to be careful
1238+
if dbTable != "" {
1239+
err = ValidatePGTable(dbTable)
1240+
if err != nil {
1241+
// Validation failed, so don't pass on the table name
1242+
1243+
// If the failed table name is "{{ db.Tablename }}", don't bother logging it. It's just a search
1244+
// bot picking up AngularJS in a string and doing a request with it
1245+
if dbTable != "{{ db.Tablename }}" {
1246+
log.Printf("Validation failed for table name: '%s': %s", SanitiseLogString(dbTable), err)
1247+
}
1248+
return
1249+
}
1250+
}
1251+
1252+
var okCache bool
1253+
if !isLive {
1254+
rowCacheKey := TableRowsCacheKey(fmt.Sprintf("tablejson/%s/%s/%d", sortCol, sortDir, rowOffset),
1255+
loggedInUser, dbOwner, dbFolder, dbName, commitID, dbTable, maxRows)
1256+
okCache, err = GetCachedData(rowCacheKey, &rowData)
1257+
if err != nil {
1258+
log.Printf("Error retrieving page data from cache: %v", err)
1259+
}
1260+
}
1261+
1262+
// If the row data wasn't in cache, read it from the database
1263+
if !okCache {
1264+
rowData, err = ReadSQLiteDB(sdb, dbTable, sortCol, sortDir, maxRows, rowOffset)
1265+
if err != nil {
1266+
// Some kind of error when reading the database data
1267+
return
1268+
}
1269+
rowData.Tablename = dbTable
1270+
}
1271+
return
1272+
}
1273+
11451274
// SQLiteRunQuery runs a SQLite query. DO NOT use this for user provided SQL queries. For those,
11461275
// use SQLiteRunQueryDefensive().
11471276
func SQLiteRunQuery(sdb *sqlite.Conn, querySource QuerySource, dbQuery string, ignoreBinary, ignoreNull bool) (memUsed, memHighWater int64, dataRows SQLiteRecordSet, err error) {

live/main.go

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ package main
1010
import (
1111
"encoding/json"
1212
"errors"
13+
"fmt"
1314
"log"
1415
"os"
1516
"path/filepath"
@@ -142,7 +143,7 @@ func main() {
142143
}
143144

144145
if com.AmqpDebug {
145-
log.Printf("Decoded request on '%s'. Correlation ID: '%s', request operation: '%s', request query: '%s'", com.Conf.Live.Nodename, msg.CorrelationId, req.Operation, req.Query)
146+
log.Printf("Decoded request on '%s'. Correlation ID: '%s', request operation: '%s', request query: '%v'", com.Conf.Live.Nodename, msg.CorrelationId, req.Operation, req.Data)
146147
}
147148

148149
// Handle each operation
@@ -169,7 +170,7 @@ func main() {
169170
case "columns":
170171
var columns []sqlite.Column
171172
var errCode com.AMQPErrorCode
172-
columns, err, errCode = com.SQLiteGetColumnsLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, req.Query) // We use the req.Query field to pass the table name
173+
columns, err, errCode = com.SQLiteGetColumnsLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, fmt.Sprintf("%s", req.Data))
173174
if err != nil {
174175
resp := com.LiveDBColumnsResponse{Node: com.Conf.Live.Nodename, Columns: []sqlite.Column{}, Error: err.Error(), ErrCode: errCode}
175176
err = com.MQResponse("COLUMNS", msg, ch, com.Conf.Live.Nodename, resp)
@@ -210,7 +211,7 @@ func main() {
210211
case "execute":
211212
// Execute a SQL statement on the database file
212213
var rowsChanged int
213-
rowsChanged, err = com.SQLiteExecuteQueryLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, req.RequestingUser, req.Query)
214+
rowsChanged, err = com.SQLiteExecuteQueryLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, req.RequestingUser, fmt.Sprintf("%s", req.Data))
214215
if err != nil {
215216
resp := com.LiveDBExecuteResponse{Node: com.Conf.Live.Nodename, RowsChanged: 0, Error: err.Error()}
216217
err = com.MQResponse("EXECUTE", msg, ch, com.Conf.Live.Nodename, resp)
@@ -250,7 +251,7 @@ func main() {
250251

251252
case "query":
252253
var rows com.SQLiteRecordSet
253-
rows, err = com.SQLiteRunQueryLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, req.RequestingUser, req.Query)
254+
rows, err = com.SQLiteRunQueryLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName, req.RequestingUser, fmt.Sprintf("%s", req.Data))
254255
if err != nil {
255256
resp := com.LiveDBQueryResponse{Node: com.Conf.Live.Nodename, Results: com.SQLiteRecordSet{}, Error: err.Error()}
256257
err = com.MQResponse("QUERY", msg, ch, com.Conf.Live.Nodename, resp)
@@ -268,6 +269,38 @@ func main() {
268269
}
269270
continue
270271

272+
case "rowdata":
273+
// Extract the request information
274+
// FIXME: Add type checks for safety instead of blind coercing
275+
var reqData = make(map[string]interface{})
276+
reqData = req.Data.(map[string]interface{})
277+
dbTable := reqData["db_table"].(string)
278+
sortCol := reqData["sort_col"].(string)
279+
sortDir := reqData["sort_dir"].(string)
280+
commitID := reqData["commit_id"].(string)
281+
maxRows := int(reqData["max_rows"].(float64))
282+
rowOffset := int(reqData["row_offset"].(float64))
283+
284+
// Open the SQLite database and read the row data
285+
resp := com.LiveDBRowsResponse{Node: com.Conf.Live.Nodename, RowData: com.SQLiteRecordSet{}}
286+
resp.Tables, resp.DefaultTable, resp.RowData, resp.DatabaseSize, err =
287+
com.SQLiteReadDatabasePage(fmt.Sprintf("live-%s", req.DBOwner), req.DBName, req.RequestingUser, req.DBOwner, req.DBName, dbTable, sortCol, sortDir, commitID, rowOffset, maxRows, true)
288+
if err != nil {
289+
resp := com.LiveDBErrorResponse{Node: com.Conf.Live.Nodename, Error: err.Error()}
290+
err = com.MQResponse("ROWDATA", msg, ch, com.Conf.Live.Nodename, resp)
291+
if err != nil {
292+
log.Printf("Error: occurred on '%s' in MQResponse() while constructing an AMQP error message response: '%s'", com.Conf.Live.Nodename, err)
293+
}
294+
continue
295+
}
296+
297+
// Return the row data to the caller
298+
err = com.MQResponse("ROWDATA", msg, ch, com.Conf.Live.Nodename, resp)
299+
if err != nil {
300+
log.Printf("Error: occurred on '%s' in MQResponse() while constructing the AMQP query response: '%s'", com.Conf.Live.Nodename, err)
301+
}
302+
continue
303+
271304
case "tables":
272305
var tables []string
273306
tables, err = com.SQLiteGetTablesLive(com.Conf.Live.StorageDir, req.DBOwner, req.DBName)

0 commit comments

Comments
 (0)