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.
Files changed (34) hide show
  1. package/changelog.md +16 -0
  2. package/frontend/index.html +1 -1
  3. package/frontend/js/api.js +67 -0
  4. package/frontend/js/app.js +1554 -999
  5. package/frontend/js/components/modal.js +208 -3
  6. package/frontend/js/components/queryEditor.js +9 -4
  7. package/frontend/js/components/queryHistoryDetail.js +34 -12
  8. package/frontend/js/components/queryHistoryPanel.js +8 -11
  9. package/frontend/js/components/rowEditorPanel.js +26 -5
  10. package/frontend/js/components/sidebar.js +49 -5
  11. package/frontend/js/components/structureGraph.js +614 -632
  12. package/frontend/js/components/tableDesignerSqlPreview.js +24 -25
  13. package/frontend/js/lib/mediaTaggingDefaults.js +27 -0
  14. package/frontend/js/router.js +81 -71
  15. package/frontend/js/store.js +3140 -2179
  16. package/frontend/js/views/charts.js +2 -2
  17. package/frontend/js/views/data.js +28 -14
  18. package/frontend/js/views/editor.js +172 -177
  19. package/frontend/js/views/mediaTagging.js +931 -0
  20. package/frontend/styles/base.css +11 -1
  21. package/frontend/styles/components.css +54 -6
  22. package/frontend/styles/views.css +910 -0
  23. package/package.json +1 -1
  24. package/server/routes/charts.js +4 -1
  25. package/server/routes/data.js +14 -0
  26. package/server/routes/mediaTagging.js +166 -0
  27. package/server/routes/sql.js +1 -0
  28. package/server/server.js +4 -0
  29. package/server/services/sqlite/dataBrowserService.js +25 -0
  30. package/server/services/sqlite/exportService.js +31 -1
  31. package/server/services/sqlite/mediaTaggingService.js +1689 -0
  32. package/server/services/storage/appStateStore.js +325 -3
  33. package/server/services/storage/queryHistoryUtils.js +165 -2
  34. 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.getQueryHistoryItemForDatabase(historyId, databaseKey);
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.getQueryHistoryItemForDatabase(queryHistoryId, databaseKey);
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, thirdKeyword] = getLeadingKeywords(sql, 3);
204
+ const [firstKeyword, secondKeyword] = getLeadingKeywords(sql, 3);
42
205
 
43
206
  if (firstKeyword === "with") {
44
- return secondKeyword === "recursive" ? thirdKeyword ?? "with" : secondKeyword ?? "with";
207
+ return resolveCteStatementKeyword(sql);
45
208
  }
46
209
 
47
210
  if (firstKeyword === "explain") {
package/shortkeys.md ADDED
@@ -0,0 +1,5 @@
1
+ # Meddia Tagging
2
+
3
+ ## tagging queue
4
+
5
+ `shift + enter` triggers tagged & next