sqlite-hub 0.4.0 → 0.5.0

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.
@@ -1,7 +1,15 @@
1
1
  const fs = require("node:fs");
2
2
  const path = require("node:path");
3
3
  const Database = require("better-sqlite3");
4
- const { ValidationError } = require("../../utils/errors");
4
+ const { NotFoundError, ValidationError } = require("../../utils/errors");
5
+ const {
6
+ buildAutoTitle,
7
+ buildSqlPreview,
8
+ detectQueryType,
9
+ detectTables,
10
+ isDestructiveQuery,
11
+ normalizeSql,
12
+ } = require("./queryHistoryUtils");
5
13
 
6
14
  const DEFAULT_STATE = {
7
15
  recentConnections: [],
@@ -30,6 +38,7 @@ const CONNECTION_LOGO_EXTENSION_BY_FILE_EXTENSION = {
30
38
  ".png": "png",
31
39
  ".webp": "webp",
32
40
  };
41
+ const QUERY_HISTORY_MIGRATION_KEY = "queryHistoryV1Migrated";
33
42
 
34
43
  class AppStateStore {
35
44
  constructor(filePath, options = {}) {
@@ -54,6 +63,8 @@ class AppStateStore {
54
63
  if (!importedLegacyDatabase && this.shouldImportLegacyState()) {
55
64
  this.tryImportLegacyState();
56
65
  }
66
+
67
+ this.migrateLegacySqlHistory();
57
68
  }
58
69
 
59
70
  configureDatabase() {
@@ -103,6 +114,62 @@ class AppStateStore {
103
114
 
104
115
  CREATE INDEX IF NOT EXISTS idx_sql_history_executed_at
105
116
  ON sql_history(executedAt DESC, id ASC);
117
+
118
+ CREATE TABLE IF NOT EXISTS query_history (
119
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
120
+ database_key TEXT NOT NULL,
121
+ normalized_sql TEXT NOT NULL,
122
+ raw_sql TEXT NOT NULL,
123
+ title TEXT,
124
+ notes TEXT,
125
+ query_type TEXT NOT NULL DEFAULT 'other',
126
+ tables_detected TEXT NOT NULL DEFAULT '[]',
127
+ is_favorite INTEGER NOT NULL DEFAULT 0,
128
+ is_saved INTEGER NOT NULL DEFAULT 0,
129
+ is_destructive INTEGER NOT NULL DEFAULT 0,
130
+ use_count INTEGER NOT NULL DEFAULT 1,
131
+ first_executed_at TEXT NOT NULL,
132
+ last_used_at TEXT NOT NULL
133
+ );
134
+
135
+ CREATE TABLE IF NOT EXISTS query_runs (
136
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
137
+ history_id INTEGER NOT NULL,
138
+ executed_at TEXT NOT NULL,
139
+ duration_ms INTEGER,
140
+ row_count INTEGER,
141
+ status TEXT NOT NULL CHECK(status IN ('success', 'error')),
142
+ error_message TEXT,
143
+ affected_rows INTEGER,
144
+ FOREIGN KEY (history_id) REFERENCES query_history(id) ON DELETE CASCADE
145
+ );
146
+
147
+ CREATE INDEX IF NOT EXISTS idx_query_history_database_key
148
+ ON query_history(database_key);
149
+
150
+ CREATE INDEX IF NOT EXISTS idx_query_history_last_used_at
151
+ ON query_history(last_used_at DESC);
152
+
153
+ CREATE INDEX IF NOT EXISTS idx_query_history_is_saved
154
+ ON query_history(is_saved);
155
+
156
+ CREATE INDEX IF NOT EXISTS idx_query_history_is_favorite
157
+ ON query_history(is_favorite);
158
+
159
+ CREATE INDEX IF NOT EXISTS idx_query_history_query_type
160
+ ON query_history(query_type);
161
+
162
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_query_history_database_normalized_sql
163
+ ON query_history(database_key, normalized_sql);
164
+
165
+ CREATE INDEX IF NOT EXISTS idx_query_runs_history_id
166
+ ON query_runs(history_id);
167
+
168
+ CREATE INDEX IF NOT EXISTS idx_query_runs_executed_at
169
+ ON query_runs(executed_at DESC);
170
+
171
+ CREATE INDEX IF NOT EXISTS idx_query_runs_status
172
+ ON query_runs(status);
106
173
  `);
107
174
 
108
175
  const recentConnectionColumns = new Set(
@@ -407,6 +474,612 @@ class AppStateStore {
407
474
  .run(key, String(value));
408
475
  }
409
476
 
477
+ normalizeQueryHistoryText(value) {
478
+ const text = String(value ?? "").trim();
479
+ return text ? text : null;
480
+ }
481
+
482
+ normalizeQueryHistoryInteger(value) {
483
+ if (value === null || value === undefined || value === "") {
484
+ return null;
485
+ }
486
+
487
+ const numericValue = Number(value);
488
+ return Number.isFinite(numericValue) ? Math.round(numericValue) : null;
489
+ }
490
+
491
+ parseTablesDetected(value) {
492
+ if (Array.isArray(value)) {
493
+ return Array.from(
494
+ new Set(
495
+ value
496
+ .map((entry) => String(entry ?? "").trim())
497
+ .filter(Boolean)
498
+ )
499
+ );
500
+ }
501
+
502
+ if (!value) {
503
+ return [];
504
+ }
505
+
506
+ try {
507
+ return this.parseTablesDetected(JSON.parse(value));
508
+ } catch {
509
+ return [];
510
+ }
511
+ }
512
+
513
+ decorateQueryRun(row = {}) {
514
+ return {
515
+ id: Number(row.id),
516
+ historyId: Number(row.history_id ?? row.historyId),
517
+ executedAt: row.executed_at ?? row.executedAt ?? null,
518
+ durationMs: this.normalizeQueryHistoryInteger(row.duration_ms ?? row.durationMs),
519
+ rowCount: this.normalizeQueryHistoryInteger(row.row_count ?? row.rowCount),
520
+ status: row.status ?? "success",
521
+ errorMessage: this.normalizeQueryHistoryText(row.error_message ?? row.errorMessage),
522
+ affectedRows: this.normalizeQueryHistoryInteger(row.affected_rows ?? row.affectedRows),
523
+ };
524
+ }
525
+
526
+ decorateQueryHistoryRow(row = {}) {
527
+ const tablesDetected = this.parseTablesDetected(row.tables_detected ?? row.tablesDetected);
528
+ const queryType = row.query_type ?? row.queryType ?? "other";
529
+ const title = this.normalizeQueryHistoryText(row.title);
530
+ const notes = this.normalizeQueryHistoryText(row.notes);
531
+ const lastRun =
532
+ row.last_run_id ?? row.lastRunId
533
+ ? this.decorateQueryRun({
534
+ id: row.last_run_id ?? row.lastRunId,
535
+ history_id: row.id,
536
+ executed_at: row.last_run_executed_at ?? row.lastRunExecutedAt,
537
+ duration_ms: row.last_run_duration_ms ?? row.lastRunDurationMs,
538
+ row_count: row.last_run_row_count ?? row.lastRunRowCount,
539
+ status: row.last_run_status ?? row.lastRunStatus,
540
+ error_message: row.last_run_error_message ?? row.lastRunErrorMessage,
541
+ affected_rows: row.last_run_affected_rows ?? row.lastRunAffectedRows,
542
+ })
543
+ : null;
544
+
545
+ return {
546
+ id: Number(row.id),
547
+ databaseKey: row.database_key ?? row.databaseKey,
548
+ normalizedSql: row.normalized_sql ?? row.normalizedSql,
549
+ rawSql: row.raw_sql ?? row.rawSql,
550
+ title,
551
+ notes,
552
+ queryType,
553
+ tablesDetected,
554
+ isFavorite: Boolean(row.is_favorite ?? row.isFavorite),
555
+ isSaved: Boolean(row.is_saved ?? row.isSaved),
556
+ isDestructive: Boolean(row.is_destructive ?? row.isDestructive),
557
+ useCount: Number(row.use_count ?? row.useCount ?? 0),
558
+ firstExecutedAt: row.first_executed_at ?? row.firstExecutedAt ?? null,
559
+ lastUsedAt: row.last_used_at ?? row.lastUsedAt ?? null,
560
+ displayTitle:
561
+ title ||
562
+ buildAutoTitle(row.raw_sql ?? row.rawSql, {
563
+ queryType,
564
+ tablesDetected,
565
+ }),
566
+ previewSql: buildSqlPreview(row.raw_sql ?? row.rawSql),
567
+ lastRun,
568
+ };
569
+ }
570
+
571
+ buildQueryHistoryFilters({
572
+ databaseKey,
573
+ search,
574
+ queryType,
575
+ onlySaved = false,
576
+ onlyFavorites = false,
577
+ latestStatus = null,
578
+ } = {}) {
579
+ const clauses = [];
580
+ const params = [];
581
+ const normalizedSearch = String(search ?? "").trim().toLowerCase();
582
+
583
+ if (databaseKey) {
584
+ clauses.push("q.database_key = ?");
585
+ params.push(databaseKey);
586
+ }
587
+
588
+ if (normalizedSearch) {
589
+ const searchPattern = `%${normalizedSearch}%`;
590
+ clauses.push(`
591
+ (
592
+ LOWER(COALESCE(q.title, '')) LIKE ?
593
+ OR LOWER(q.raw_sql) LIKE ?
594
+ OR LOWER(COALESCE(q.notes, '')) LIKE ?
595
+ OR LOWER(q.normalized_sql) LIKE ?
596
+ OR LOWER(q.tables_detected) LIKE ?
597
+ )
598
+ `);
599
+ params.push(
600
+ searchPattern,
601
+ searchPattern,
602
+ searchPattern,
603
+ searchPattern,
604
+ searchPattern
605
+ );
606
+ }
607
+
608
+ if (queryType) {
609
+ clauses.push("q.query_type = ?");
610
+ params.push(queryType);
611
+ }
612
+
613
+ if (onlySaved) {
614
+ clauses.push("q.is_saved = 1");
615
+ }
616
+
617
+ if (onlyFavorites) {
618
+ clauses.push("q.is_favorite = 1");
619
+ }
620
+
621
+ if (latestStatus) {
622
+ clauses.push("latest.status = ?");
623
+ params.push(latestStatus);
624
+ }
625
+
626
+ return {
627
+ whereSql: clauses.length ? `WHERE ${clauses.join(" AND ")}` : "",
628
+ params,
629
+ };
630
+ }
631
+
632
+ getQueryHistoryOrderBy({ onlySaved = false, onlyFavorites = false, latestStatus = null } = {}) {
633
+ if (onlySaved || onlyFavorites) {
634
+ return "ORDER BY q.is_favorite DESC, q.last_used_at DESC, q.id DESC";
635
+ }
636
+
637
+ if (latestStatus === "error") {
638
+ return "ORDER BY latest.executed_at DESC, q.id DESC";
639
+ }
640
+
641
+ return "ORDER BY q.last_used_at DESC, q.id DESC";
642
+ }
643
+
644
+ buildQueryHistoryCollection({
645
+ databaseKey,
646
+ limit = 30,
647
+ offset = 0,
648
+ search = "",
649
+ queryType = null,
650
+ onlySaved = false,
651
+ onlyFavorites = false,
652
+ latestStatus = null,
653
+ } = {}) {
654
+ const normalizedLimit = Math.max(1, Math.min(100, Number(limit) || 30));
655
+ const normalizedOffset = Math.max(0, Number(offset) || 0);
656
+
657
+ if (!databaseKey) {
658
+ return {
659
+ items: [],
660
+ total: 0,
661
+ limit: normalizedLimit,
662
+ offset: normalizedOffset,
663
+ hasMore: false,
664
+ };
665
+ }
666
+
667
+ const baseFromSql = `
668
+ FROM query_history q
669
+ LEFT JOIN query_runs latest
670
+ ON latest.id = (
671
+ SELECT runs.id
672
+ FROM query_runs runs
673
+ WHERE runs.history_id = q.id
674
+ ORDER BY runs.executed_at DESC, runs.id DESC
675
+ LIMIT 1
676
+ )
677
+ `;
678
+ const { whereSql, params } = this.buildQueryHistoryFilters({
679
+ databaseKey,
680
+ search,
681
+ queryType,
682
+ onlySaved,
683
+ onlyFavorites,
684
+ latestStatus,
685
+ });
686
+ const orderBySql = this.getQueryHistoryOrderBy({
687
+ onlySaved,
688
+ onlyFavorites,
689
+ latestStatus,
690
+ });
691
+ const rows = this.db
692
+ .prepare(`
693
+ SELECT
694
+ q.id,
695
+ q.database_key,
696
+ q.normalized_sql,
697
+ q.raw_sql,
698
+ q.title,
699
+ q.notes,
700
+ q.query_type,
701
+ q.tables_detected,
702
+ q.is_favorite,
703
+ q.is_saved,
704
+ q.is_destructive,
705
+ q.use_count,
706
+ q.first_executed_at,
707
+ q.last_used_at,
708
+ latest.id AS last_run_id,
709
+ latest.executed_at AS last_run_executed_at,
710
+ latest.duration_ms AS last_run_duration_ms,
711
+ latest.row_count AS last_run_row_count,
712
+ latest.status AS last_run_status,
713
+ latest.error_message AS last_run_error_message,
714
+ latest.affected_rows AS last_run_affected_rows
715
+ ${baseFromSql}
716
+ ${whereSql}
717
+ ${orderBySql}
718
+ LIMIT ?
719
+ OFFSET ?
720
+ `)
721
+ .all(...params, normalizedLimit, normalizedOffset)
722
+ .map((row) => this.decorateQueryHistoryRow(row));
723
+ const countRow = this.db
724
+ .prepare(`
725
+ SELECT COUNT(*) AS count
726
+ ${baseFromSql}
727
+ ${whereSql}
728
+ `)
729
+ .get(...params);
730
+ const total = Number(countRow?.count ?? 0);
731
+
732
+ return {
733
+ items: rows,
734
+ total,
735
+ limit: normalizedLimit,
736
+ offset: normalizedOffset,
737
+ hasMore: normalizedOffset + rows.length < total,
738
+ };
739
+ }
740
+
741
+ migrateLegacySqlHistory() {
742
+ if (this.getMetaValue(QUERY_HISTORY_MIGRATION_KEY) === "1") {
743
+ return;
744
+ }
745
+
746
+ const hasLegacyTable = Boolean(
747
+ this.db
748
+ .prepare(
749
+ "SELECT 1 AS exists_flag FROM sqlite_master WHERE type = 'table' AND name = 'sql_history'"
750
+ )
751
+ .get()
752
+ );
753
+
754
+ if (!hasLegacyTable) {
755
+ this.setMetaValue(QUERY_HISTORY_MIGRATION_KEY, "1");
756
+ return;
757
+ }
758
+
759
+ const legacyCount = Number(
760
+ this.db.prepare("SELECT COUNT(*) AS count FROM sql_history").get()?.count ?? 0
761
+ );
762
+
763
+ if (!legacyCount) {
764
+ this.setMetaValue(QUERY_HISTORY_MIGRATION_KEY, "1");
765
+ return;
766
+ }
767
+
768
+ const currentHistoryCount = Number(
769
+ this.db.prepare("SELECT COUNT(*) AS count FROM query_history").get()?.count ?? 0
770
+ );
771
+ const currentRunCount = Number(
772
+ this.db.prepare("SELECT COUNT(*) AS count FROM query_runs").get()?.count ?? 0
773
+ );
774
+
775
+ if (currentHistoryCount > 0 || currentRunCount > 0) {
776
+ this.setMetaValue(QUERY_HISTORY_MIGRATION_KEY, "1");
777
+ return;
778
+ }
779
+
780
+ const legacyRows = this.db
781
+ .prepare(`
782
+ SELECT
783
+ connectionId,
784
+ connectionLabel,
785
+ sql,
786
+ timingMs,
787
+ rowCount,
788
+ affectedRowCount,
789
+ executedAt
790
+ FROM sql_history
791
+ ORDER BY executedAt ASC, id ASC
792
+ `)
793
+ .all();
794
+
795
+ this.db.transaction(() => {
796
+ legacyRows.forEach((entry) => {
797
+ const databaseKey =
798
+ this.normalizeQueryHistoryText(entry.connectionId) ??
799
+ this.normalizeQueryHistoryText(entry.connectionLabel) ??
800
+ "legacy:default";
801
+ const rawSql = String(entry.sql ?? "");
802
+
803
+ if (!normalizeSql(rawSql)) {
804
+ return;
805
+ }
806
+
807
+ this.recordQueryExecutionInTransaction({
808
+ databaseKey,
809
+ rawSql,
810
+ status: "success",
811
+ durationMs: entry.timingMs,
812
+ rowCount: entry.rowCount,
813
+ affectedRows: entry.affectedRowCount,
814
+ executedAt: entry.executedAt ?? new Date().toISOString(),
815
+ });
816
+ });
817
+ })();
818
+
819
+ this.setMetaValue(QUERY_HISTORY_MIGRATION_KEY, "1");
820
+ }
821
+
822
+ recordQueryExecution(entry = {}) {
823
+ return this.db.transaction(() => this.recordQueryExecutionInTransaction(entry))();
824
+ }
825
+
826
+ recordQueryExecutionInTransaction({
827
+ databaseKey,
828
+ rawSql,
829
+ status,
830
+ durationMs = null,
831
+ rowCount = null,
832
+ affectedRows = null,
833
+ errorMessage = null,
834
+ executedAt = null,
835
+ } = {}) {
836
+ const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
837
+ const normalizedRawSql = String(rawSql ?? "");
838
+ const normalizedSql = normalizeSql(normalizedRawSql);
839
+
840
+ if (!normalizedDatabaseKey) {
841
+ throw new ValidationError("Query history requires a database key.");
842
+ }
843
+
844
+ if (!normalizedSql) {
845
+ throw new ValidationError("Query history requires executable SQL.");
846
+ }
847
+
848
+ if (!["success", "error"].includes(status)) {
849
+ throw new ValidationError(`Unsupported query run status: ${status}`);
850
+ }
851
+
852
+ const queryType = detectQueryType(normalizedRawSql);
853
+ const tablesDetected = detectTables(normalizedRawSql);
854
+ const timestamp = this.normalizeQueryHistoryText(executedAt) ?? new Date().toISOString();
855
+ const destructive = isDestructiveQuery(normalizedRawSql) ? 1 : 0;
856
+ const serializedTables = JSON.stringify(tablesDetected);
857
+ const existing = this.db
858
+ .prepare(
859
+ `
860
+ SELECT id
861
+ FROM query_history
862
+ WHERE database_key = ? AND normalized_sql = ?
863
+ `
864
+ )
865
+ .get(normalizedDatabaseKey, normalizedSql);
866
+ let historyId = Number(existing?.id ?? 0);
867
+
868
+ if (historyId) {
869
+ this.db
870
+ .prepare(`
871
+ UPDATE query_history
872
+ SET
873
+ raw_sql = ?,
874
+ query_type = ?,
875
+ tables_detected = ?,
876
+ is_destructive = ?,
877
+ use_count = use_count + 1,
878
+ last_used_at = ?
879
+ WHERE id = ?
880
+ `)
881
+ .run(
882
+ normalizedRawSql,
883
+ queryType,
884
+ serializedTables,
885
+ destructive,
886
+ timestamp,
887
+ historyId
888
+ );
889
+ } else {
890
+ const insertResult = this.db
891
+ .prepare(`
892
+ INSERT INTO query_history (
893
+ database_key,
894
+ normalized_sql,
895
+ raw_sql,
896
+ query_type,
897
+ tables_detected,
898
+ is_destructive,
899
+ use_count,
900
+ first_executed_at,
901
+ last_used_at
902
+ )
903
+ VALUES (?, ?, ?, ?, ?, ?, 1, ?, ?)
904
+ `)
905
+ .run(
906
+ normalizedDatabaseKey,
907
+ normalizedSql,
908
+ normalizedRawSql,
909
+ queryType,
910
+ serializedTables,
911
+ destructive,
912
+ timestamp,
913
+ timestamp
914
+ );
915
+ historyId = Number(insertResult.lastInsertRowid);
916
+ }
917
+
918
+ this.db
919
+ .prepare(`
920
+ INSERT INTO query_runs (
921
+ history_id,
922
+ executed_at,
923
+ duration_ms,
924
+ row_count,
925
+ status,
926
+ error_message,
927
+ affected_rows
928
+ )
929
+ VALUES (?, ?, ?, ?, ?, ?, ?)
930
+ `)
931
+ .run(
932
+ historyId,
933
+ timestamp,
934
+ this.normalizeQueryHistoryInteger(durationMs),
935
+ this.normalizeQueryHistoryInteger(rowCount),
936
+ status,
937
+ this.normalizeQueryHistoryText(errorMessage),
938
+ this.normalizeQueryHistoryInteger(affectedRows)
939
+ );
940
+
941
+ return historyId;
942
+ }
943
+
944
+ getRecentQueries(options = {}) {
945
+ return this.buildQueryHistoryCollection(options);
946
+ }
947
+
948
+ getFailedQueries(options = {}) {
949
+ return this.buildQueryHistoryCollection({
950
+ ...options,
951
+ latestStatus: "error",
952
+ });
953
+ }
954
+
955
+ getQueryHistoryItemById(historyId) {
956
+ const row = this.db
957
+ .prepare(`
958
+ SELECT
959
+ q.id,
960
+ q.database_key,
961
+ q.normalized_sql,
962
+ q.raw_sql,
963
+ q.title,
964
+ q.notes,
965
+ q.query_type,
966
+ q.tables_detected,
967
+ q.is_favorite,
968
+ q.is_saved,
969
+ q.is_destructive,
970
+ q.use_count,
971
+ q.first_executed_at,
972
+ q.last_used_at,
973
+ latest.id AS last_run_id,
974
+ latest.executed_at AS last_run_executed_at,
975
+ latest.duration_ms AS last_run_duration_ms,
976
+ latest.row_count AS last_run_row_count,
977
+ latest.status AS last_run_status,
978
+ latest.error_message AS last_run_error_message,
979
+ latest.affected_rows AS last_run_affected_rows
980
+ FROM query_history q
981
+ LEFT JOIN query_runs latest
982
+ ON latest.id = (
983
+ SELECT runs.id
984
+ FROM query_runs runs
985
+ WHERE runs.history_id = q.id
986
+ ORDER BY runs.executed_at DESC, runs.id DESC
987
+ LIMIT 1
988
+ )
989
+ WHERE q.id = ?
990
+ `)
991
+ .get(Number(historyId));
992
+
993
+ if (!row) {
994
+ throw new NotFoundError(`Query history item not found: ${historyId}`);
995
+ }
996
+
997
+ return this.decorateQueryHistoryRow(row);
998
+ }
999
+
1000
+ getQueryRunsByHistoryId(historyId, limit = 8) {
1001
+ const normalizedLimit = Math.max(1, Math.min(50, Number(limit) || 8));
1002
+
1003
+ return this.db
1004
+ .prepare(`
1005
+ SELECT
1006
+ id,
1007
+ history_id,
1008
+ executed_at,
1009
+ duration_ms,
1010
+ row_count,
1011
+ status,
1012
+ error_message,
1013
+ affected_rows
1014
+ FROM query_runs
1015
+ WHERE history_id = ?
1016
+ ORDER BY executed_at DESC, id DESC
1017
+ LIMIT ?
1018
+ `)
1019
+ .all(Number(historyId), normalizedLimit)
1020
+ .map((row) => this.decorateQueryRun(row));
1021
+ }
1022
+
1023
+ updateQueryHistoryField(historyId, fieldName, value) {
1024
+ const result = this.db
1025
+ .prepare(`UPDATE query_history SET ${fieldName} = ? WHERE id = ?`)
1026
+ .run(value, Number(historyId));
1027
+
1028
+ if (!result.changes) {
1029
+ throw new NotFoundError(`Query history item not found: ${historyId}`);
1030
+ }
1031
+
1032
+ return this.getQueryHistoryItemById(historyId);
1033
+ }
1034
+
1035
+ toggleFavorite(historyId, nextValue) {
1036
+ return this.updateQueryHistoryField(historyId, "is_favorite", nextValue ? 1 : 0);
1037
+ }
1038
+
1039
+ toggleSaved(historyId, nextValue) {
1040
+ return this.updateQueryHistoryField(historyId, "is_saved", nextValue ? 1 : 0);
1041
+ }
1042
+
1043
+ renameQuery(historyId, title) {
1044
+ return this.updateQueryHistoryField(
1045
+ historyId,
1046
+ "title",
1047
+ this.normalizeQueryHistoryText(title)
1048
+ );
1049
+ }
1050
+
1051
+ updateQueryNotes(historyId, notes) {
1052
+ return this.updateQueryHistoryField(
1053
+ historyId,
1054
+ "notes",
1055
+ this.normalizeQueryHistoryText(notes)
1056
+ );
1057
+ }
1058
+
1059
+ deleteQueryHistoryItem(historyId) {
1060
+ const result = this.db
1061
+ .prepare("DELETE FROM query_history WHERE id = ?")
1062
+ .run(Number(historyId));
1063
+
1064
+ if (!result.changes) {
1065
+ throw new NotFoundError(`Query history item not found: ${historyId}`);
1066
+ }
1067
+
1068
+ return true;
1069
+ }
1070
+
1071
+ clearQueryHistoryForDatabase(databaseKey) {
1072
+ const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
1073
+
1074
+ if (!normalizedDatabaseKey) {
1075
+ return 0;
1076
+ }
1077
+
1078
+ return this.db
1079
+ .prepare("DELETE FROM query_history WHERE database_key = ?")
1080
+ .run(normalizedDatabaseKey).changes;
1081
+ }
1082
+
410
1083
  trimRecentConnections() {
411
1084
  const maxRecentConnections = Number(
412
1085
  this.getSettings().maxRecentConnections ?? DEFAULT_STATE.settings.maxRecentConnections