diff --git a/.changeset/list-pagination.md b/.changeset/list-pagination.md new file mode 100644 index 0000000..1d0d2ea --- /dev/null +++ b/.changeset/list-pagination.md @@ -0,0 +1,7 @@ +--- +"@ocas/core": minor +"@ocas/fs": minor +"@ocas/cli": minor +--- + +**Breaking:** `listByType()`, `listMeta()`, `listSchemas()` now return `ListEntry[]` (with `hash`, `created`, `updated`) instead of `Hash[]`. All list commands support `--sort`, `--limit`, `--offset`, `--desc` flags. `var list` supports the same pagination options via SQL. diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 6d49d6a..c3a9f24 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -238,7 +238,9 @@ function parseTagsLabels(args: string[]): { * Validates each flag and dies with a clear error on invalid values. */ function parseListOptions(): ListOptions { - const opts: ListOptions = {}; + // Default limit applied at the CLI layer only; core treats undefined as + // "no limit" so internal callers (e.g. gc) can fetch full result sets. + const opts: ListOptions = { limit: 100 }; const sortFlag = flags.sort; if (sortFlag !== undefined) { if (typeof sortFlag !== "string") { @@ -988,7 +990,6 @@ async function cmdTemplateList(_args: string[]): Promise { const variables = varStore.list({ namePrefix: "@ocas/template/text/", schema: stringHash, - limit: Number.MAX_SAFE_INTEGER, }); const templates = variables.map((v) => ({ diff --git a/packages/core/src/gc.ts b/packages/core/src/gc.ts index 06a29c1..19de8cd 100644 --- a/packages/core/src/gc.ts +++ b/packages/core/src/gc.ts @@ -17,9 +17,9 @@ export interface GcStats { * - Schema preservation: schemas of reachable nodes are also marked */ export function gc(store: Store, varStore: VariableStore): GcStats { - // Get all variables (no filters → global). Pass a very large limit so the - // API-level default of 100 does not silently truncate gc roots. - const variables = varStore.list({ limit: Number.MAX_SAFE_INTEGER }); + // Get all variables (no filters → global). Omit `limit` so the full + // variable set is returned for use as gc roots. + const variables = varStore.list(); const scanned = variables.length; // Collect unique root hashes from all variables diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index d136879..11c273c 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -25,7 +25,6 @@ export { export { createMemoryStore } from "./store.js"; export { type CasNode, - DEFAULT_LIST_LIMIT, type Hash, type ListEntry, type ListOptions, diff --git a/packages/core/src/list-pagination.test.ts b/packages/core/src/list-pagination.test.ts index 20eff98..943fb60 100644 --- a/packages/core/src/list-pagination.test.ts +++ b/packages/core/src/list-pagination.test.ts @@ -103,11 +103,13 @@ describe("listByType - pagination + sort + timestamps", () => { expect(store.listByType(m, { offset: 100 })).toEqual([]); }); - test("A9. default limit is 100", async () => { + test("A9. core has no default limit (returns all)", async () => { const store = createMemoryStore(); const m = await store[BOOTSTRAP_STORE]({ type: "object" }); await putN(store, m, 150, 0); - expect(store.listByType(m)).toHaveLength(100); + // No CLI-layer cap; with 150 nodes of type m (plus m itself which is + // self-typed), the full set is returned. + expect(store.listByType(m)).toHaveLength(151); }); test("A10. desc + offset + limit combined", async () => { @@ -136,12 +138,12 @@ describe("listMeta / listSchemas - pagination", () => { expect(typeof e.updated).toBe("number"); }); - test("B2. listMeta default limit 100", async () => { + test("B2. listMeta has no default limit (returns all)", async () => { const store = createMemoryStore(); for (let i = 0; i < 150; i++) { await store[BOOTSTRAP_STORE]({ type: "object", i }); } - expect(store.listMeta()).toHaveLength(100); + expect(store.listMeta()).toHaveLength(150); }); test("B3. listMeta limit/offset/desc", async () => { diff --git a/packages/core/src/list-utils.ts b/packages/core/src/list-utils.ts index 7faceb5..5897170 100644 --- a/packages/core/src/list-utils.ts +++ b/packages/core/src/list-utils.ts @@ -1,13 +1,9 @@ -import { - DEFAULT_LIST_LIMIT, - type Hash, - type ListEntry, - type ListOptions, -} from "./types.js"; +import type { Hash, ListEntry, ListOptions } from "./types.js"; /** * Apply sort/desc/offset/limit to an array of `ListEntry` records. - * Default sort is by `created` ascending; default limit is `DEFAULT_LIST_LIMIT`. + * Default sort is by `created` ascending. If `limit` is omitted, all entries + * (after offset) are returned. * * Tiebreaker is the entry hash (lexicographic ascending) so ordering remains * deterministic for entries sharing the same timestamp. @@ -18,7 +14,7 @@ export function applyListOptions( ): ListEntry[] { const sort = options?.sort ?? "created"; const desc = options?.desc ?? false; - const limit = options?.limit ?? DEFAULT_LIST_LIMIT; + const limit = options?.limit; const offset = options?.offset ?? 0; const sorted = [...entries].sort((a, b) => { @@ -30,8 +26,11 @@ export function applyListOptions( return desc ? (a.hash < b.hash ? 1 : -1) : a.hash < b.hash ? -1 : 1; }); - if (limit <= 0) return []; - return sorted.slice(offset, offset + limit); + if (limit !== undefined) { + if (limit <= 0) return []; + return sorted.slice(offset, offset + limit); + } + return sorted.slice(offset); } /** diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts index 66d801f..4e8ed7a 100644 --- a/packages/core/src/types.ts +++ b/packages/core/src/types.ts @@ -44,11 +44,6 @@ export type ListEntry = { updated: number; }; -/** - * Default limit applied when callers omit `limit` from list options. - */ -export const DEFAULT_LIST_LIMIT = 100; - /** * Content-addressable store interface. * Self-referencing nodes are created only via bootstrap(). diff --git a/packages/core/src/variable-list-pagination.test.ts b/packages/core/src/variable-list-pagination.test.ts index d37d1ee..3f4b8e4 100644 --- a/packages/core/src/variable-list-pagination.test.ts +++ b/packages/core/src/variable-list-pagination.test.ts @@ -82,10 +82,10 @@ describe("VariableStore.list - pagination + sort", () => { ).toHaveLength(3); }); - test("D5. default limit is 100", async () => { + test("D5. core has no default limit (returns all)", async () => { await setN("big", 105, 0); const list = varStore.list({ namePrefix: "big-" }); - expect(list).toHaveLength(100); + expect(list).toHaveLength(105); }); test("D6. pagination applied AFTER namePrefix/schema filters", async () => { diff --git a/packages/core/src/variable-store.ts b/packages/core/src/variable-store.ts index 46076ba..240ec93 100644 --- a/packages/core/src/variable-store.ts +++ b/packages/core/src/variable-store.ts @@ -1,6 +1,5 @@ import { Database } from "bun:sqlite"; import type { Hash, ListSort, Store } from "./types.js"; -import { DEFAULT_LIST_LIMIT } from "./types.js"; import type { Variable } from "./variable.js"; /** @@ -608,7 +607,6 @@ export class VariableStore { // Remove all schema variants for this name const variants = this.list({ exactName: name, - limit: Number.MAX_SAFE_INTEGER, }); if (variants.length === 0) { @@ -652,10 +650,10 @@ export class VariableStore { const filterLabels = options?.labels ?? []; const sort = options?.sort ?? "created"; const desc = options?.desc ?? false; - const limit = options?.limit ?? DEFAULT_LIST_LIMIT; + const limit = options?.limit; const offset = options?.offset ?? 0; - if (limit <= 0) return []; + if (limit !== undefined && limit <= 0) return []; // Build query with filters let query = ` @@ -713,8 +711,14 @@ export class VariableStore { const direction = desc ? "DESC" : "ASC"; // Tiebreaker: name ASC for stable ordering across same-ms timestamps query += ` ORDER BY ${sortColumn} ${direction}, v.name ASC`; - query += " LIMIT ? OFFSET ?"; - params.push(limit, offset); + if (limit !== undefined) { + query += " LIMIT ? OFFSET ?"; + params.push(limit, offset); + } else if (offset > 0) { + // SQLite requires LIMIT when using OFFSET; use -1 to mean "no limit". + query += " LIMIT -1 OFFSET ?"; + params.push(offset); + } const stmt = this.db.prepare(query); const rows = stmt.all(...params) as Array<{