@gmickel/gno 0.16.0 → 0.17.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.
- package/README.md +36 -1
- package/package.json +4 -1
- package/src/cli/commands/ask.ts +9 -0
- package/src/cli/commands/query.ts +3 -2
- package/src/cli/pager.ts +1 -1
- package/src/cli/program.ts +89 -0
- package/src/core/links.ts +92 -20
- package/src/ingestion/sync.ts +267 -23
- package/src/ingestion/types.ts +2 -0
- package/src/ingestion/walker.ts +2 -1
- package/src/mcp/tools/index.ts +30 -1
- package/src/mcp/tools/query.ts +22 -2
- package/src/mcp/tools/search.ts +8 -0
- package/src/mcp/tools/vsearch.ts +8 -0
- package/src/pipeline/answer.ts +324 -7
- package/src/pipeline/expansion.ts +243 -7
- package/src/pipeline/explain.ts +93 -5
- package/src/pipeline/hybrid.ts +240 -57
- package/src/pipeline/query-modes.ts +125 -0
- package/src/pipeline/rerank.ts +34 -13
- package/src/pipeline/search.ts +41 -3
- package/src/pipeline/temporal.ts +257 -0
- package/src/pipeline/types.ts +58 -0
- package/src/pipeline/vsearch.ts +107 -9
- package/src/serve/public/app.tsx +1 -3
- package/src/serve/public/globals.built.css +2 -2
- package/src/serve/public/lib/retrieval-filters.ts +167 -0
- package/src/serve/public/pages/Ask.tsx +339 -109
- package/src/serve/public/pages/Browse.tsx +71 -5
- package/src/serve/public/pages/DocView.tsx +2 -21
- package/src/serve/public/pages/Search.tsx +507 -120
- package/src/serve/routes/api.ts +202 -2
- package/src/store/migrations/006-document-metadata.ts +104 -0
- package/src/store/migrations/007-document-date-fields.ts +24 -0
- package/src/store/migrations/index.ts +3 -1
- package/src/store/sqlite/adapter.ts +218 -5
- package/src/store/types.ts +46 -0
package/src/serve/routes/api.ts
CHANGED
|
@@ -6,7 +6,12 @@
|
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
8
|
import type { Config, ModelPreset } from "../../config/types";
|
|
9
|
-
import type {
|
|
9
|
+
import type {
|
|
10
|
+
AskResult,
|
|
11
|
+
Citation,
|
|
12
|
+
QueryModeInput,
|
|
13
|
+
SearchOptions,
|
|
14
|
+
} from "../../pipeline/types";
|
|
10
15
|
import type { SqliteAdapter } from "../../store/sqlite/adapter";
|
|
11
16
|
import type { EmbedScheduler } from "../embed-scheduler";
|
|
12
17
|
|
|
@@ -61,6 +66,11 @@ export interface SearchRequestBody {
|
|
|
61
66
|
limit?: number;
|
|
62
67
|
minScore?: number;
|
|
63
68
|
collection?: string;
|
|
69
|
+
since?: string;
|
|
70
|
+
until?: string;
|
|
71
|
+
/** Comma-separated category filters */
|
|
72
|
+
category?: string;
|
|
73
|
+
author?: string;
|
|
64
74
|
/** Comma-separated tags - filter to docs having ALL (AND) */
|
|
65
75
|
tagsAll?: string;
|
|
66
76
|
/** Comma-separated tags - filter to docs having ANY (OR) */
|
|
@@ -73,6 +83,12 @@ export interface QueryRequestBody {
|
|
|
73
83
|
minScore?: number;
|
|
74
84
|
collection?: string;
|
|
75
85
|
lang?: string;
|
|
86
|
+
since?: string;
|
|
87
|
+
until?: string;
|
|
88
|
+
/** Comma-separated category filters */
|
|
89
|
+
category?: string;
|
|
90
|
+
author?: string;
|
|
91
|
+
queryModes?: QueryModeInput[];
|
|
76
92
|
noExpand?: boolean;
|
|
77
93
|
noRerank?: boolean;
|
|
78
94
|
/** Comma-separated tags - filter to docs having ALL (AND) */
|
|
@@ -86,6 +102,11 @@ export interface AskRequestBody {
|
|
|
86
102
|
limit?: number;
|
|
87
103
|
collection?: string;
|
|
88
104
|
lang?: string;
|
|
105
|
+
since?: string;
|
|
106
|
+
until?: string;
|
|
107
|
+
/** Comma-separated category filters */
|
|
108
|
+
category?: string;
|
|
109
|
+
author?: string;
|
|
89
110
|
maxAnswerTokens?: number;
|
|
90
111
|
noExpand?: boolean;
|
|
91
112
|
noRerank?: boolean;
|
|
@@ -137,6 +158,17 @@ function errorResponse(code: string, message: string, status = 400): Response {
|
|
|
137
158
|
return jsonResponse({ error: { code, message } }, status);
|
|
138
159
|
}
|
|
139
160
|
|
|
161
|
+
function parseCommaSeparatedValues(input: string): string[] {
|
|
162
|
+
return Array.from(
|
|
163
|
+
new Set(
|
|
164
|
+
input
|
|
165
|
+
.split(",")
|
|
166
|
+
.map((value) => value.trim().toLowerCase())
|
|
167
|
+
.filter(Boolean)
|
|
168
|
+
)
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
140
172
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
141
173
|
// Route Handlers
|
|
142
174
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -431,6 +463,12 @@ export async function handleDocs(
|
|
|
431
463
|
url: URL
|
|
432
464
|
): Promise<Response> {
|
|
433
465
|
const collection = url.searchParams.get("collection") || undefined;
|
|
466
|
+
const sortFieldRaw = (url.searchParams.get("sortField") ?? "modified")
|
|
467
|
+
.trim()
|
|
468
|
+
.toLowerCase();
|
|
469
|
+
const sortOrderRaw = (url.searchParams.get("sortOrder") ?? "desc")
|
|
470
|
+
.trim()
|
|
471
|
+
.toLowerCase();
|
|
434
472
|
|
|
435
473
|
// Validate limit: positive integer, max 100
|
|
436
474
|
const limitParam = Number(url.searchParams.get("limit"));
|
|
@@ -452,6 +490,18 @@ export async function handleDocs(
|
|
|
452
490
|
}
|
|
453
491
|
const offset = offsetParam || 0;
|
|
454
492
|
|
|
493
|
+
if (sortFieldRaw !== "modified" && !/^[a-z0-9_]+$/.test(sortFieldRaw)) {
|
|
494
|
+
return errorResponse(
|
|
495
|
+
"VALIDATION",
|
|
496
|
+
"sortField must be 'modified' or a lowercase frontmatter date key"
|
|
497
|
+
);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (sortOrderRaw !== "asc" && sortOrderRaw !== "desc") {
|
|
501
|
+
return errorResponse("VALIDATION", "sortOrder must be 'asc' or 'desc'");
|
|
502
|
+
}
|
|
503
|
+
const sortOrder: "asc" | "desc" = sortOrderRaw === "asc" ? "asc" : "desc";
|
|
504
|
+
|
|
455
505
|
// Parse tag filters
|
|
456
506
|
let tagsAll: string[] | undefined;
|
|
457
507
|
let tagsAny: string[] | undefined;
|
|
@@ -480,12 +530,30 @@ export async function handleDocs(
|
|
|
480
530
|
}
|
|
481
531
|
}
|
|
482
532
|
|
|
533
|
+
const dateFieldsResult = await store.getCollectionDateFields(collection);
|
|
534
|
+
if (!dateFieldsResult.ok) {
|
|
535
|
+
return errorResponse("RUNTIME", dateFieldsResult.error.message, 500);
|
|
536
|
+
}
|
|
537
|
+
const availableDateFields = dateFieldsResult.value;
|
|
538
|
+
|
|
539
|
+
if (
|
|
540
|
+
sortFieldRaw !== "modified" &&
|
|
541
|
+
!availableDateFields.includes(sortFieldRaw)
|
|
542
|
+
) {
|
|
543
|
+
return errorResponse(
|
|
544
|
+
"VALIDATION",
|
|
545
|
+
`Unknown sortField: ${sortFieldRaw} for current collection`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
|
|
483
549
|
const result = await store.listDocumentsPaginated({
|
|
484
550
|
collection,
|
|
485
551
|
limit,
|
|
486
552
|
offset,
|
|
487
553
|
tagsAll,
|
|
488
554
|
tagsAny,
|
|
555
|
+
sortField: sortFieldRaw,
|
|
556
|
+
sortOrder,
|
|
489
557
|
});
|
|
490
558
|
|
|
491
559
|
if (!result.ok) {
|
|
@@ -508,6 +576,9 @@ export async function handleDocs(
|
|
|
508
576
|
total,
|
|
509
577
|
limit,
|
|
510
578
|
offset,
|
|
579
|
+
availableDateFields,
|
|
580
|
+
sortField: sortFieldRaw,
|
|
581
|
+
sortOrder,
|
|
511
582
|
});
|
|
512
583
|
}
|
|
513
584
|
|
|
@@ -1016,6 +1087,22 @@ export async function handleSearch(
|
|
|
1016
1087
|
);
|
|
1017
1088
|
}
|
|
1018
1089
|
|
|
1090
|
+
if (body.since !== undefined && typeof body.since !== "string") {
|
|
1091
|
+
return errorResponse("VALIDATION", "since must be a string");
|
|
1092
|
+
}
|
|
1093
|
+
if (body.until !== undefined && typeof body.until !== "string") {
|
|
1094
|
+
return errorResponse("VALIDATION", "until must be a string");
|
|
1095
|
+
}
|
|
1096
|
+
if (body.category !== undefined && typeof body.category !== "string") {
|
|
1097
|
+
return errorResponse(
|
|
1098
|
+
"VALIDATION",
|
|
1099
|
+
"category must be a comma-separated string"
|
|
1100
|
+
);
|
|
1101
|
+
}
|
|
1102
|
+
if (body.author !== undefined && typeof body.author !== "string") {
|
|
1103
|
+
return errorResponse("VALIDATION", "author must be a string");
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1019
1106
|
// Parse tag filters
|
|
1020
1107
|
let tagsAll: string[] | undefined;
|
|
1021
1108
|
let tagsAny: string[] | undefined;
|
|
@@ -1042,6 +1129,11 @@ export async function handleSearch(
|
|
|
1042
1129
|
}
|
|
1043
1130
|
}
|
|
1044
1131
|
|
|
1132
|
+
const categories = body.category
|
|
1133
|
+
? parseCommaSeparatedValues(body.category)
|
|
1134
|
+
: undefined;
|
|
1135
|
+
const author = body.author?.trim() || undefined;
|
|
1136
|
+
|
|
1045
1137
|
// Only BM25 supported in web UI (vector/hybrid require LLM ports)
|
|
1046
1138
|
const options: SearchOptions = {
|
|
1047
1139
|
limit: Math.min(body.limit || 10, 50),
|
|
@@ -1049,6 +1141,10 @@ export async function handleSearch(
|
|
|
1049
1141
|
collection: body.collection,
|
|
1050
1142
|
tagsAll,
|
|
1051
1143
|
tagsAny,
|
|
1144
|
+
since: body.since,
|
|
1145
|
+
until: body.until,
|
|
1146
|
+
categories,
|
|
1147
|
+
author,
|
|
1052
1148
|
};
|
|
1053
1149
|
|
|
1054
1150
|
const result = await searchBm25(store, query, options);
|
|
@@ -1062,7 +1158,7 @@ export async function handleSearch(
|
|
|
1062
1158
|
|
|
1063
1159
|
/**
|
|
1064
1160
|
* POST /api/query
|
|
1065
|
-
* Body: { query, limit?, minScore?, collection?, lang?, noExpand?, noRerank? }
|
|
1161
|
+
* Body: { query, limit?, minScore?, collection?, lang?, queryModes?, noExpand?, noRerank? }
|
|
1066
1162
|
* Returns hybrid search results (BM25 + vector + expansion + reranking).
|
|
1067
1163
|
*/
|
|
1068
1164
|
export async function handleQuery(
|
|
@@ -1106,6 +1202,72 @@ export async function handleQuery(
|
|
|
1106
1202
|
);
|
|
1107
1203
|
}
|
|
1108
1204
|
|
|
1205
|
+
if (body.since !== undefined && typeof body.since !== "string") {
|
|
1206
|
+
return errorResponse("VALIDATION", "since must be a string");
|
|
1207
|
+
}
|
|
1208
|
+
if (body.until !== undefined && typeof body.until !== "string") {
|
|
1209
|
+
return errorResponse("VALIDATION", "until must be a string");
|
|
1210
|
+
}
|
|
1211
|
+
if (body.category !== undefined && typeof body.category !== "string") {
|
|
1212
|
+
return errorResponse(
|
|
1213
|
+
"VALIDATION",
|
|
1214
|
+
"category must be a comma-separated string"
|
|
1215
|
+
);
|
|
1216
|
+
}
|
|
1217
|
+
if (body.author !== undefined && typeof body.author !== "string") {
|
|
1218
|
+
return errorResponse("VALIDATION", "author must be a string");
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// Validate queryModes
|
|
1222
|
+
let queryModes: QueryModeInput[] | undefined;
|
|
1223
|
+
if (body.queryModes !== undefined) {
|
|
1224
|
+
if (!Array.isArray(body.queryModes)) {
|
|
1225
|
+
return errorResponse(
|
|
1226
|
+
"VALIDATION",
|
|
1227
|
+
"queryModes must be an array of { mode, text } objects"
|
|
1228
|
+
);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
queryModes = [];
|
|
1232
|
+
let hydeCount = 0;
|
|
1233
|
+
|
|
1234
|
+
for (const [index, entry] of body.queryModes.entries()) {
|
|
1235
|
+
if (!entry || typeof entry !== "object") {
|
|
1236
|
+
return errorResponse(
|
|
1237
|
+
"VALIDATION",
|
|
1238
|
+
`queryModes[${index}] must be an object`
|
|
1239
|
+
);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
const mode = (entry as { mode?: unknown }).mode;
|
|
1243
|
+
const text = (entry as { text?: unknown }).text;
|
|
1244
|
+
if (mode !== "term" && mode !== "intent" && mode !== "hyde") {
|
|
1245
|
+
return errorResponse(
|
|
1246
|
+
"VALIDATION",
|
|
1247
|
+
`queryModes[${index}].mode must be one of: term, intent, hyde`
|
|
1248
|
+
);
|
|
1249
|
+
}
|
|
1250
|
+
if (typeof text !== "string" || !text.trim()) {
|
|
1251
|
+
return errorResponse(
|
|
1252
|
+
"VALIDATION",
|
|
1253
|
+
`queryModes[${index}].text must be a non-empty string`
|
|
1254
|
+
);
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
if (mode === "hyde") {
|
|
1258
|
+
hydeCount += 1;
|
|
1259
|
+
if (hydeCount > 1) {
|
|
1260
|
+
return errorResponse(
|
|
1261
|
+
"VALIDATION",
|
|
1262
|
+
"Only one hyde mode is allowed in queryModes"
|
|
1263
|
+
);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
|
|
1267
|
+
queryModes.push({ mode, text: text.trim() });
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1109
1271
|
// Parse tag filters
|
|
1110
1272
|
let tagsAll: string[] | undefined;
|
|
1111
1273
|
let tagsAny: string[] | undefined;
|
|
@@ -1132,6 +1294,11 @@ export async function handleQuery(
|
|
|
1132
1294
|
}
|
|
1133
1295
|
}
|
|
1134
1296
|
|
|
1297
|
+
const categories = body.category
|
|
1298
|
+
? parseCommaSeparatedValues(body.category)
|
|
1299
|
+
: undefined;
|
|
1300
|
+
const author = body.author?.trim() || undefined;
|
|
1301
|
+
|
|
1135
1302
|
const result = await searchHybrid(
|
|
1136
1303
|
{
|
|
1137
1304
|
store: ctx.store,
|
|
@@ -1147,10 +1314,15 @@ export async function handleQuery(
|
|
|
1147
1314
|
minScore: body.minScore,
|
|
1148
1315
|
collection: body.collection,
|
|
1149
1316
|
lang: body.lang,
|
|
1317
|
+
queryModes,
|
|
1150
1318
|
noExpand: body.noExpand,
|
|
1151
1319
|
noRerank: body.noRerank,
|
|
1152
1320
|
tagsAll,
|
|
1153
1321
|
tagsAny,
|
|
1322
|
+
since: body.since,
|
|
1323
|
+
until: body.until,
|
|
1324
|
+
categories,
|
|
1325
|
+
author,
|
|
1154
1326
|
}
|
|
1155
1327
|
);
|
|
1156
1328
|
|
|
@@ -1199,6 +1371,22 @@ export async function handleAsk(
|
|
|
1199
1371
|
let tagsAll: string[] | undefined;
|
|
1200
1372
|
let tagsAny: string[] | undefined;
|
|
1201
1373
|
|
|
1374
|
+
if (body.since !== undefined && typeof body.since !== "string") {
|
|
1375
|
+
return errorResponse("VALIDATION", "since must be a string");
|
|
1376
|
+
}
|
|
1377
|
+
if (body.until !== undefined && typeof body.until !== "string") {
|
|
1378
|
+
return errorResponse("VALIDATION", "until must be a string");
|
|
1379
|
+
}
|
|
1380
|
+
if (body.category !== undefined && typeof body.category !== "string") {
|
|
1381
|
+
return errorResponse(
|
|
1382
|
+
"VALIDATION",
|
|
1383
|
+
"category must be a comma-separated string"
|
|
1384
|
+
);
|
|
1385
|
+
}
|
|
1386
|
+
if (body.author !== undefined && typeof body.author !== "string") {
|
|
1387
|
+
return errorResponse("VALIDATION", "author must be a string");
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1202
1390
|
if (body.tagsAll) {
|
|
1203
1391
|
try {
|
|
1204
1392
|
tagsAll = parseAndValidateTagFilter(body.tagsAll);
|
|
@@ -1221,6 +1409,11 @@ export async function handleAsk(
|
|
|
1221
1409
|
}
|
|
1222
1410
|
}
|
|
1223
1411
|
|
|
1412
|
+
const categories = body.category
|
|
1413
|
+
? parseCommaSeparatedValues(body.category)
|
|
1414
|
+
: undefined;
|
|
1415
|
+
const author = body.author?.trim() || undefined;
|
|
1416
|
+
|
|
1224
1417
|
const limit = Math.min(body.limit ?? 5, 20);
|
|
1225
1418
|
|
|
1226
1419
|
// Run hybrid search first
|
|
@@ -1242,6 +1435,10 @@ export async function handleAsk(
|
|
|
1242
1435
|
noRerank: body.noRerank,
|
|
1243
1436
|
tagsAll,
|
|
1244
1437
|
tagsAny,
|
|
1438
|
+
since: body.since,
|
|
1439
|
+
until: body.until,
|
|
1440
|
+
categories,
|
|
1441
|
+
author,
|
|
1245
1442
|
}
|
|
1246
1443
|
);
|
|
1247
1444
|
|
|
@@ -1254,6 +1451,7 @@ export async function handleAsk(
|
|
|
1254
1451
|
// Generate grounded answer (requires genPort)
|
|
1255
1452
|
let answer: string | undefined;
|
|
1256
1453
|
let citations: Citation[] | undefined;
|
|
1454
|
+
let answerContext: AskResult["meta"]["answerContext"] | undefined;
|
|
1257
1455
|
let answerGenerated = false;
|
|
1258
1456
|
|
|
1259
1457
|
if (ctx.genPort) {
|
|
@@ -1269,6 +1467,7 @@ export async function handleAsk(
|
|
|
1269
1467
|
const processed = processAnswerResult(rawResult);
|
|
1270
1468
|
answer = processed.answer;
|
|
1271
1469
|
citations = processed.citations;
|
|
1470
|
+
answerContext = processed.answerContext;
|
|
1272
1471
|
answerGenerated = true;
|
|
1273
1472
|
}
|
|
1274
1473
|
}
|
|
@@ -1286,6 +1485,7 @@ export async function handleAsk(
|
|
|
1286
1485
|
vectorsUsed: searchResult.value.meta.vectorsUsed ?? false,
|
|
1287
1486
|
answerGenerated,
|
|
1288
1487
|
totalResults: results.length,
|
|
1488
|
+
answerContext,
|
|
1289
1489
|
},
|
|
1290
1490
|
};
|
|
1291
1491
|
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: document metadata fields for temporal/category/author filtering.
|
|
3
|
+
*
|
|
4
|
+
* @module src/store/migrations/006-document-metadata
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import type { Migration } from "./runner";
|
|
10
|
+
|
|
11
|
+
const TABLE_INFO_DOCUMENTS = "PRAGMA table_info(documents)";
|
|
12
|
+
|
|
13
|
+
interface TableInfoRow {
|
|
14
|
+
name: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function getDocumentColumns(db: Database): Set<string> {
|
|
18
|
+
const rows = db.query<TableInfoRow, []>(TABLE_INFO_DOCUMENTS).all();
|
|
19
|
+
return new Set(rows.map((row) => row.name));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function addColumnIfMissing(
|
|
23
|
+
db: Database,
|
|
24
|
+
columns: Set<string>,
|
|
25
|
+
column: string,
|
|
26
|
+
ddl: string
|
|
27
|
+
): void {
|
|
28
|
+
if (columns.has(column)) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
db.exec(ddl);
|
|
33
|
+
columns.add(column);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const migration: Migration = {
|
|
37
|
+
version: 6,
|
|
38
|
+
name: "document_metadata",
|
|
39
|
+
|
|
40
|
+
up(db): void {
|
|
41
|
+
const columns = getDocumentColumns(db);
|
|
42
|
+
|
|
43
|
+
addColumnIfMissing(
|
|
44
|
+
db,
|
|
45
|
+
columns,
|
|
46
|
+
"source_ctime",
|
|
47
|
+
"ALTER TABLE documents ADD COLUMN source_ctime TEXT"
|
|
48
|
+
);
|
|
49
|
+
// SQLite rejects non-constant defaults in ALTER TABLE on older engines.
|
|
50
|
+
addColumnIfMissing(
|
|
51
|
+
db,
|
|
52
|
+
columns,
|
|
53
|
+
"indexed_at",
|
|
54
|
+
"ALTER TABLE documents ADD COLUMN indexed_at TEXT"
|
|
55
|
+
);
|
|
56
|
+
addColumnIfMissing(
|
|
57
|
+
db,
|
|
58
|
+
columns,
|
|
59
|
+
"content_type",
|
|
60
|
+
"ALTER TABLE documents ADD COLUMN content_type TEXT"
|
|
61
|
+
);
|
|
62
|
+
addColumnIfMissing(
|
|
63
|
+
db,
|
|
64
|
+
columns,
|
|
65
|
+
"categories",
|
|
66
|
+
"ALTER TABLE documents ADD COLUMN categories TEXT"
|
|
67
|
+
);
|
|
68
|
+
addColumnIfMissing(
|
|
69
|
+
db,
|
|
70
|
+
columns,
|
|
71
|
+
"author",
|
|
72
|
+
"ALTER TABLE documents ADD COLUMN author TEXT"
|
|
73
|
+
);
|
|
74
|
+
addColumnIfMissing(
|
|
75
|
+
db,
|
|
76
|
+
columns,
|
|
77
|
+
"frontmatter_date",
|
|
78
|
+
"ALTER TABLE documents ADD COLUMN frontmatter_date TEXT"
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
db.exec(`
|
|
82
|
+
UPDATE documents
|
|
83
|
+
SET indexed_at = datetime('now')
|
|
84
|
+
WHERE indexed_at IS NULL
|
|
85
|
+
`);
|
|
86
|
+
|
|
87
|
+
db.exec(
|
|
88
|
+
"CREATE INDEX IF NOT EXISTS idx_documents_source_mtime ON documents(source_mtime)"
|
|
89
|
+
);
|
|
90
|
+
db.exec(
|
|
91
|
+
"CREATE INDEX IF NOT EXISTS idx_documents_content_type ON documents(content_type)"
|
|
92
|
+
);
|
|
93
|
+
db.exec(
|
|
94
|
+
"CREATE INDEX IF NOT EXISTS idx_documents_author ON documents(author)"
|
|
95
|
+
);
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
down(db): void {
|
|
99
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_author");
|
|
100
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_content_type");
|
|
101
|
+
db.exec("DROP INDEX IF EXISTS idx_documents_source_mtime");
|
|
102
|
+
// SQLite cannot drop columns without table rebuild; keep columns on rollback.
|
|
103
|
+
},
|
|
104
|
+
};
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migration: document frontmatter date field map.
|
|
3
|
+
*
|
|
4
|
+
* @module src/store/migrations/007-document-date-fields
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { Database } from "bun:sqlite";
|
|
8
|
+
|
|
9
|
+
import type { Migration } from "./runner";
|
|
10
|
+
|
|
11
|
+
export const migration: Migration = {
|
|
12
|
+
version: 7,
|
|
13
|
+
name: "document_date_fields",
|
|
14
|
+
|
|
15
|
+
up(db: Database): void {
|
|
16
|
+
db.exec(`
|
|
17
|
+
ALTER TABLE documents ADD COLUMN date_fields TEXT
|
|
18
|
+
`);
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
down(): void {
|
|
22
|
+
// SQLite cannot drop columns without table rebuild; keep column on rollback.
|
|
23
|
+
},
|
|
24
|
+
};
|
|
@@ -19,6 +19,8 @@ import { migration as m002 } from "./002-documents-fts";
|
|
|
19
19
|
import { migration as m003 } from "./003-doc-tags";
|
|
20
20
|
import { migration as m004 } from "./004-doc-links";
|
|
21
21
|
import { migration as m005 } from "./005-graph-indexes";
|
|
22
|
+
import { migration as m006 } from "./006-document-metadata";
|
|
23
|
+
import { migration as m007 } from "./007-document-date-fields";
|
|
22
24
|
|
|
23
25
|
/** All migrations in order */
|
|
24
|
-
export const migrations = [m001, m002, m003, m004, m005];
|
|
26
|
+
export const migrations = [m001, m002, m003, m004, m005, m006, m007];
|