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

Commit d48dc02

Browse files
committed
webui: Save history of executed statements in the database
This saves the SQL statements executed via the execute page in a database table. This means a user can see their last statements including their results. This a) makes sure the state of the terminal component is not lost when the page is refreshed or a link clicked and b) allows users to continue their work on a different device or day. The migrations for this are: create table public.sql_terminal_history (history_id bigint not null, user_id bigint not null, db_id bigint not null, sql_stmt text, result jsonb, state text not null); comment on table public.sql_terminal_history is 'This table holds the history of executed SQL statements for a user and database'; create sequence public.sql_terminal_history_history_id_seq start with 1 increment by 1 no minvalue no maxvalue cache 1; alter sequence public.sql_terminal_history_history_id_seq owned by public.sql_terminal_history.history_id; alter table only public.sql_terminal_history alter column history_id set default nextval('public.sql_terminal_history_history_id_seq'::regclass); create index sql_terminal_history_user_id_db_id_index on public.sql_terminal_history using btree(user_id,db_id); alter table only public.sql_terminal_history add constraint sql_terminal_history_users_user_id_fk foreign key (user_id) references public.users(user_id); alter table only public.sql_terminal_history add constraint sql_terminal_history_sqlite_databases_db_id_fk foreign key (db_id) references public.sqlite_databases(db_id) on delete cascade;
1 parent 761aa87 commit d48dc02

File tree

6 files changed

+238
-5
lines changed

6 files changed

+238
-5
lines changed

common/postgresql.go

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2666,6 +2666,112 @@ func LiveGetMinioNames(loggedInUser, dbOwner, dbName string) (bucketName, object
26662666
return
26672667
}
26682668

2669+
// LiveSqlHistoryAdd adds a new record to the history of recently executed SQL statements
2670+
func LiveSqlHistoryAdd(loggedInUser, dbOwner, dbName, stmt string, state SqlHistoryItemStates, result interface{}) (err error) {
2671+
// Delete old records. We want to keep 100 records, so delete all but 99 and add one new in the next step
2672+
// TODO Make this number configurable or something
2673+
err = LiveSqlHistoryDeleteOld(loggedInUser, dbOwner, dbName, 99)
2674+
if err != nil {
2675+
return err
2676+
}
2677+
2678+
dbQuery := `
2679+
WITH u AS (
2680+
SELECT user_id
2681+
FROM users
2682+
WHERE lower(user_name) = lower($1)
2683+
), d AS (
2684+
SELECT db.db_id
2685+
FROM sqlite_databases AS db, u
2686+
WHERE db.user_id = u.user_id
2687+
AND db.db_name = $2
2688+
), l AS (
2689+
SELECT user_id
2690+
FROM users
2691+
WHERE lower(user_name) = lower($3)
2692+
)
2693+
INSERT INTO sql_terminal_history (user_id, db_id, sql_stmt, state, result)
2694+
VALUES ((SELECT user_id FROM l), (SELECT db_id FROM d), $4, $5, $6)`
2695+
commandTag, err := pdb.Exec(dbQuery, dbOwner, dbName, loggedInUser, stmt, state, result)
2696+
if err != nil {
2697+
return err
2698+
}
2699+
if numRows := commandTag.RowsAffected(); numRows != 1 {
2700+
log.Printf("Wrong number of rows (%d) affected while saving SQL statement for user '%s'", numRows,
2701+
SanitiseLogString(loggedInUser))
2702+
}
2703+
return
2704+
}
2705+
2706+
// LiveSqlHistoryDeleteOld deletes all saved SQL statements in the SQL history table, except for the most recent ones
2707+
func LiveSqlHistoryDeleteOld(loggedInUser, dbOwner, dbName string, keepRecords int) (err error) {
2708+
dbQuery := `
2709+
WITH u AS (
2710+
SELECT user_id
2711+
FROM users
2712+
WHERE lower(user_name) = lower($1)
2713+
), d AS (
2714+
SELECT db.db_id
2715+
FROM sqlite_databases AS db, u
2716+
WHERE db.user_id = u.user_id
2717+
AND db.db_name = $2
2718+
), l AS (
2719+
SELECT user_id
2720+
FROM users
2721+
WHERE lower(user_name) = lower($3)
2722+
)
2723+
DELETE FROM sql_terminal_history
2724+
WHERE history_id NOT IN (
2725+
SELECT h.history_id FROM sql_terminal_history h, u, d, l WHERE h.user_id=u.user_id AND h.db_id=d.db_id
2726+
ORDER BY h.history_id DESC LIMIT $4
2727+
)`
2728+
_, err = pdb.Exec(dbQuery, dbOwner, dbName, loggedInUser, keepRecords)
2729+
if err != nil {
2730+
return err
2731+
}
2732+
return
2733+
}
2734+
2735+
// LiveSqlHistoryGet returns the list of recently executed SQL statement for a user and database
2736+
func LiveSqlHistoryGet(loggedInUser, dbOwner, dbName string) (history []SqlHistoryItem, err error) {
2737+
dbQuery := `
2738+
WITH u AS (
2739+
SELECT user_id
2740+
FROM users
2741+
WHERE lower(user_name) = lower($1)
2742+
), d AS (
2743+
SELECT db.db_id
2744+
FROM sqlite_databases AS db, u
2745+
WHERE db.user_id = u.user_id
2746+
AND db.db_name = $2
2747+
), l AS (
2748+
SELECT user_id
2749+
FROM users
2750+
WHERE lower(user_name) = lower($3)
2751+
)
2752+
SELECT h.sql_stmt, h.result, h.state
2753+
FROM sql_terminal_history h, l, d, u
2754+
WHERE h.user_id=l.user_id AND h.db_id=d.db_id
2755+
ORDER BY history_id ASC`
2756+
rows, err := pdb.Query(dbQuery, dbOwner, dbName, loggedInUser)
2757+
if err != nil {
2758+
log.Printf("Database query failed: %v", err)
2759+
return nil, err
2760+
}
2761+
defer rows.Close()
2762+
2763+
for rows.Next() {
2764+
var item SqlHistoryItem
2765+
err = rows.Scan(&item.Statement, &item.Result, &item.State)
2766+
if err != nil {
2767+
return nil, err
2768+
}
2769+
2770+
history = append(history, item)
2771+
}
2772+
return
2773+
}
2774+
26692775
// LiveUserDBs returns the list of live databases owned by the user
26702776
func LiveUserDBs(dbOwner string, public AccessType) (list []DBInfo, err error) {
26712777
dbQuery := `
@@ -3044,6 +3150,7 @@ func ResetDB() error {
30443150
"discussions",
30453151
"email_queue",
30463152
"events",
3153+
"sql_terminal_history",
30473154
"sqlite_databases",
30483155
"users",
30493156
"vis_params",
@@ -3063,6 +3170,7 @@ func ResetDB() error {
30633170
"discussions_disc_id_seq",
30643171
"email_queue_email_id_seq",
30653172
"events_event_id_seq",
3173+
"sql_terminal_history_history_id_seq",
30663174
"sqlite_databases_db_id_seq",
30673175
"users_user_id_seq",
30683176
"vis_query_runs_query_run_id_seq",

common/types.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,20 @@ type ShareDatabasePermissionsUser struct {
379379
Permission ShareDatabasePermissions `json:"permission"`
380380
}
381381

382+
type SqlHistoryItemStates string
383+
384+
const (
385+
Executed SqlHistoryItemStates = "executed"
386+
Queried SqlHistoryItemStates = "queried"
387+
Error SqlHistoryItemStates = "error"
388+
)
389+
390+
type SqlHistoryItem struct {
391+
Statement string `json:"input"`
392+
Result interface{} `json:"output"`
393+
State SqlHistoryItemStates `json:"state"`
394+
}
395+
382396
type SQLiteDBinfo struct {
383397
Info DBInfo
384398
MaxRows int

database/dbhub.sql

Lines changed: 71 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
--
44

55
-- Dumped from database version 15.2
6-
-- Dumped by pg_dump version 15.2 (Ubuntu 15.2-1.pgdg20.04+1)
6+
-- Dumped by pg_dump version 15.2
77

88
SET statement_timeout = 0;
99
SET lock_timeout = 0;
@@ -425,6 +425,46 @@ CREATE SEQUENCE public.events_event_id_seq
425425
ALTER SEQUENCE public.events_event_id_seq OWNED BY public.events.event_id;
426426

427427

428+
--
429+
-- Name: sql_terminal_history; Type: TABLE; Schema: public; Owner: -
430+
--
431+
432+
CREATE TABLE public.sql_terminal_history (
433+
history_id bigint NOT NULL,
434+
user_id bigint NOT NULL,
435+
db_id bigint NOT NULL,
436+
sql_stmt text,
437+
result jsonb,
438+
state text NOT NULL
439+
);
440+
441+
442+
--
443+
-- Name: TABLE sql_terminal_history; Type: COMMENT; Schema: public; Owner: -
444+
--
445+
446+
COMMENT ON TABLE public.sql_terminal_history IS 'This table holds the history of executed SQL statements for a user and database';
447+
448+
449+
--
450+
-- Name: sql_terminal_history_history_id_seq; Type: SEQUENCE; Schema: public; Owner: -
451+
--
452+
453+
CREATE SEQUENCE public.sql_terminal_history_history_id_seq
454+
START WITH 1
455+
INCREMENT BY 1
456+
NO MINVALUE
457+
NO MAXVALUE
458+
CACHE 1;
459+
460+
461+
--
462+
-- Name: sql_terminal_history_history_id_seq; Type: SEQUENCE OWNED BY; Schema: public; Owner: -
463+
--
464+
465+
ALTER SEQUENCE public.sql_terminal_history_history_id_seq OWNED BY public.sql_terminal_history.history_id;
466+
467+
428468
--
429469
-- Name: sqlite_databases; Type: TABLE; Schema: public; Owner: -
430470
--
@@ -669,6 +709,13 @@ ALTER TABLE ONLY public.email_queue ALTER COLUMN email_id SET DEFAULT nextval('p
669709
ALTER TABLE ONLY public.events ALTER COLUMN event_id SET DEFAULT nextval('public.events_event_id_seq'::regclass);
670710

671711

712+
--
713+
-- Name: sql_terminal_history history_id; Type: DEFAULT; Schema: public; Owner: -
714+
--
715+
716+
ALTER TABLE ONLY public.sql_terminal_history ALTER COLUMN history_id SET DEFAULT nextval('public.sql_terminal_history_history_id_seq'::regclass);
717+
718+
672719
--
673720
-- Name: sqlite_databases db_id; Type: DEFAULT; Schema: public; Owner: -
674721
--
@@ -956,6 +1003,13 @@ CREATE INDEX fki_discussion_comments_db_id_fkey ON public.discussion_comments US
9561003
CREATE INDEX fki_discussions_source_db_id_fkey ON public.discussions USING btree (mr_source_db_id);
9571004

9581005

1006+
--
1007+
-- Name: sql_terminal_history_user_id_db_id_index; Type: INDEX; Schema: public; Owner: -
1008+
--
1009+
1010+
CREATE INDEX sql_terminal_history_user_id_db_id_index ON public.sql_terminal_history USING btree (user_id, db_id);
1011+
1012+
9591013
--
9601014
-- Name: users_lower_user_name_idx; Type: INDEX; Schema: public; Owner: -
9611015
--
@@ -1135,6 +1189,22 @@ ALTER TABLE ONLY public.events
11351189
ADD CONSTRAINT events_db_id_fkey FOREIGN KEY (db_id) REFERENCES public.sqlite_databases(db_id) ON UPDATE CASCADE ON DELETE CASCADE;
11361190

11371191

1192+
--
1193+
-- Name: sql_terminal_history sql_terminal_history_sqlite_databases_db_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
1194+
--
1195+
1196+
ALTER TABLE ONLY public.sql_terminal_history
1197+
ADD CONSTRAINT sql_terminal_history_sqlite_databases_db_id_fk FOREIGN KEY (db_id) REFERENCES public.sqlite_databases(db_id) ON DELETE CASCADE;
1198+
1199+
1200+
--
1201+
-- Name: sql_terminal_history sql_terminal_history_users_user_id_fk; Type: FK CONSTRAINT; Schema: public; Owner: -
1202+
--
1203+
1204+
ALTER TABLE ONLY public.sql_terminal_history
1205+
ADD CONSTRAINT sql_terminal_history_users_user_id_fk FOREIGN KEY (user_id) REFERENCES public.users(user_id);
1206+
1207+
11381208
--
11391209
-- Name: sqlite_databases sqlite_databases_user_id_fkey; Type: FK CONSTRAINT; Schema: public; Owner: -
11401210
--

webui/execute.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ func executePage(w http.ResponseWriter, r *http.Request) {
1515
var pageData struct {
1616
DB com.SQLiteDBinfo
1717
PageMeta PageMetaInfo
18+
SqlHistory []com.SqlHistoryItem
1819
}
1920

2021
// Get all meta information
@@ -67,6 +68,13 @@ func executePage(w http.ResponseWriter, r *http.Request) {
6768
return
6869
}
6970

71+
// Get SQL history
72+
pageData.SqlHistory, err = com.LiveSqlHistoryGet(pageData.PageMeta.LoggedInUser, dbName.Owner, dbName.Database)
73+
if err != nil {
74+
errorPage(w, r, http.StatusInternalServerError, err.Error())
75+
return
76+
}
77+
7078
// Fill out various metadata fields
7179
pageData.PageMeta.Title = fmt.Sprintf("Execute SQL - %s / %s", dbName.Owner, dbName.Database)
7280
pageData.PageMeta.PageSection = "db_exec"
@@ -155,6 +163,18 @@ func execLiveSQL(w http.ResponseWriter, r *http.Request) {
155163
return
156164
}
157165

166+
// In case of an error in the statement, save it in the terminal history as well.
167+
// This should not be registered too early because we do not want to save it when
168+
// there are validation or permission errors.
169+
var logError = func(e error) {
170+
// Store statement in sql terminal history
171+
err = com.LiveSqlHistoryAdd(loggedInUser, dbOwner, dbName, sql, com.Error, map[string]interface{}{"error": e.Error()})
172+
if err != nil {
173+
w.WriteHeader(http.StatusInternalServerError)
174+
fmt.Fprint(w, err)
175+
}
176+
}
177+
158178
// Send the SQL execution request to our AMQP backend
159179
var z interface{}
160180
rowsChanged, err := com.LiveExecute(liveNode, loggedInUser, dbOwner, dbName, sql)
@@ -163,6 +183,7 @@ func execLiveSQL(w http.ResponseWriter, r *http.Request) {
163183
log.Println(err)
164184
w.WriteHeader(http.StatusInternalServerError)
165185
fmt.Fprint(w, err)
186+
logError(err)
166187
return
167188
}
168189

@@ -171,11 +192,26 @@ func execLiveSQL(w http.ResponseWriter, r *http.Request) {
171192
if err != nil {
172193
w.WriteHeader(http.StatusInternalServerError)
173194
fmt.Fprint(w, err.Error())
195+
logError(err)
174196
return
175197
}
198+
199+
// Store statement in sql terminal history
200+
err = com.LiveSqlHistoryAdd(loggedInUser, dbOwner, dbName, sql, com.Queried, z)
201+
if err != nil {
202+
w.WriteHeader(http.StatusInternalServerError)
203+
fmt.Fprint(w, err)
204+
}
176205
} else {
177206
// The SQL statement execution succeeded, so pass along the # of rows changed
178207
z = com.ExecuteResponseContainer{RowsChanged: rowsChanged, Status: "OK"}
208+
209+
// Store statement in sql terminal history
210+
err = com.LiveSqlHistoryAdd(loggedInUser, dbOwner, dbName, sql, com.Executed, z)
211+
if err != nil {
212+
w.WriteHeader(http.StatusInternalServerError)
213+
fmt.Fprint(w, err)
214+
}
179215
}
180216

181217
// Return the success message
@@ -184,8 +220,10 @@ func execLiveSQL(w http.ResponseWriter, r *http.Request) {
184220
log.Println(err)
185221
w.WriteHeader(http.StatusInternalServerError)
186222
fmt.Fprint(w, err)
223+
logError(err)
187224
return
188225
}
189226
fmt.Fprintf(w, "%s", jsonData)
227+
190228
return
191229
}

webui/jsx/sql-terminal.js

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ function SqlTerminalCommandOutput({state, data}) {
4040
if (state === "loading") {
4141
output = <span className="text-muted">loading...</span>;
4242
} else if (state === "error") {
43-
output = <span className="text-danger"><strong>error: </strong>{data}</span>;
43+
output = <span className="text-danger"><strong>error: </strong>{data.error}</span>;
4444
} else if (state === "executed") {
4545
output = <span className="text-info"><strong>done: </strong>{data.rows_changed + " row" + (data.rows_changed === 1 ? "" : "s") + " changed"}</span>;
4646
} else if (state === "queried") {
@@ -102,7 +102,7 @@ function SqlTerminalCommand({command}) {
102102
.catch(error => {
103103
error.text().then(text => {
104104
setState("error");
105-
setOutput(text);
105+
setOutput({error: text});
106106
});
107107
});
108108
}
@@ -118,7 +118,7 @@ function SqlTerminalCommand({command}) {
118118

119119
export default function SqlTerminal() {
120120
const [code, setCode] = React.useState("");
121-
const [recentCommands, setRecentCommands] = React.useState([]);
121+
const [recentCommands, setRecentCommands] = React.useState(historyData === null ? [] : historyData);
122122
const [executeOnEnter, setExecuteOnEnter] = React.useState(false);
123123
const [isDragging, setDragging] = React.useState(false); // We use this to distinguish simple clicks on the component from drag & drop movements. The latter are ignored to not interfere with selecting text for copy & paste.
124124

@@ -217,7 +217,7 @@ export default function SqlTerminal() {
217217
minHeight: "42px",
218218
}}
219219
/>
220-
<div className="input-group-btn dropup">
220+
<div className={recentCommands.length > 0 ? "input-group-btn dropup" : "input-group-btn"}>
221221
<button type="button" className="btn btn-primary" disabled={code.trim() === "" ? "disabled" : null} onClick={() => execute()} data-cy="executebtn"><i className="fa fa-play" /> Execute</button>
222222
<button type="button" className="btn btn-primary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" data-cy="dropdownbtn"><span className="caret"></span></button>
223223
<ul className="dropdown-menu dropdown-menu-right">

webui/templates/execute.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
<div id="sql-terminal"></div>
1010
</div>
1111
[[ template "script_db_header" . ]]
12+
<script>
13+
const historyData = [[ .SqlHistory ]];
14+
</script>
1215
[[ template "footer" . ]]
1316
</body>
1417
</html>

0 commit comments

Comments
 (0)