sqlite-hub 0.9.0 → 0.9.3
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.
- package/changelog.md +12 -0
- package/frontend/index.html +21 -20
- package/frontend/js/app.js +386 -8
- package/frontend/js/components/modal.js +13 -4
- package/frontend/js/components/queryHistoryDetail.js +14 -2
- package/frontend/js/components/queryHistoryPanel.js +13 -9
- package/frontend/js/components/rowEditorPanel.js +48 -25
- package/frontend/js/lib/queryChartOptions.js +5 -1
- package/frontend/js/lib/queryCharts.js +50 -2
- package/frontend/js/store.js +177 -21
- package/frontend/js/views/charts.js +103 -30
- package/frontend/js/views/data.js +31 -1
- package/frontend/js/views/editor.js +1 -0
- package/frontend/js/views/mediaTagging.js +146 -76
- package/frontend/styles/base.css +57 -13
- package/frontend/styles/components.css +172 -96
- package/frontend/styles/layout.css +1 -1
- package/frontend/styles/structure-graph.css +11 -11
- package/frontend/styles/tokens.css +47 -4
- package/frontend/styles/utilities.css +21 -0
- package/frontend/styles/views.css +218 -96
- package/package.json +1 -1
- package/server/services/storage/appStateStore.js +9 -6
- package/server/services/storage/queryHistoryChartUtils.js +19 -2
- package/server/services/storage/queryHistoryUtils.js +165 -2
- package/shortkeys.md +5 -0
|
@@ -100,8 +100,11 @@ function normalizeMediaTaggingConfigRecord(config = {}) {
|
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
-
function
|
|
104
|
-
return
|
|
103
|
+
function isChartCompatibleQuery(queryType, rawSql = "") {
|
|
104
|
+
return (
|
|
105
|
+
String(queryType ?? "").trim().toLowerCase() === "select" ||
|
|
106
|
+
detectQueryType(rawSql) === "select"
|
|
107
|
+
);
|
|
105
108
|
}
|
|
106
109
|
|
|
107
110
|
class AppStateStore {
|
|
@@ -752,7 +755,7 @@ class AppStateStore {
|
|
|
752
755
|
previewSql: buildSqlPreview(row.raw_sql ?? row.rawSql),
|
|
753
756
|
lastRun,
|
|
754
757
|
chartsEligible:
|
|
755
|
-
|
|
758
|
+
isChartCompatibleQuery(queryType, row.raw_sql ?? row.rawSql) &&
|
|
756
759
|
(!lastRun || String(lastRun.status ?? "").trim().toLowerCase() !== "error"),
|
|
757
760
|
};
|
|
758
761
|
}
|
|
@@ -1252,12 +1255,12 @@ class AppStateStore {
|
|
|
1252
1255
|
LIMIT 1
|
|
1253
1256
|
)
|
|
1254
1257
|
WHERE q.database_key = ?
|
|
1255
|
-
AND q.query_type = 'select'
|
|
1256
1258
|
AND COALESCE(latest.status, 'success') != 'error'
|
|
1257
1259
|
ORDER BY q.last_used_at DESC, q.id DESC
|
|
1258
1260
|
`)
|
|
1259
1261
|
.all(normalizedDatabaseKey)
|
|
1260
|
-
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1262
|
+
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1263
|
+
.filter((item) => item.chartsEligible);
|
|
1261
1264
|
}
|
|
1262
1265
|
|
|
1263
1266
|
getQueryHistoryItemById(historyId) {
|
|
@@ -1367,7 +1370,7 @@ class AppStateStore {
|
|
|
1367
1370
|
getChartQueryHistoryItemForDatabase(historyId, databaseKey) {
|
|
1368
1371
|
const item = this.getQueryHistoryItemForDatabase(historyId, databaseKey);
|
|
1369
1372
|
|
|
1370
|
-
if (!
|
|
1373
|
+
if (!isChartCompatibleQuery(item.queryType, item.rawSql)) {
|
|
1371
1374
|
throw new ValidationError("Only SELECT queries can be opened in Charts.");
|
|
1372
1375
|
}
|
|
1373
1376
|
|
|
@@ -53,7 +53,23 @@ function normalizeRequiredColumn(value, label) {
|
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function normalizeSortDirection(value, fallback = "asc") {
|
|
56
|
-
|
|
56
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
57
|
+
|
|
58
|
+
if (normalized === "asc" || normalized === "desc") {
|
|
59
|
+
return normalized;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return fallback;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function normalizeBarSortBy(value, fallback = "x") {
|
|
66
|
+
const normalized = String(value ?? "").trim().toLowerCase();
|
|
67
|
+
|
|
68
|
+
if (normalized === "x" || normalized === "y") {
|
|
69
|
+
return normalized;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return fallback;
|
|
57
73
|
}
|
|
58
74
|
|
|
59
75
|
function normalizeChartConfig(chartType, config = {}) {
|
|
@@ -67,7 +83,8 @@ function normalizeChartConfig(chartType, config = {}) {
|
|
|
67
83
|
y_column: normalizeRequiredColumn(source.y_column, "Bar chart y column"),
|
|
68
84
|
show_legend: normalizeBooleanFlag(source.show_legend, true),
|
|
69
85
|
show_labels: normalizeBooleanFlag(source.show_labels, false),
|
|
70
|
-
|
|
86
|
+
sort_by: normalizeBarSortBy(source.sort_by, "y"),
|
|
87
|
+
sort_direction: normalizeSortDirection(source.sort_direction, "desc"),
|
|
71
88
|
};
|
|
72
89
|
case "line":
|
|
73
90
|
return {
|
|
@@ -37,11 +37,174 @@ function getLeadingKeywords(sql = "", limit = 3) {
|
|
|
37
37
|
return matches.slice(0, limit).map((token) => token.toLowerCase());
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
function readWordAt(value = "", index = 0) {
|
|
41
|
+
const match = String(value).slice(index).match(/^[A-Za-z]+/);
|
|
42
|
+
return match ? match[0].toLowerCase() : "";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function skipWhitespace(value = "", index = 0) {
|
|
46
|
+
let cursor = index;
|
|
47
|
+
|
|
48
|
+
while (cursor < value.length && /\s/.test(value[cursor])) {
|
|
49
|
+
cursor += 1;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return cursor;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function advanceQuotedSql(value = "", index = 0) {
|
|
56
|
+
const quote = value[index];
|
|
57
|
+
const closingQuote = quote === "[" ? "]" : quote;
|
|
58
|
+
let cursor = index + 1;
|
|
59
|
+
|
|
60
|
+
while (cursor < value.length) {
|
|
61
|
+
if (value[cursor] === closingQuote) {
|
|
62
|
+
if ((quote === "'" || quote === '"') && value[cursor + 1] === closingQuote) {
|
|
63
|
+
cursor += 2;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return cursor + 1;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
cursor += 1;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return value.length;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function isSqlWordBoundary(value = "", index = 0) {
|
|
77
|
+
return index < 0 || index >= value.length || !/[A-Za-z0-9_]/.test(value[index]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function findTopLevelWord(value = "", word = "", startIndex = 0) {
|
|
81
|
+
const normalizedWord = String(word).toLowerCase();
|
|
82
|
+
let cursor = startIndex;
|
|
83
|
+
let depth = 0;
|
|
84
|
+
|
|
85
|
+
while (cursor < value.length) {
|
|
86
|
+
const char = value[cursor];
|
|
87
|
+
|
|
88
|
+
if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
89
|
+
cursor = advanceQuotedSql(value, cursor);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (char === "(") {
|
|
94
|
+
depth += 1;
|
|
95
|
+
cursor += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (char === ")") {
|
|
100
|
+
depth = Math.max(0, depth - 1);
|
|
101
|
+
cursor += 1;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (
|
|
106
|
+
depth === 0 &&
|
|
107
|
+
value.slice(cursor, cursor + normalizedWord.length).toLowerCase() === normalizedWord &&
|
|
108
|
+
isSqlWordBoundary(value, cursor - 1) &&
|
|
109
|
+
isSqlWordBoundary(value, cursor + normalizedWord.length)
|
|
110
|
+
) {
|
|
111
|
+
return cursor;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
cursor += 1;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function findMatchingParen(value = "", openIndex = 0) {
|
|
121
|
+
let cursor = openIndex;
|
|
122
|
+
let depth = 0;
|
|
123
|
+
|
|
124
|
+
while (cursor < value.length) {
|
|
125
|
+
const char = value[cursor];
|
|
126
|
+
|
|
127
|
+
if (char === "'" || char === '"' || char === "`" || char === "[") {
|
|
128
|
+
cursor = advanceQuotedSql(value, cursor);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (char === "(") {
|
|
133
|
+
depth += 1;
|
|
134
|
+
} else if (char === ")") {
|
|
135
|
+
depth -= 1;
|
|
136
|
+
|
|
137
|
+
if (depth === 0) {
|
|
138
|
+
return cursor;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
cursor += 1;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return -1;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function resolveCteStatementKeyword(sql = "") {
|
|
149
|
+
const stripped = stripBlockComments(stripLineComments(sql)).trim();
|
|
150
|
+
let cursor = skipWhitespace(stripped, 0);
|
|
151
|
+
|
|
152
|
+
if (readWordAt(stripped, cursor) !== "with") {
|
|
153
|
+
return "";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
cursor += 4;
|
|
157
|
+
cursor = skipWhitespace(stripped, cursor);
|
|
158
|
+
|
|
159
|
+
if (readWordAt(stripped, cursor) === "recursive") {
|
|
160
|
+
cursor += "recursive".length;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
while (cursor < stripped.length) {
|
|
164
|
+
const asIndex = findTopLevelWord(stripped, "as", cursor);
|
|
165
|
+
|
|
166
|
+
if (asIndex < 0) {
|
|
167
|
+
return "with";
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
cursor = skipWhitespace(stripped, asIndex + 2);
|
|
171
|
+
|
|
172
|
+
if (readWordAt(stripped, cursor) === "not") {
|
|
173
|
+
cursor = skipWhitespace(stripped, cursor + 3);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (readWordAt(stripped, cursor) === "materialized") {
|
|
177
|
+
cursor = skipWhitespace(stripped, cursor + "materialized".length);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (stripped[cursor] !== "(") {
|
|
181
|
+
return "with";
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const closeIndex = findMatchingParen(stripped, cursor);
|
|
185
|
+
|
|
186
|
+
if (closeIndex < 0) {
|
|
187
|
+
return "with";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
cursor = skipWhitespace(stripped, closeIndex + 1);
|
|
191
|
+
|
|
192
|
+
if (stripped[cursor] === ",") {
|
|
193
|
+
cursor = skipWhitespace(stripped, cursor + 1);
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return readWordAt(stripped, cursor) || "with";
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return "with";
|
|
201
|
+
}
|
|
202
|
+
|
|
40
203
|
function resolveEffectiveKeyword(sql = "") {
|
|
41
|
-
const [firstKeyword, secondKeyword
|
|
204
|
+
const [firstKeyword, secondKeyword] = getLeadingKeywords(sql, 3);
|
|
42
205
|
|
|
43
206
|
if (firstKeyword === "with") {
|
|
44
|
-
return
|
|
207
|
+
return resolveCteStatementKeyword(sql);
|
|
45
208
|
}
|
|
46
209
|
|
|
47
210
|
if (firstKeyword === "explain") {
|