Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
2903502
Reach filtered full-text matches beyond native result windows
kylebernhardy Oct 18, 2026
b2f6077
Validate traversal replay and honor terminal cancellation
kylebernhardy Oct 18, 2026
f4d030a
Define continuation budgets and authenticated replay tokens
kylebernhardy Oct 18, 2026
545d462
Merge branch 'kyle/fulltext-continuation' into kyle/fulltext-snapshot…
kylebernhardy Oct 18, 2026
9c5d15c
Bound traversal work with an explicit configurable request budget
kylebernhardy Oct 18, 2026
af5691c
Bound continuation admission across Harper workers
kylebernhardy Oct 18, 2026
ff4f49e
Carry configured traversal bounds into continuation work
kylebernhardy Oct 18, 2026
514c983
Merge reviewed traversal foundation into snapshot continuation
kylebernhardy Oct 18, 2026
31e1ee2
Add instance-local full-text snapshot continuation pages
kylebernhardy Oct 18, 2026
9ef0151
Expose full-text limit aliases and continuation page types
kylebernhardy Oct 18, 2026
8cd043b
Verify async continuation authorization and bound worker test cleanup
kylebernhardy Oct 18, 2026
8216c80
Preserve per-page permission checks for reused continuation targets
kylebernhardy Oct 18, 2026
650babd
Bind continuations to database identity and reject overload promptly
kylebernhardy Oct 18, 2026
e9ac098
Include array condition properties in continuation bindings
kylebernhardy Oct 18, 2026
1c9317a
Clarify continuation capacity failures and cover reader lifecycle
kylebernhardy Oct 18, 2026
823fdae
Await full-text admission release and validate continuation progress
kylebernhardy Oct 19, 2026
bb1e130
Fence continuation setup against index definition changes
kylebernhardy Oct 19, 2026
41bc22c
Fix continuation query validation and request lifecycle fences
kylebernhardy Oct 19, 2026
56dbb24
Preserve acknowledged full-text completion after cleanup
kylebernhardy Oct 19, 2026
21c85cc
Preserve parsed continuation targets and credential checks
kylebernhardy Oct 19, 2026
4b41421
Reject full-text continuation on destructive requests
kylebernhardy Oct 19, 2026
cc3efed
Use protocol request target in delete regression
kylebernhardy Oct 19, 2026
68aad5b
Format destructive continuation regression
kylebernhardy Oct 19, 2026
6615440
Clarify continuation method contract
kylebernhardy Oct 19, 2026
e3d3730
Assert rejected continuation requests preserve cursor capacity
kylebernhardy Oct 19, 2026
31bc91f
Avoid eager continuation admission and reuse retained highlight reade…
kylebernhardy Oct 19, 2026
5567d5f
Measure continuation costs and reuse limits within each command
kylebernhardy Oct 19, 2026
a7359c4
Use the shared full-text pending-request default in worker dispatch
kylebernhardy Oct 19, 2026
6913e4b
Share continuation deadlines across Node and Bun workers
kylebernhardy Oct 19, 2026
f09520c
Distinguish native query failures from request cancellation
kylebernhardy Oct 19, 2026
d0d258d
Validate continuation flags and publish metadata after serialization
kylebernhardy Oct 20, 2026
486b42e
Exercise the collection target in continuation deletion tests
kylebernhardy Oct 20, 2026
b8dde76
Verify continuation replay after response serialization fails
kylebernhardy Oct 20, 2026
2d6de31
Clarify continuation header delivery and reserved options
kylebernhardy Oct 20, 2026
14ecb07
Bind continuation identity to the active query index configuration
kylebernhardy Oct 20, 2026
0a235f5
Handle malformed continuation cleanup after worker exit
kylebernhardy Oct 20, 2026
fe0cc56
Fix continuation review policy and merged design document budget
kylebernhardy Oct 20, 2026
fddf587
Merge main into fulltext snapshot continuation
kylebernhardy Oct 20, 2026
f4a8fbe
Correlate the transaction report test with its own holder
kylebernhardy Oct 20, 2026
0f0cfb9
Preserve iterable searches and bound ordinary full-text work
kylebernhardy Oct 20, 2026
a6c42ad
Bound mixed-width continuation replies and isolate runtime diagnostics
kylebernhardy Oct 20, 2026
81a2498
Verify paging boundaries and record final worker measurements
kylebernhardy Oct 20, 2026
b706930
Enforce paging response and application worker boundaries
kylebernhardy Oct 20, 2026
6e61e05
Preserve custom QUERY contracts and verify nested worker inheritance
kylebernhardy Oct 21, 2026
ef71002
Merge current main into full-text continuation repairs
kylebernhardy Oct 21, 2026
5bc5a1b
Capture native full-text settings before loading applications
kylebernhardy Oct 21, 2026
e219e4c
Scope paging response checks to their own QUERY dispatch
kylebernhardy Oct 21, 2026
1fce795
Preserve paging headers through response envelopes and conditional reads
kylebernhardy Oct 21, 2026
0872140
Record worker measurements after current main reconciliation
kylebernhardy Oct 21, 2026
475371a
Reject copied page data inside standard response envelopes
kylebernhardy Oct 21, 2026
dabebf8
Preserve error status from paging response envelopes
kylebernhardy Oct 21, 2026
4161090
Verify paging envelope errors retain their HTTP status
kylebernhardy Oct 21, 2026
efd9d39
Distinguish full-text hit and time budget errors in the source guide
kylebernhardy Oct 21, 2026
5b1509e
Require an explicit numeric page limit in typed table searches
kylebernhardy Oct 21, 2026
3f31e2e
Align full-text search with existing limit and offset paging
kylebernhardy Oct 21, 2026
4ad5519
Check removed query functions through existing parser errors
kylebernhardy Oct 21, 2026
9001498
Clarify paging memory costs and count estimates
kylebernhardy Oct 21, 2026
ea2baef
Tighten native paging invariants and initialization diagnostics
kylebernhardy Oct 21, 2026
db4c1cd
Document table globals and fixed native query bounds
kylebernhardy Oct 21, 2026
90f7721
Use native traversal directly and separate retained-result bounds
kylebernhardy Oct 21, 2026
2d4a86a
Reject incomplete unbounded results at the configured ceiling
kylebernhardy Oct 21, 2026
18d9fe5
Preserve native ordinary-query admission before traversal
kylebernhardy Oct 21, 2026
a6ec7f2
Read full-text limits from registered configuration keys
kylebernhardy Oct 21, 2026
35c541d
Avoid unused runtime policy reads in full-text queries
kylebernhardy Oct 21, 2026
a2a4306
Share worker-restart budget assertions across configuration phases
kylebernhardy Oct 21, 2026
5015a73
Verify native prefix ordering across different page sizes
kylebernhardy Oct 21, 2026
02cfb17
Preserve native test failures and report the effective query bound
kylebernhardy Oct 21, 2026
5889c1f
Reject impossible replay work and capture native CI crashes
kylebernhardy Oct 21, 2026
93579e4
Preserve test outcomes when crash diagnostics are unavailable
kylebernhardy Oct 21, 2026
4073995
Verify replay budgets with lower-bound native totals
kylebernhardy Oct 21, 2026
c192c2f
Remove temporary LMDB crash diagnostics from paging PR
kylebernhardy Oct 21, 2026
4c57379
Remove duplicate full-text source-version validation
kylebernhardy Oct 21, 2026
6538281
Merge main and retain full-text worker configuration through TypeScri…
kylebernhardy Oct 26, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/unit-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -330,7 +330,7 @@ jobs:
run: npm run build

- name: Verify the published native binding loads
run: node --input-type=module --eval "const binding = await import('@harperfast/fulltext/native'); if (typeof binding.openNativeFullTextIndex !== 'function' || typeof binding.openNativeFullTextReader !== 'function') throw new Error('@harperfast/fulltext loaded without native writer and reader openers');"
run: node --input-type=module --eval "const binding = await import('@harperfast/fulltext/native'); if (typeof binding.openNativeFullTextIndex !== 'function' || typeof binding.openNativeFullTextReader !== 'function' || typeof binding.NativeFullTextIndex?.prototype.searchPage !== 'function' || (await binding.runtimeInfo()).queryApiVersion !== 4) throw new Error('@harperfast/fulltext loaded without the required writer, reader, and query API 4 contract');"

- name: Native full-text lifecycle test
run: npx mocha unitTests/resources/nativeFullTextDerivedIndexLifecycle.test.js unitTests/resources/nativeFullTextTableSearch.test.js
run: npx mocha unitTests/resources/nativeFullTextDerivedIndexLifecycle.test.js unitTests/resources/nativeFullTextTableSearch.test.js --forbid-pending --fail-zero
69 changes: 69 additions & 0 deletions config-root.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,75 @@
"description": "Storage engine configuration.",
"additionalProperties": true,
"properties": {
"fullText": {
"type": "object",
"description": "Full-text request work limits and native process budgets. Native process budgets require a Harper process restart to change; they do not cap total process memory or disk usage.",
"additionalProperties": false,
"properties": {
"requestBudgetMilliseconds": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 5000
},
"maxRawHitsPerRequest": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 10000,
"description": "Maximum fetched native hits per full-text request, including bounded prefix replay."
},
"maxResultsPerRequest": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 10000,
"description": "Maximum offset plus limit retained by a full-text request, independent of fetched-hit work."
},
"runtime": {
"type": "object",
"additionalProperties": false,
"properties": {
"maxResidentIndexes": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295,
"default": 128
},
"maxIndexingThreads": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295,
"default": 128
},
"maxSearchThreads": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295,
"default": 256
},
"maxWriterMemoryBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 4294967296
},
"maxQueuedBytes": {
"type": "integer",
"minimum": 1,
"maximum": 9007199254740991,
"default": 8589934592
},
"maxExpensiveSearches": {
"type": "integer",
"minimum": 1,
"maximum": 4294967295,
"default": 8
}
}
}
}
},
"writeAsync": {
"type": "boolean",
"description": "When true, sets LMDB noSync to disable LMDB's commit disk sync for faster writes (risk of losing recent commits on OS crash or power loss); when false, LMDB keeps commit disk sync enabled (overlapped on Linux and macOS by default). Applies only to the LMDB storage engine; this setting has no effect on RocksDB, where Harper does not fsync individual commits to disk (transaction logs are synced on memtable flush rather than per commit). Default: false"
Expand Down
127 changes: 126 additions & 1 deletion integrationTests/resources/full-text-search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,7 @@ suite('deployed full-text fields and native search', (ctx: ContextWithHarper) =>
const packageJson = JSON.parse(await readFile(resolve(import.meta.dirname, '../..', 'package.json'), 'utf8'));
assert.strictEqual(info.packageVersion, packageJson.optionalDependencies['@harperfast/fulltext']);
assert.strictEqual(info.mutationBatchApiVersion, 5);
assert.strictEqual(info.queryApiVersion, 3);
assert.strictEqual(info.queryApiVersion, 4);
assert.ok(info.queryClassIsolationMinimumSearchThreads > 0);
const description = await operation({ operation: 'describe_table', schema: 'data', table: 'Product' });
const index = description.full_text_indexes.find(({ name }) => name === 'catalogSearch');
Expand Down Expand Up @@ -284,6 +284,43 @@ suite('deployed full-text fields and native search', (ctx: ContextWithHarper) =>
);
});

test('rejects removed paging functions before a collection delete can run', async () => {
for (const option of ['continuation()', 'continuation(token)', 'closeContinuation()']) {
for (const method of ['GET', 'DELETE']) {
const response = await fetch(`${ctx.harper.httpURL}/Product/?catalogSearch=matches=pagination&${option}`, {
method,
headers: { Authorization: authorization },
signal: AbortSignal.timeout(45000),
});
assert.strictEqual(response.ok, false);
assert.strictEqual((await response.json()).code, 'SyntaxViolation');
}
}
assert.deepStrictEqual(ids(await search(query('pagination'))), PAGE_IDS);
});

test('fills REST autocomplete pages after structured filtering and beyond the first native batch', async () => {
const all = await search(query('pagination'));
const expected = all
.filter(({ category }) => category === 'even')
.slice(0, 100)
.map(({ id }) => id);
for (const comparator of ['matches_prefix', 'matches_fuzzy_prefix']) {
const results = await request(
`/Product/?catalogSearch=${comparator}=pagin&category=even&limit(100)&select(id,title)`
);
assert.deepStrictEqual(
results.map(({ id }) => id),
expected
);
const larger = await request(`/Product/?catalogSearch=${comparator}=pagin&limit(150)&select(id,title)`);
assert.deepStrictEqual(
larger.map(({ id }) => id),
all.slice(0, 150).map(({ id }) => id)
);
}
});

test('preserves source permissions and row filters for non-admin searches', async () => {
await operation({
operation: 'add_role',
Expand Down Expand Up @@ -578,4 +615,92 @@ suite('deployed full-text fields and native search', (ctx: ContextWithHarper) =>
await waitForReady('Product');
await waitForIds(query('waterproof'), ['one', 'two']);
});
test(
'applies full-text request limits through set_configuration and worker restart',
{ skip: process.platform === 'win32' ? 'HTTP worker restart on Windows: HarperFast/harper#549' : false },
async () => {
async function restartWorkers() {
const { job_id } = await operation({ operation: 'restart_service', service: 'http_workers' });
assert(job_id);
const deadline = Date.now() + 60000;
let status;
do {
let job;
try {
[job] = await operation({ operation: 'get_job', id: job_id });
} catch (error) {
const code = (error as any)?.cause?.code ?? (error as any)?.code;
if (!['ECONNREFUSED', 'ECONNRESET', 'UND_ERR_SOCKET'].includes(code)) throw error;
assert.strictEqual(
ctx.harper.process.exitCode,
null,
`Harper exited during worker restart: ${ctx.harper.process.exitCode}`
);
assert.strictEqual(
ctx.harper.process.signalCode,
null,
`Harper stopped during worker restart: ${ctx.harper.process.signalCode}`
);
}
status = job?.status;
if (status === 'COMPLETE' || status === 'ERROR') break;
await delay(25);
} while (Date.now() < deadline);
assert.strictEqual(status, 'COMPLETE');
await waitForReady('Product');
}
async function assertBudget(name: string) {
let status: number;
let body: string;
const deadline = Date.now() + 10000;
do {
const response = await fetch(`${ctx.harper.httpURL}/SearchProduct/`, {
method: 'POST',
headers: { 'Authorization': authorization, 'Content-Type': 'application/json' },
body: JSON.stringify(query('shoe', {}, { limit: 2 })),
signal: AbortSignal.timeout(5000),
});
status = response.status;
body = await response.text();
if (status === 400 && body.includes(name)) break;
await delay(25);
} while (Date.now() < deadline);
assert.strictEqual(status, 400, body);
assert.ok(body.includes(name), body);
}
const original = (await operation({ operation: 'get_configuration' })).storage.fullText;
assert.ok(original, 'Installed configuration must include full-text defaults');
try {
await operation({
operation: 'set_configuration',
storage_fullText_maxRawHitsPerRequest: 500,
storage_fullText_maxResultsPerRequest: 1,
storage_fullText_requestBudgetMilliseconds: 1000,
});
const updated = (await operation({ operation: 'get_configuration' })).storage.fullText;
assert.strictEqual(updated.maxRawHitsPerRequest, 500);
assert.strictEqual(updated.maxResultsPerRequest, 1);
assert.strictEqual(updated.requestBudgetMilliseconds, 1000);
await restartWorkers();
await assertBudget('maxResultsPerRequest');
await operation({
operation: 'set_configuration',
storage_fullText_maxRawHitsPerRequest: 1,
storage_fullText_maxResultsPerRequest: 500,
});
await restartWorkers();
await assertBudget('maxRawHitsPerRequest');
} finally {
if (ctx.harper.process.exitCode === null && ctx.harper.process.signalCode === null) {
await operation({
operation: 'set_configuration',
storage_fullText_maxRawHitsPerRequest: original.maxRawHitsPerRequest,
storage_fullText_maxResultsPerRequest: original.maxResultsPerRequest,
storage_fullText_requestBudgetMilliseconds: original.requestBudgetMilliseconds,
});
await restartWorkers();
}
}
}
);
});
Loading
Loading