sqlite-hub 0.8.7 → 0.9.1
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 +16 -0
- package/frontend/index.html +1 -1
- package/frontend/js/api.js +67 -0
- package/frontend/js/app.js +1554 -999
- package/frontend/js/components/modal.js +208 -3
- package/frontend/js/components/queryEditor.js +9 -4
- package/frontend/js/components/queryHistoryDetail.js +34 -12
- package/frontend/js/components/queryHistoryPanel.js +8 -11
- package/frontend/js/components/rowEditorPanel.js +26 -5
- package/frontend/js/components/sidebar.js +49 -5
- package/frontend/js/components/structureGraph.js +614 -632
- package/frontend/js/components/tableDesignerSqlPreview.js +24 -25
- package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
- package/frontend/js/router.js +81 -71
- package/frontend/js/store.js +3140 -2179
- package/frontend/js/views/charts.js +2 -2
- package/frontend/js/views/data.js +28 -14
- package/frontend/js/views/editor.js +172 -177
- package/frontend/js/views/mediaTagging.js +931 -0
- package/frontend/styles/base.css +11 -1
- package/frontend/styles/components.css +54 -6
- package/frontend/styles/views.css +910 -0
- package/package.json +1 -1
- package/server/routes/charts.js +4 -1
- package/server/routes/data.js +14 -0
- package/server/routes/mediaTagging.js +166 -0
- package/server/routes/sql.js +1 -0
- package/server/server.js +4 -0
- package/server/services/sqlite/dataBrowserService.js +25 -0
- package/server/services/sqlite/exportService.js +31 -1
- package/server/services/sqlite/mediaTaggingService.js +1689 -0
- package/server/services/storage/appStateStore.js +325 -3
- package/server/services/storage/queryHistoryUtils.js +165 -2
- package/shortkeys.md +5 -0
|
@@ -46,6 +46,66 @@ const CONNECTION_LOGO_EXTENSION_BY_FILE_EXTENSION = {
|
|
|
46
46
|
".webp": "webp",
|
|
47
47
|
};
|
|
48
48
|
const QUERY_HISTORY_MIGRATION_KEY = "queryHistoryV1Migrated";
|
|
49
|
+
const MEDIA_TAGGING_CONFIG_FIELDS = [
|
|
50
|
+
{
|
|
51
|
+
column: "tag_table",
|
|
52
|
+
property: "tagTable",
|
|
53
|
+
definition: "tag_table TEXT NOT NULL DEFAULT ''",
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
column: "media_table",
|
|
57
|
+
property: "mediaTable",
|
|
58
|
+
definition: "media_table TEXT NOT NULL DEFAULT ''",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
column: "path_column",
|
|
62
|
+
property: "pathColumn",
|
|
63
|
+
definition: "path_column TEXT NOT NULL DEFAULT ''",
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
column: "tagged_column",
|
|
67
|
+
property: "taggedColumn",
|
|
68
|
+
definition: "tagged_column TEXT NOT NULL DEFAULT ''",
|
|
69
|
+
},
|
|
70
|
+
{
|
|
71
|
+
column: "untagged_query",
|
|
72
|
+
property: "untaggedQuery",
|
|
73
|
+
definition: "untagged_query TEXT NOT NULL DEFAULT ''",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
column: "tagged_query",
|
|
77
|
+
property: "taggedQuery",
|
|
78
|
+
definition: "tagged_query TEXT NOT NULL DEFAULT ''",
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
column: "mapping_table",
|
|
82
|
+
property: "mappingTable",
|
|
83
|
+
definition: "mapping_table TEXT NOT NULL DEFAULT ''",
|
|
84
|
+
},
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
function normalizeMediaTaggingConfigValue(value) {
|
|
88
|
+
return String(value ?? "").trim();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function normalizeMediaTaggingConfigRecord(config = {}) {
|
|
92
|
+
return {
|
|
93
|
+
tagTable: normalizeMediaTaggingConfigValue(config.tagTable),
|
|
94
|
+
mediaTable: normalizeMediaTaggingConfigValue(config.mediaTable),
|
|
95
|
+
pathColumn: normalizeMediaTaggingConfigValue(config.pathColumn),
|
|
96
|
+
taggedColumn: normalizeMediaTaggingConfigValue(config.taggedColumn),
|
|
97
|
+
untaggedQuery: String(config.untaggedQuery ?? "").trim(),
|
|
98
|
+
taggedQuery: String(config.taggedQuery ?? "").trim(),
|
|
99
|
+
mappingTable: normalizeMediaTaggingConfigValue(config.mappingTable),
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function isChartCompatibleQuery(queryType, rawSql = "") {
|
|
104
|
+
return (
|
|
105
|
+
String(queryType ?? "").trim().toLowerCase() === "select" ||
|
|
106
|
+
detectQueryType(rawSql) === "select"
|
|
107
|
+
);
|
|
108
|
+
}
|
|
49
109
|
|
|
50
110
|
class AppStateStore {
|
|
51
111
|
constructor(filePath, options = {}) {
|
|
@@ -200,6 +260,18 @@ class AppStateStore {
|
|
|
200
260
|
|
|
201
261
|
CREATE INDEX IF NOT EXISTS idx_query_history_chart_chart_type
|
|
202
262
|
ON query_history_chart(chart_type);
|
|
263
|
+
|
|
264
|
+
CREATE TABLE IF NOT EXISTS media_tagging_config (
|
|
265
|
+
database_key TEXT PRIMARY KEY,
|
|
266
|
+
tag_table TEXT NOT NULL DEFAULT '',
|
|
267
|
+
media_table TEXT NOT NULL DEFAULT '',
|
|
268
|
+
path_column TEXT NOT NULL DEFAULT '',
|
|
269
|
+
tagged_column TEXT NOT NULL DEFAULT '',
|
|
270
|
+
untagged_query TEXT NOT NULL DEFAULT '',
|
|
271
|
+
tagged_query TEXT NOT NULL DEFAULT '',
|
|
272
|
+
mapping_table TEXT NOT NULL DEFAULT '',
|
|
273
|
+
updated_at TEXT NOT NULL
|
|
274
|
+
);
|
|
203
275
|
`);
|
|
204
276
|
|
|
205
277
|
const recentConnectionColumns = new Set(
|
|
@@ -212,6 +284,8 @@ class AppStateStore {
|
|
|
212
284
|
if (!recentConnectionColumns.has("logoPath")) {
|
|
213
285
|
this.db.exec("ALTER TABLE recent_connections ADD COLUMN logoPath TEXT");
|
|
214
286
|
}
|
|
287
|
+
|
|
288
|
+
this.ensureMediaTaggingConfigSchema();
|
|
215
289
|
}
|
|
216
290
|
|
|
217
291
|
seedDefaultSettings() {
|
|
@@ -481,6 +555,85 @@ class AppStateStore {
|
|
|
481
555
|
}
|
|
482
556
|
}
|
|
483
557
|
|
|
558
|
+
ensureMediaTaggingConfigSchema() {
|
|
559
|
+
const columns = this.db
|
|
560
|
+
.prepare("PRAGMA table_info(media_tagging_config)")
|
|
561
|
+
.all()
|
|
562
|
+
.map((column) => column.name);
|
|
563
|
+
const columnSet = new Set(columns);
|
|
564
|
+
|
|
565
|
+
this.mediaTaggingConfigHasLegacyJsonColumn = columnSet.has("config_json");
|
|
566
|
+
|
|
567
|
+
for (const field of MEDIA_TAGGING_CONFIG_FIELDS) {
|
|
568
|
+
if (!columnSet.has(field.column)) {
|
|
569
|
+
this.db.exec(`ALTER TABLE media_tagging_config ADD COLUMN ${field.definition}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
if (this.mediaTaggingConfigHasLegacyJsonColumn) {
|
|
574
|
+
this.backfillMediaTaggingConfigColumnsFromLegacyJson();
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
backfillMediaTaggingConfigColumnsFromLegacyJson() {
|
|
579
|
+
const rows = this.db
|
|
580
|
+
.prepare(
|
|
581
|
+
`
|
|
582
|
+
SELECT database_key, config_json
|
|
583
|
+
FROM media_tagging_config
|
|
584
|
+
WHERE config_json IS NOT NULL
|
|
585
|
+
`
|
|
586
|
+
)
|
|
587
|
+
.all();
|
|
588
|
+
|
|
589
|
+
if (!rows.length) {
|
|
590
|
+
return;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
const updateStatement = this.db.prepare(`
|
|
594
|
+
UPDATE media_tagging_config
|
|
595
|
+
SET
|
|
596
|
+
tag_table = COALESCE(NULLIF(tag_table, ''), ?),
|
|
597
|
+
media_table = COALESCE(NULLIF(media_table, ''), ?),
|
|
598
|
+
path_column = COALESCE(NULLIF(path_column, ''), ?),
|
|
599
|
+
tagged_column = COALESCE(NULLIF(tagged_column, ''), ?),
|
|
600
|
+
untagged_query = COALESCE(NULLIF(untagged_query, ''), ?),
|
|
601
|
+
tagged_query = COALESCE(NULLIF(tagged_query, ''), ?),
|
|
602
|
+
mapping_table = COALESCE(NULLIF(mapping_table, ''), ?)
|
|
603
|
+
WHERE database_key = ?
|
|
604
|
+
`);
|
|
605
|
+
|
|
606
|
+
this.db.transaction(() => {
|
|
607
|
+
for (const row of rows) {
|
|
608
|
+
const parsedConfig = this.parseStoredValue(row.config_json);
|
|
609
|
+
const normalizedConfig = normalizeMediaTaggingConfigRecord(
|
|
610
|
+
parsedConfig && typeof parsedConfig === "object" && !Array.isArray(parsedConfig)
|
|
611
|
+
? parsedConfig
|
|
612
|
+
: {}
|
|
613
|
+
);
|
|
614
|
+
|
|
615
|
+
updateStatement.run(
|
|
616
|
+
normalizedConfig.tagTable,
|
|
617
|
+
normalizedConfig.mediaTable,
|
|
618
|
+
normalizedConfig.pathColumn,
|
|
619
|
+
normalizedConfig.taggedColumn,
|
|
620
|
+
normalizedConfig.untaggedQuery,
|
|
621
|
+
normalizedConfig.taggedQuery,
|
|
622
|
+
normalizedConfig.mappingTable,
|
|
623
|
+
row.database_key
|
|
624
|
+
);
|
|
625
|
+
}
|
|
626
|
+
})();
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
buildMediaTaggingConfigRecord(row = {}) {
|
|
630
|
+
return normalizeMediaTaggingConfigRecord(
|
|
631
|
+
Object.fromEntries(
|
|
632
|
+
MEDIA_TAGGING_CONFIG_FIELDS.map((field) => [field.property, row[field.column] ?? null])
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
|
|
484
637
|
getMetaValue(key) {
|
|
485
638
|
const row = this.db
|
|
486
639
|
.prepare("SELECT value FROM app_meta WHERE key = ?")
|
|
@@ -601,6 +754,9 @@ class AppStateStore {
|
|
|
601
754
|
}),
|
|
602
755
|
previewSql: buildSqlPreview(row.raw_sql ?? row.rawSql),
|
|
603
756
|
lastRun,
|
|
757
|
+
chartsEligible:
|
|
758
|
+
isChartCompatibleQuery(queryType, row.raw_sql ?? row.rawSql) &&
|
|
759
|
+
(!lastRun || String(lastRun.status ?? "").trim().toLowerCase() !== "error"),
|
|
604
760
|
};
|
|
605
761
|
}
|
|
606
762
|
|
|
@@ -661,6 +817,7 @@ class AppStateStore {
|
|
|
661
817
|
search,
|
|
662
818
|
queryType,
|
|
663
819
|
onlySaved = false,
|
|
820
|
+
onlyUnsaved = false,
|
|
664
821
|
onlyFavorites = false,
|
|
665
822
|
latestStatus = null,
|
|
666
823
|
} = {}) {
|
|
@@ -702,6 +859,10 @@ class AppStateStore {
|
|
|
702
859
|
clauses.push("q.is_saved = 1");
|
|
703
860
|
}
|
|
704
861
|
|
|
862
|
+
if (onlyUnsaved) {
|
|
863
|
+
clauses.push("q.is_saved = 0");
|
|
864
|
+
}
|
|
865
|
+
|
|
705
866
|
if (onlyFavorites) {
|
|
706
867
|
clauses.push("q.is_favorite = 1");
|
|
707
868
|
}
|
|
@@ -736,6 +897,7 @@ class AppStateStore {
|
|
|
736
897
|
search = "",
|
|
737
898
|
queryType = null,
|
|
738
899
|
onlySaved = false,
|
|
900
|
+
onlyUnsaved = false,
|
|
739
901
|
onlyFavorites = false,
|
|
740
902
|
latestStatus = null,
|
|
741
903
|
} = {}) {
|
|
@@ -768,6 +930,7 @@ class AppStateStore {
|
|
|
768
930
|
search,
|
|
769
931
|
queryType,
|
|
770
932
|
onlySaved,
|
|
933
|
+
onlyUnsaved,
|
|
771
934
|
onlyFavorites,
|
|
772
935
|
latestStatus,
|
|
773
936
|
});
|
|
@@ -1096,7 +1259,8 @@ class AppStateStore {
|
|
|
1096
1259
|
ORDER BY q.last_used_at DESC, q.id DESC
|
|
1097
1260
|
`)
|
|
1098
1261
|
.all(normalizedDatabaseKey)
|
|
1099
|
-
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1262
|
+
.map((row) => this.decorateQueryHistoryRow(row))
|
|
1263
|
+
.filter((item) => item.chartsEligible);
|
|
1100
1264
|
}
|
|
1101
1265
|
|
|
1102
1266
|
getQueryHistoryItemById(historyId) {
|
|
@@ -1144,6 +1308,54 @@ class AppStateStore {
|
|
|
1144
1308
|
return this.decorateQueryHistoryRow(row);
|
|
1145
1309
|
}
|
|
1146
1310
|
|
|
1311
|
+
findQueryHistoryItemBySql(databaseKey, rawSql) {
|
|
1312
|
+
const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
|
|
1313
|
+
const normalizedSql = normalizeSql(rawSql);
|
|
1314
|
+
|
|
1315
|
+
if (!normalizedDatabaseKey || !normalizedSql) {
|
|
1316
|
+
return null;
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
const row = this.db
|
|
1320
|
+
.prepare(`
|
|
1321
|
+
SELECT
|
|
1322
|
+
q.id,
|
|
1323
|
+
q.database_key,
|
|
1324
|
+
q.normalized_sql,
|
|
1325
|
+
q.raw_sql,
|
|
1326
|
+
q.title,
|
|
1327
|
+
q.notes,
|
|
1328
|
+
q.query_type,
|
|
1329
|
+
q.tables_detected,
|
|
1330
|
+
q.is_favorite,
|
|
1331
|
+
q.is_saved,
|
|
1332
|
+
q.is_destructive,
|
|
1333
|
+
q.use_count,
|
|
1334
|
+
q.first_executed_at,
|
|
1335
|
+
q.last_used_at,
|
|
1336
|
+
latest.id AS last_run_id,
|
|
1337
|
+
latest.executed_at AS last_run_executed_at,
|
|
1338
|
+
latest.duration_ms AS last_run_duration_ms,
|
|
1339
|
+
latest.row_count AS last_run_row_count,
|
|
1340
|
+
latest.status AS last_run_status,
|
|
1341
|
+
latest.error_message AS last_run_error_message,
|
|
1342
|
+
latest.affected_rows AS last_run_affected_rows
|
|
1343
|
+
FROM query_history q
|
|
1344
|
+
LEFT JOIN query_runs latest
|
|
1345
|
+
ON latest.id = (
|
|
1346
|
+
SELECT runs.id
|
|
1347
|
+
FROM query_runs runs
|
|
1348
|
+
WHERE runs.history_id = q.id
|
|
1349
|
+
ORDER BY runs.executed_at DESC, runs.id DESC
|
|
1350
|
+
LIMIT 1
|
|
1351
|
+
)
|
|
1352
|
+
WHERE q.database_key = ? AND q.normalized_sql = ?
|
|
1353
|
+
`)
|
|
1354
|
+
.get(normalizedDatabaseKey, normalizedSql);
|
|
1355
|
+
|
|
1356
|
+
return row ? this.decorateQueryHistoryRow(row) : null;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1147
1359
|
getQueryHistoryItemForDatabase(historyId, databaseKey) {
|
|
1148
1360
|
const item = this.getQueryHistoryItemById(historyId);
|
|
1149
1361
|
const normalizedDatabaseKey = this.normalizeQueryHistoryText(databaseKey);
|
|
@@ -1155,6 +1367,16 @@ class AppStateStore {
|
|
|
1155
1367
|
return item;
|
|
1156
1368
|
}
|
|
1157
1369
|
|
|
1370
|
+
getChartQueryHistoryItemForDatabase(historyId, databaseKey) {
|
|
1371
|
+
const item = this.getQueryHistoryItemForDatabase(historyId, databaseKey);
|
|
1372
|
+
|
|
1373
|
+
if (!isChartCompatibleQuery(item.queryType, item.rawSql)) {
|
|
1374
|
+
throw new ValidationError("Only SELECT queries can be opened in Charts.");
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
return item;
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1158
1380
|
getQueryRunsByHistoryId(historyId, limit = 8) {
|
|
1159
1381
|
const normalizedLimit = Math.max(1, Math.min(50, Number(limit) || 8));
|
|
1160
1382
|
|
|
@@ -1259,7 +1481,7 @@ class AppStateStore {
|
|
|
1259
1481
|
}
|
|
1260
1482
|
|
|
1261
1483
|
getQueryHistoryChartsDetail(historyId, databaseKey) {
|
|
1262
|
-
const item = this.
|
|
1484
|
+
const item = this.getChartQueryHistoryItemForDatabase(historyId, databaseKey);
|
|
1263
1485
|
|
|
1264
1486
|
return {
|
|
1265
1487
|
item,
|
|
@@ -1318,7 +1540,7 @@ class AppStateStore {
|
|
|
1318
1540
|
tableVisible = true,
|
|
1319
1541
|
databaseKey = null,
|
|
1320
1542
|
} = {}) {
|
|
1321
|
-
const item = this.
|
|
1543
|
+
const item = this.getChartQueryHistoryItemForDatabase(queryHistoryId, databaseKey);
|
|
1322
1544
|
const normalizedChartType = normalizeChartType(chartType);
|
|
1323
1545
|
const normalizedConfig = normalizeChartConfig(normalizedChartType, config);
|
|
1324
1546
|
const normalizedResultColumns = normalizeResultColumns(resultColumns);
|
|
@@ -1860,6 +2082,106 @@ class AppStateStore {
|
|
|
1860
2082
|
|
|
1861
2083
|
return this.getSettings();
|
|
1862
2084
|
}
|
|
2085
|
+
|
|
2086
|
+
getMediaTaggingConfig(databaseKey) {
|
|
2087
|
+
const normalizedDatabaseKey = String(databaseKey ?? "").trim();
|
|
2088
|
+
|
|
2089
|
+
if (!normalizedDatabaseKey) {
|
|
2090
|
+
return null;
|
|
2091
|
+
}
|
|
2092
|
+
|
|
2093
|
+
const row = this.db
|
|
2094
|
+
.prepare(
|
|
2095
|
+
`
|
|
2096
|
+
SELECT
|
|
2097
|
+
tag_table,
|
|
2098
|
+
media_table,
|
|
2099
|
+
path_column,
|
|
2100
|
+
tagged_column,
|
|
2101
|
+
untagged_query,
|
|
2102
|
+
tagged_query,
|
|
2103
|
+
mapping_table,
|
|
2104
|
+
updated_at
|
|
2105
|
+
FROM media_tagging_config
|
|
2106
|
+
WHERE database_key = ?
|
|
2107
|
+
`
|
|
2108
|
+
)
|
|
2109
|
+
.get(normalizedDatabaseKey);
|
|
2110
|
+
|
|
2111
|
+
if (!row) {
|
|
2112
|
+
return null;
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
return {
|
|
2116
|
+
config: structuredClone(this.buildMediaTaggingConfigRecord(row)),
|
|
2117
|
+
updatedAt: row.updated_at ?? null,
|
|
2118
|
+
};
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
setMediaTaggingConfig(databaseKey, config) {
|
|
2122
|
+
const normalizedDatabaseKey = String(databaseKey ?? "").trim();
|
|
2123
|
+
|
|
2124
|
+
if (!normalizedDatabaseKey) {
|
|
2125
|
+
throw new ValidationError("Media tagging configuration requires a database key.");
|
|
2126
|
+
}
|
|
2127
|
+
|
|
2128
|
+
const updatedAt = new Date().toISOString();
|
|
2129
|
+
const normalizedConfig = normalizeMediaTaggingConfigRecord(config);
|
|
2130
|
+
const values = [
|
|
2131
|
+
normalizedDatabaseKey,
|
|
2132
|
+
normalizedConfig.tagTable,
|
|
2133
|
+
normalizedConfig.mediaTable,
|
|
2134
|
+
normalizedConfig.pathColumn,
|
|
2135
|
+
normalizedConfig.taggedColumn,
|
|
2136
|
+
normalizedConfig.untaggedQuery,
|
|
2137
|
+
normalizedConfig.taggedQuery,
|
|
2138
|
+
normalizedConfig.mappingTable,
|
|
2139
|
+
];
|
|
2140
|
+
const explicitColumns = [
|
|
2141
|
+
"database_key",
|
|
2142
|
+
...MEDIA_TAGGING_CONFIG_FIELDS.map((field) => field.column),
|
|
2143
|
+
];
|
|
2144
|
+
const assignments = MEDIA_TAGGING_CONFIG_FIELDS.map(
|
|
2145
|
+
(field) => `${field.column} = excluded.${field.column}`
|
|
2146
|
+
);
|
|
2147
|
+
|
|
2148
|
+
if (this.mediaTaggingConfigHasLegacyJsonColumn) {
|
|
2149
|
+
explicitColumns.push("config_json");
|
|
2150
|
+
assignments.push("config_json = excluded.config_json");
|
|
2151
|
+
values.push(JSON.stringify(normalizedConfig));
|
|
2152
|
+
}
|
|
2153
|
+
|
|
2154
|
+
explicitColumns.push("updated_at");
|
|
2155
|
+
assignments.push("updated_at = excluded.updated_at");
|
|
2156
|
+
values.push(updatedAt);
|
|
2157
|
+
|
|
2158
|
+
this.db
|
|
2159
|
+
.prepare(
|
|
2160
|
+
`
|
|
2161
|
+
INSERT INTO media_tagging_config (${explicitColumns.join(", ")})
|
|
2162
|
+
VALUES (${explicitColumns.map(() => "?").join(", ")})
|
|
2163
|
+
ON CONFLICT(database_key) DO UPDATE SET
|
|
2164
|
+
${assignments.join(",\n ")}
|
|
2165
|
+
`
|
|
2166
|
+
)
|
|
2167
|
+
.run(...values);
|
|
2168
|
+
|
|
2169
|
+
return this.getMediaTaggingConfig(normalizedDatabaseKey);
|
|
2170
|
+
}
|
|
2171
|
+
|
|
2172
|
+
clearMediaTaggingConfig(databaseKey) {
|
|
2173
|
+
const normalizedDatabaseKey = String(databaseKey ?? "").trim();
|
|
2174
|
+
|
|
2175
|
+
if (!normalizedDatabaseKey) {
|
|
2176
|
+
return false;
|
|
2177
|
+
}
|
|
2178
|
+
|
|
2179
|
+
const result = this.db
|
|
2180
|
+
.prepare("DELETE FROM media_tagging_config WHERE database_key = ?")
|
|
2181
|
+
.run(normalizedDatabaseKey);
|
|
2182
|
+
|
|
2183
|
+
return result.changes > 0;
|
|
2184
|
+
}
|
|
1863
2185
|
}
|
|
1864
2186
|
|
|
1865
2187
|
module.exports = {
|
|
@@ -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") {
|