diff --git a/docs/doc/00-overview.md b/docs/doc/00-overview.md index 4c604b2..1122910 100644 --- a/docs/doc/00-overview.md +++ b/docs/doc/00-overview.md @@ -40,7 +40,7 @@ Use ColQL when: Avoid ColQL when: - you need durable storage, transactions, joins, or SQL -- row indexes must be stable identifiers +- row indexes must be stable external identifiers - every query requires arbitrary sorting or grouping - you need concurrent writers or multi-process coordination @@ -59,6 +59,8 @@ Avoid ColQL when: - Runtime validation and structured `ColQLError` failures - Binary serialization and deserialization +Indexes are derived performance structures. Query results must be the same whether ColQL uses an index or a full scan. + ## Quick Example ```ts diff --git a/docs/doc/03-inserts-and-bulk-loading.md b/docs/doc/03-inserts-and-bulk-loading.md index a466d88..f9d496d 100644 --- a/docs/doc/03-inserts-and-bulk-loading.md +++ b/docs/doc/03-inserts-and-bulk-loading.md @@ -24,7 +24,7 @@ users.insertMany([ ]); ``` -`insertMany` validates every row before inserting any row. If one row is invalid, the table is not partially mutated. +`insertMany` validates every row before inserting any row. If one row is invalid, the table is not partially mutated. It is also optimized for bulk insertion, so prefer it over repeated `insert` calls when loading batches. ```ts try { diff --git a/docs/doc/04-querying.md b/docs/doc/04-querying.md index ca6706d..9c8b81a 100644 --- a/docs/doc/04-querying.md +++ b/docs/doc/04-querying.md @@ -10,6 +10,21 @@ users.where("status", "=", "active"); users.where("is_active", "=", true); ``` +Object predicates are also supported: + +```ts +people.where({ + age: { gt: 25 }, + active: true, +}); + +people.where({ + country: { in: ["TR", "US"] }, +}); +``` + +Object `where` is syntactic sugar over the same structured predicates as tuple `where(column, operator, value)`. It can still use equality and sorted indexes when the translated predicates are indexable. + Supported operators: ```txt @@ -25,6 +40,35 @@ not in Range operators (`>`, `>=`, `<`, `<=`) are supported for numeric columns. Equality and membership are supported for numeric, dictionary, and boolean columns, subject to validation. +Object predicates use these operator names: + +```ts +users.where({ age: { eq: 25 } }); +users.where({ age: { gt: 25, lte: 65 } }); +users.where({ country: { in: ["TR", "US"] } }); +``` + +Numeric columns support `eq`, `gt`, `gte`, `lt`, `lte`, and `in`. Boolean and dictionary columns support equality/default values and `in`. + +## Callback Filters + +Use `filter(fn)` as a full-scan escape hatch when a predicate is easier to express in TypeScript: + +```ts +users.filter((row) => row.age > 25); +``` + +Callback filters run after structured predicates: + +```ts +const rows = users + .where({ status: "active" }) + .filter((row) => row.age > 25) + .toArray(); +``` + +`filter(fn)` is not index-aware. Structured predicates run first; callback filters then run as a full-scan callback pass over rows that remain eligible. + ## Membership Helpers ```ts @@ -90,4 +134,6 @@ for (const row of users.where("status", "=", "active")) { Without a usable index, queries scan row indexes from `0` to `rowCount - 1`. If an equality or sorted index exists, ColQL may use it automatically when the planner estimates the candidate set is selective enough. Broad indexed queries may still fall back to scan to avoid index overhead. +Indexes and planner choices affect performance only. A query must return the same result whether ColQL uses an index or a full scan. + See [Equality Indexes](./06-indexing.md) and [Sorted Indexes](./07-sorted-indexes.md). diff --git a/docs/doc/06-indexing.md b/docs/doc/06-indexing.md index d00f77c..391f1d8 100644 --- a/docs/doc/06-indexing.md +++ b/docs/doc/06-indexing.md @@ -1,6 +1,6 @@ # Equality Indexes -Equality indexes are optional derived structures for selective equality and membership queries. +Equality indexes are optional derived performance structures for selective equality and membership queries. A query must return the same result whether ColQL uses an index or a full scan. ```ts users.createIndex("id"); @@ -25,7 +25,7 @@ users.rebuildIndexes(); ## Supported Columns and Operators -Equality indexes support numeric and dictionary columns. Boolean columns are not supported by equality indexes. +Equality indexes support numeric and dictionary columns. Boolean columns are not supported by equality indexes because scanning low-cardinality boolean values is often as efficient as indexing them. Indexed operators: @@ -38,13 +38,13 @@ Not indexed: - `!=` - `not in` - boolean columns -- compound predicates as a combined compound index +- multi-column compound indexes -Queries can still use unsupported predicates; they scan instead. +ColQL does not build a combined index for compound predicates. Multiple predicates are still combined at query time. Queries can still use unsupported predicates; they scan instead. Fallback to scan affects performance only, not correctness. ## Planner Behavior -ColQL uses a cost-aware planner. If an index exists but would return too many candidate rows, the planner can fall back to a scan. This avoids allocating or iterating a broad index candidate set when a scan is likely cheaper. +ColQL uses a cost-aware planner. If an index exists but would return too many candidate rows, the planner can fall back to a scan. This avoids allocating or iterating a broad index candidate set when a scan is likely cheaper. Planner decisions affect performance only, not query results. Indexes are most useful for selective predicates: @@ -62,7 +62,7 @@ users.where("status", "in", ["active", "passive"]).count(); ## Dirty and Lazy Rebuilds -Deletes and updates can change row indexes or indexed values. ColQL marks existing indexes dirty after nonzero mutations and rebuilds them lazily when an indexed query needs them. The first indexed query after a mutation may be slower than later queries. +Inserts, deletes, and updates can change internal row positions or indexed values. Row positions are not stable IDs and should not be used as external identifiers. If stable identity is required, define and index an ID column. ColQL marks existing indexes dirty after nonzero mutations. When an indexed query requires a dirty index, ColQL rebuilds it before use. The first indexed query after a mutation may be slower than later queries. You can rebuild explicitly: @@ -75,7 +75,7 @@ users.rebuildIndex("status"); ## Serialization -Indexes are not serialized. They are derived data and can be recreated: +Indexes are not serialized. They are derived performance data and can be recreated: ```ts const restored = table.deserialize(buffer); diff --git a/docs/doc/07-sorted-indexes.md b/docs/doc/07-sorted-indexes.md index e7e81bc..e86fa7e 100644 --- a/docs/doc/07-sorted-indexes.md +++ b/docs/doc/07-sorted-indexes.md @@ -1,6 +1,6 @@ # Sorted Indexes -Sorted indexes accelerate selective numeric range queries. +Sorted indexes are optional derived performance structures that accelerate selective numeric range queries. A query must return the same result whether ColQL uses a sorted index or a full scan. ```ts users.createSortedIndex("age"); @@ -23,11 +23,11 @@ users.rebuildSortedIndex("age"); users.rebuildIndexes(); ``` -Sorted indexes are separate from equality indexes because they store row IDs ordered by numeric column value instead of buckets by exact value. +Sorted indexes are separate from equality indexes because they store internal row positions ordered by numeric column value instead of buckets by exact value. ## Supported Columns and Operators -Sorted indexes are numeric-only. +Sorted indexes are numeric-only. Dictionary and boolean columns do not support sorted range indexes. Supported range operators: @@ -36,11 +36,11 @@ Supported range operators: - `<` - `<=` -Equality on a numeric column can use an equality index, not a sorted index. +Equality on a numeric column can use an equality index, not a sorted index. Multi-column compound indexes are not supported; multiple predicates are combined at query time. ## Planner Behavior -The planner estimates the number of matching rows from sorted-index bounds. If the range is selective enough, ColQL scans the candidate row IDs. If the range is broad, ColQL may fall back to a table scan. +The planner estimates the number of matching rows from sorted-index bounds. If the range is selective enough, ColQL scans the candidate row positions. If the range is broad, ColQL may fall back to a table scan. Planner decisions affect performance only, not query results. ```ts users.createSortedIndex("score"); @@ -49,11 +49,11 @@ const highScores = users.where("score", ">", 900).toArray(); const manyRows = users.where("score", ">", 10).count(); // may scan ``` -Candidate row IDs are returned in scan order so query output preserves logical row order. +Candidate row positions are returned in scan order so query output preserves logical row order. ## Dirty and Lazy Rebuilds -Sorted indexes are marked dirty after inserts, deletes, and updates. They are rebuilt lazily when a query needs them, or eagerly with: +Sorted indexes are marked dirty after inserts, deletes, and updates. When an indexed query requires a dirty sorted index, ColQL rebuilds it before use. Dirty sorted indexes are not used to return stale results. You can also rebuild eagerly with: ```ts users.rebuildSortedIndex("age"); diff --git a/docs/doc/08-mutations.md b/docs/doc/08-mutations.md index 7dd1d6e..97840c2 100644 --- a/docs/doc/08-mutations.md +++ b/docs/doc/08-mutations.md @@ -9,6 +9,9 @@ users.update(rowIndex, partialRow); users.where(...).update(partialRow); users.where(...).delete(); +users.updateMany(predicate, partialRow); +users.deleteMany(predicate); + users.updateWhere(column, operator, value, partialRow); users.deleteWhere(column, operator, value); ``` @@ -29,6 +32,8 @@ users.delete(rowIndex); // returns the table instance `users.update(rowIndex, partialRow)` returns `{ affectedRows: 1 }` when successful. Predicate update/delete return `{ affectedRows: number }`; no-match predicate mutations return `{ affectedRows: 0 }`. +`updateMany` and `deleteMany` are preferred table-level convenience wrappers for common predicate mutations. Existing query mutation APIs remain available, and `updateWhere`/`deleteWhere` remain legacy convenience aliases. No mutation APIs are removed. + ## Single-Row Update ```ts @@ -46,6 +51,15 @@ const result = users.updateWhere("status", "=", "passive", { }); ``` +Object predicate form: + +```ts +const result = users.updateMany( + { status: "passive", age: { gte: 18 } }, + { status: "active" }, +); +``` + Query form: ```ts @@ -72,6 +86,12 @@ users const result = users.deleteWhere("age", "<", 18); ``` +Object predicate form: + +```ts +const result = users.deleteMany({ status: "archived" }); +``` + Query form: ```ts @@ -82,7 +102,7 @@ const result = users .delete(); ``` -Predicate deletes physically remove rows. Row indexes after deleted rows may shift. +Predicate deletes physically remove rows. Row indexes are not stable external identifiers and may shift after mutations. ## Safety Rules @@ -96,6 +116,8 @@ ColQL applies mutation safety rules internally: - nonzero update/delete mutations mark existing indexes dirty - incremental index maintenance is not attempted +Dirty indexes are rebuilt before an indexed query uses them, so index dirtiness affects rebuild cost, not query correctness. + Snapshotting matters when an update changes the predicate column: ```ts diff --git a/docs/doc/09-physical-deletes.md b/docs/doc/09-physical-deletes.md index 1c1962b..026dee4 100644 --- a/docs/doc/09-physical-deletes.md +++ b/docs/doc/09-physical-deletes.md @@ -10,7 +10,7 @@ users.delete(3); ## Row Index Semantics -Logical row order is preserved, but row indexes after the deleted row may shift. +Logical row order is preserved, but row indexes after the deleted row may shift. Row indexes are internal positions, not stable IDs. ```ts const before = users.get(4); @@ -46,7 +46,7 @@ Descending deletion avoids accidentally skipping rows as lower row indexes shift ## Indexes After Delete -Deletes mark existing equality and sorted indexes dirty. ColQL rebuilds dirty indexes lazily on the next indexed query, or explicitly: +Deletes mark existing equality and sorted indexes dirty. If an indexed query needs a dirty index, ColQL rebuilds it before use. Dirty indexes are not used to return stale results. You can also rebuild explicitly: ```ts users.deleteWhere("age", "<", 18); diff --git a/docs/doc/10-error-handling.md b/docs/doc/10-error-handling.md index 301c8bf..41f096e 100644 --- a/docs/doc/10-error-handling.md +++ b/docs/doc/10-error-handling.md @@ -48,6 +48,7 @@ Query errors: - `COLQL_INVALID_COLUMN` - `COLQL_INVALID_OPERATOR` +- `COLQL_INVALID_PREDICATE` - `COLQL_INVALID_LIMIT` - `COLQL_INVALID_OFFSET` - `COLQL_INVALID_ROW_INDEX` @@ -81,6 +82,15 @@ users.where("status", ">", "active"); // COLQL_INVALID_OPERATOR because range operators require numeric columns ``` +Invalid object predicate: + +```ts +users.where({}); +// COLQL_INVALID_PREDICATE +``` + +`COLQL_INVALID_PREDICATE` is thrown for empty object predicates, invalid object predicate operators, and invalid predicate shapes. + Invalid row index: ```ts diff --git a/docs/doc/11-serialization.md b/docs/doc/11-serialization.md index ecac257..ad4caa2 100644 --- a/docs/doc/11-serialization.md +++ b/docs/doc/11-serialization.md @@ -27,7 +27,7 @@ Indexes are not serialized: - equality indexes - sorted indexes -They are derived data and can be rebuilt after deserialization. +They are derived performance data and can be rebuilt after deserialization. Recreating indexes after deserialization affects performance only, not query correctness. ```ts const restored = table.deserialize(buffer); diff --git a/docs/doc/12-memory-model.md b/docs/doc/12-memory-model.md index a85f9a4..b922ff2 100644 --- a/docs/doc/12-memory-model.md +++ b/docs/doc/12-memory-model.md @@ -36,12 +36,12 @@ Chunking lets columns grow and physically delete rows without depending on one g ## Index Memory -Indexes are separate derived structures: +Indexes are separate derived performance structures: -- equality indexes store row-ID buckets by value -- sorted indexes store row IDs sorted by numeric value +- equality indexes store row-position buckets by value +- sorted indexes store row positions sorted by numeric value -Indexes improve selected query shapes but increase memory. Drop indexes if memory matters more than indexed lookup speed: +Indexes improve selected query shapes but increase memory. They do not change query correctness; the same query must return the same result through an index or a full scan. Drop indexes if memory matters more than indexed lookup speed: ```ts users.dropIndex("status"); @@ -89,6 +89,7 @@ For very broad changes, expect temporary row-index snapshot memory. - Use indexes for selective hot queries. - Avoid indexes for columns with low selectivity unless queries prove useful. - Drop indexes to recover derived-memory overhead. +- Expect the first indexed query after mutation to include lazy rebuild cost if the needed index is dirty. - Avoid `toArray()` for huge result sets when counting or streaming is enough. - Remember that `heapUsed` alone can under-report typed-array storage; inspect `arrayBuffers` too. diff --git a/docs/doc/13-performance-and-benchmarks.md b/docs/doc/13-performance-and-benchmarks.md index d9dad6e..7279852 100644 --- a/docs/doc/13-performance-and-benchmarks.md +++ b/docs/doc/13-performance-and-benchmarks.md @@ -65,13 +65,13 @@ users.createIndex("id"); users.where("id", "=", 123).first(); ``` -Broad predicates may fall back to scan by planner choice. This is expected, not a failed index. +Broad predicates may fall back to scan by planner choice. This is expected, not a failed index. Planner choices affect performance only, not query results. In the same local run, `benchmark:indexed` showed selective `id = 99990` queries benefiting from the equality index, while `status in all` was close to scan time because the planner avoids broad index work. This is the expected tradeoff: indexes help selective lookups and cost memory. `benchmark:range` showed sorted indexes helping selective ranges such as `age > 90`, while broad `age > 10` was similar to scan. It also showed that combining a selective equality index with an additional range filter can be much faster than scanning the broad range first. -`benchmark:optimizer` measures the planner choosing the smallest useful indexed candidate source and then applying remaining filters. +`benchmark:optimizer` measures the planner choosing the smallest useful indexed candidate source and then applying remaining filters. Multiple predicates are combined at query time; ColQL does not build multi-column compound indexes. ## Interpreting Delete and Mutation Benchmarks @@ -87,7 +87,7 @@ The delete benchmark separates phases: - materialized query output - index drop -The first indexed query after mutation may include lazy index rebuild cost. +The first indexed query after mutation may include lazy index rebuild cost. Dirty indexes are rebuilt before use and are not used to return stale results. In the local delete/mutation run, the first indexed query after dirtying indexes was much slower than the second indexed query because it paid lazy rebuild cost. The benchmark also shows `toArray()` as a separate memory phase because it materializes row objects. diff --git a/docs/doc/14-typescript-type-safety.md b/docs/doc/14-typescript-type-safety.md index c4f54d1..6758a4a 100644 --- a/docs/doc/14-typescript-type-safety.md +++ b/docs/doc/14-typescript-type-safety.md @@ -51,6 +51,57 @@ users.where("status", "=", "deleted"); // error with literal dictionary Runtime validation still runs. +## Object Predicate Typing + +Object predicates are typed from the table schema: + +```ts +users.where({ + age: { gt: 18, lte: 65 }, + status: { in: ["active"] }, + is_active: true, +}); +``` + +Numeric columns allow `eq`, `gt`, `gte`, `lt`, `lte`, and `in`: + +```ts +users.where({ age: 25 }); +users.where({ age: { eq: 25, gt: 18, in: [25, 30] } }); +``` + +Boolean columns allow default equality, `eq`, and `in`: + +```ts +users.where({ is_active: true }); +users.where({ is_active: { eq: false, in: [true, false] } }); +``` + +Dictionary columns allow default equality, `eq`, and `in`: + +```ts +users.where({ status: "active" }); +users.where({ status: { eq: "passive", in: ["active"] } }); +``` + +Range operators on boolean and dictionary columns are compile-time errors: + +```ts +users.where({ status: { gt: "active" } }); // error +users.where({ is_active: { lt: true } }); // error +``` + +## Callback Filter Typing + +`filter(fn)` receives a typed full row and must return a boolean: + +```ts +users.filter((row) => row.age > 18 && row.status === "active"); + +users.filter((row) => row.email === "x"); // error: unknown column +users.filter((row) => row.age); // error: must return boolean +``` + ## Select Typing ```ts diff --git a/docs/doc/15-limitations-and-design-decisions.md b/docs/doc/15-limitations-and-design-decisions.md index c9c6577..a226396 100644 --- a/docs/doc/15-limitations-and-design-decisions.md +++ b/docs/doc/15-limitations-and-design-decisions.md @@ -2,7 +2,7 @@ ColQL intentionally keeps a narrow, explicit feature set. -ColQL v0.1.x aims to keep the public API reasonably stable, but breaking changes may still happen before 1.0.0. +ColQL aims to keep the public API reasonably stable, but breaking changes may still happen before 1.0.0. ## Not Included @@ -33,7 +33,7 @@ Adding SQL, joins, transactional semantics, or automatic indexing would make the ## Row Indexes Are Not Stable IDs -Physical deletes shift row indexes after the deleted row. Do not store row indexes as permanent identifiers. Use an explicit ID column: +Row indexes are internal positions, not stable external identifiers. Inserts, updates, and deletes may change row positions; physical deletes shift row indexes after the deleted row. Use an explicit ID column for stable identity: ```ts const users = table({ @@ -44,9 +44,9 @@ const users = table({ ## Indexes Are Derived -Equality and sorted indexes are optional and not serialized. They are rebuilt lazily after mutations or explicitly by the user. +Equality and sorted indexes are optional derived structures and are not serialized. They affect performance only, not correctness. A query must return the same result through an index or a full scan. -This avoids complex incremental row-ID maintenance, especially around physical deletes. +Dirty indexes are rebuilt before use or explicitly by the user. This avoids complex incremental row-position maintenance, especially around physical deletes. ## Mutation Semantics Are Safety-Oriented diff --git a/docs/doc/16-api-reference.md b/docs/doc/16-api-reference.md index 88a439e..51060cf 100644 --- a/docs/doc/16-api-reference.md +++ b/docs/doc/16-api-reference.md @@ -6,19 +6,47 @@ This is a factual summary of the public API. See the topic docs for deeper behav ```ts import { table, column, ColQLError } from "@colql/colql"; -import type { MutationResult, Operator, RowForSchema, Schema } from "@colql/colql"; +import type { + MutationResult, + ObjectWherePredicate, + Operator, + QueryHook, + QueryInfo, + RowForSchema, + RowPredicate, + Schema, + TableOptions, +} from "@colql/colql"; ``` ## Table Creation ```ts const users = table(schema); +const instrumented = table(schema, { onQuery: (info) => console.log(info) }); const restored = table.deserialize(buffer); ``` `table(schema)` returns a `Table` instance. +`table(schema, options)` accepts compatible table options such as `onQuery`. `table.deserialize(input)` accepts an `ArrayBuffer` or `Uint8Array` and returns a table. +`onQuery` is called by terminal query operations such as `toArray`, `first`, `count`, aggregations, and query mutations. Query construction itself is not instrumented. + +```ts +type QueryInfo = { + duration: number; + rowsScanned: number; + indexUsed: boolean; +}; + +type QueryHook = (info: QueryInfo) => void; + +type TableOptions = { + onQuery?: QueryHook; +}; +``` + ## Columns ```ts @@ -57,14 +85,16 @@ users.getComparableValue(rowIndex, column); users.getNumericValue(rowIndex, numericColumn); ``` -These are mainly useful for advanced integrations and diagnostics. Most application code should use query and row APIs. +These are mainly useful for advanced integrations and diagnostics. Row indexes are internal positions, not stable external IDs. Most application code should use query and row APIs with an explicit ID column when stable identity is required. ## Query Construction ```ts users.where(column, operator, value); +users.where(objectPredicate); users.whereIn(column, values); users.whereNotIn(column, values); +users.filter(callback); users.select(columns); users.limit(n); users.offset(n); @@ -73,6 +103,13 @@ users.query(); `query()` creates an unfiltered query over the table. The table-level helpers above are the usual entrypoints for application code. +```ts +users.where({ age: { gt: 25 }, status: "active" }); +users.filter((row) => row.age > 25); +``` + +`where(objectPredicate)` is structured predicate syntax and may use indexes. `filter(callback)` is a full-scan callback escape hatch, runs after structured predicates, and is not index-aware. + Operators: ```ts @@ -107,8 +144,10 @@ Queries returned by `where`, `select`, `limit`, `offset`, and `query()` support: ```ts query.where(column, operator, value); +query.where(objectPredicate); query.whereIn(column, values); query.whereNotIn(column, values); +query.filter(callback); query.select(columns); query.limit(n); query.offset(n); @@ -138,6 +177,14 @@ for (const row of query) { `query.update()` and `query.delete()` respect filters, `offset`, and `limit`. `select()` affects query output but does not restrict update payloads. +```ts +type ObjectWherePredicate = { + // column-specific object predicate shape +}; + +type RowPredicate = (row: RowForSchema) => boolean; +``` + ## Aggregations ```ts @@ -159,6 +206,8 @@ users.delete(rowIndex); // this users.update(rowIndex, partialRow); // MutationResult users.updateWhere(column, operator, value, partialRow); // MutationResult users.deleteWhere(column, operator, value); // MutationResult +users.updateMany(predicate, partialRow); // MutationResult +users.deleteMany(predicate); // MutationResult users.where(...).update(partialRow); // MutationResult users.where(...).delete(); // MutationResult @@ -182,6 +231,8 @@ users.rebuildIndex(column); // this users.rebuildIndexes(); // this ``` +Equality indexes are derived performance structures. Unsupported predicates fall back to scan without changing query results. + ## Sorted Indexes ```ts @@ -194,6 +245,8 @@ users.rebuildSortedIndex(numericColumn); // this users.rebuildIndexes(); // this ``` +Sorted indexes are numeric range indexes. They are derived performance structures and are rebuilt before use when dirty. + ## Serialization ```ts @@ -201,7 +254,7 @@ const buffer = users.serialize(); // ArrayBuffer const restored = table.deserialize(buffer); ``` -`deserialize` accepts `ArrayBuffer` or `Uint8Array`. +`deserialize` accepts `ArrayBuffer` or `Uint8Array`. Indexes are not serialized; recreate equality and sorted indexes after deserialization when indexed performance is needed. ## Diagnostics diff --git a/examples/fastify-api/.gitignore b/examples/fastify-api/.gitignore new file mode 100644 index 0000000..3c3629e --- /dev/null +++ b/examples/fastify-api/.gitignore @@ -0,0 +1 @@ +node_modules diff --git a/examples/fastify-api/README.md b/examples/fastify-api/README.md new file mode 100644 index 0000000..d259926 --- /dev/null +++ b/examples/fastify-api/README.md @@ -0,0 +1,97 @@ +# ColQL Fastify API Example + +Small process-local Fastify backend showing ColQL v0.2.0 in an HTTP API. + +The app stores users in memory. It is useful as an integration example, not as a persistent database-backed service. Restarting the process resets the data. + +## Run + +```sh +npm install +npm run dev +``` + +The server listens on `http://localhost:3000` by default. Set `PORT` to use another port. + +By default the app starts with a tiny deterministic seed. To test a larger process-local dataset, set `COLQL_EXAMPLE_SEED_SIZE`: + +```sh +COLQL_EXAMPLE_SEED_SIZE=1000000 npm run dev +``` + +There is also a shortcut: + +```sh +npm run dev:1m +``` + +The generated seed uses deterministic dictionary values for `country` and `name`, so it still exercises ColQL dictionary columns, equality indexes, and the sorted age index. + +## Test + +```sh +npm test +``` + +The tests start the Fastify app in memory and use `app.inject()` to send real HTTP requests. + +Large-dataset validation is separate from the normal test suite so everyday tests stay fast: + +```sh +npm run test:large +``` + +`test:large` starts the app with 1M generated users, performs `updateMany`, `deleteMany`, and `insertMany` through HTTP requests, verifies filtered query correctness after lazy index rebuilds, and prints latency summaries for indexed, range, broad scan, and callback-filter requests. + +Run the basic concurrent stress check with: + +```sh +npm run stress +``` + +The stress script sends 50 concurrent requests to an index-friendly structured query and checks that all responses are successful and consistent. + +Run the memory sanity check with: + +```sh +npm run memory:example +``` + +The memory script reports `heapUsed`, `rss`, and `arrayBuffers` after 1M seed, after mutations, and after repeated queries. These scripts do not enforce strict latency or memory thresholds; they are smoke validations for local machines. + +`filter(fn)` is a full-scan escape hatch and is not index-aware. On 1M rows, callback-filter requests are expected to be slower than structured indexed queries. + +## Endpoints + +- `GET /health` +- `POST /users` +- `POST /users/bulk` +- `GET /users` +- `GET /users/count` +- `PATCH /users/by-country/:country` +- `DELETE /users/inactive` +- `GET /debug/query-log` +- `GET /debug/indexes` +- `GET /debug/memory` + +## ColQL Features Demonstrated + +- `insert(row)` +- `insertMany(rows)` +- object-based `where({ ... })` +- tuple `where(column, operator, value)` +- `filter(fn)` for callback search +- `select()` projection +- `count()` +- `updateMany(predicate, partialRow)` +- `deleteMany(predicate)` +- equality indexes with `createIndex` +- sorted/range index usage with `createSortedIndex` +- query diagnostics with `onQuery` +- public memory-related counters such as `rowCount`, `capacity`, `materializedRowCount`, and `scannedRowCount` + +Example query: + +```sh +curl "http://localhost:3000/users?country=TR&minAge=25&search=mi" +``` diff --git a/examples/fastify-api/package-lock.json b/examples/fastify-api/package-lock.json new file mode 100644 index 0000000..3290882 --- /dev/null +++ b/examples/fastify-api/package-lock.json @@ -0,0 +1,2441 @@ +{ + "name": "@colql/example-fastify-api", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@colql/example-fastify-api", + "version": "0.1.0", + "dependencies": { + "fastify": "^5.6.2" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "tsx": "^4.20.6", + "typescript": "^6.0.3", + "vitest": "^4.1.5" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@fastify/ajv-compiler": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@fastify/ajv-compiler/-/ajv-compiler-4.0.5.tgz", + "integrity": "sha512-KoWKW+MhvfTRWL4qrhUwAAZoaChluo0m0vbiJlGMt2GXvL4LVPQEjt8kSpHI3IBq5Rez8fg+XeH3cneztq+C7A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0" + } + }, + "node_modules/@fastify/error": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/@fastify/error/-/error-4.2.0.tgz", + "integrity": "sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/fast-json-stringify-compiler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/@fastify/fast-json-stringify-compiler/-/fast-json-stringify-compiler-5.0.3.tgz", + "integrity": "sha512-uik7yYHkLr6fxd8hJSZ8c+xF4WafPK+XzneQDPU+D10r5X19GW8lJcom2YijX2+qtFF1ENJlHXKFM9ouXNJYgQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "fast-json-stringify": "^6.0.0" + } + }, + "node_modules/@fastify/forwarded": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@fastify/forwarded/-/forwarded-3.0.1.tgz", + "integrity": "sha512-JqDochHFqXs3C3Ml3gOY58zM7OqO9ENqPo0UqAjAjH8L01fRZqwX9iLeX34//kiJubF7r2ZQHtBRU36vONbLlw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/@fastify/merge-json-schemas": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@fastify/merge-json-schemas/-/merge-json-schemas-0.2.1.tgz", + "integrity": "sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@fastify/proxy-addr": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@fastify/proxy-addr/-/proxy-addr-5.1.0.tgz", + "integrity": "sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/forwarded": "^3.0.0", + "ipaddr.js": "^2.1.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@pinojs/redact": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/@pinojs/redact/-/redact-0.4.0.tgz", + "integrity": "sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==", + "license": "MIT" + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", + "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", + "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.5", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", + "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", + "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.5", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", + "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "@vitest/utils": "4.1.5", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", + "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", + "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.5", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/abstract-logging": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/abstract-logging/-/abstract-logging-2.0.1.tgz", + "integrity": "sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==", + "license": "MIT" + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/atomic-sleep": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/atomic-sleep/-/atomic-sleep-1.0.0.tgz", + "integrity": "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==", + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/avvio": { + "version": "9.2.0", + "resolved": "https://registry.npmjs.org/avvio/-/avvio-9.2.0.tgz", + "integrity": "sha512-2t/sy01ArdHHE0vRH5Hsay+RtCZt3dLPji7W7/MMOCEgze5b7SNDC4j5H6FnVgPkI1MTNFGzHdHrVXDDl7QSSQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/error": "^4.0.0", + "fastq": "^1.17.1" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookie": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", + "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-decode-uri-component": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fast-decode-uri-component/-/fast-decode-uri-component-1.0.1.tgz", + "integrity": "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==", + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-json-stringify": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/fast-json-stringify/-/fast-json-stringify-6.3.0.tgz", + "integrity": "sha512-oRCntNDY/329HJPlmdNLIdogNtt6Vyjb1WuT01Soss3slIdyUp8kAcDU3saQTOquEK8KFVfwIIF7FebxUAu+yA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/merge-json-schemas": "^0.2.0", + "ajv": "^8.12.0", + "ajv-formats": "^3.0.1", + "fast-uri": "^3.0.0", + "json-schema-ref-resolver": "^3.0.0", + "rfdc": "^1.2.0" + } + }, + "node_modules/fast-querystring": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/fast-querystring/-/fast-querystring-1.1.2.tgz", + "integrity": "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==", + "license": "MIT", + "dependencies": { + "fast-decode-uri-component": "^1.0.1" + } + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastify": { + "version": "5.8.5", + "resolved": "https://registry.npmjs.org/fastify/-/fastify-5.8.5.tgz", + "integrity": "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "@fastify/ajv-compiler": "^4.0.5", + "@fastify/error": "^4.0.0", + "@fastify/fast-json-stringify-compiler": "^5.0.0", + "@fastify/proxy-addr": "^5.0.0", + "abstract-logging": "^2.0.1", + "avvio": "^9.0.0", + "fast-json-stringify": "^6.0.0", + "find-my-way": "^9.0.0", + "light-my-request": "^6.0.0", + "pino": "^9.14.0 || ^10.1.0", + "process-warning": "^5.0.0", + "rfdc": "^1.3.1", + "secure-json-parse": "^4.0.0", + "semver": "^7.6.0", + "toad-cache": "^3.7.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/find-my-way": { + "version": "9.5.0", + "resolved": "https://registry.npmjs.org/find-my-way/-/find-my-way-9.5.0.tgz", + "integrity": "sha512-VW2RfnmscZO5KgBY5XVyKREMW5nMZcxDy+buTOsL+zIPnBlbKm+00sgzoQzq1EVh4aALZLfKdwv6atBGcjvjrQ==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-querystring": "^1.0.0", + "safe-regex2": "^5.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz", + "integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/json-schema-ref-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-schema-ref-resolver/-/json-schema-ref-resolver-3.0.0.tgz", + "integrity": "sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, + "node_modules/light-my-request": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/light-my-request/-/light-my-request-6.6.0.tgz", + "integrity": "sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause", + "dependencies": { + "cookie": "^1.0.1", + "process-warning": "^4.0.0", + "set-cookie-parser": "^2.6.0" + } + }, + "node_modules/light-my-request/node_modules/process-warning": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-4.0.1.tgz", + "integrity": "sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/on-exit-leak-free": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/on-exit-leak-free/-/on-exit-leak-free-2.1.2.tgz", + "integrity": "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==", + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pino": { + "version": "10.3.1", + "resolved": "https://registry.npmjs.org/pino/-/pino-10.3.1.tgz", + "integrity": "sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==", + "license": "MIT", + "dependencies": { + "@pinojs/redact": "^0.4.0", + "atomic-sleep": "^1.0.0", + "on-exit-leak-free": "^2.1.0", + "pino-abstract-transport": "^3.0.0", + "pino-std-serializers": "^7.0.0", + "process-warning": "^5.0.0", + "quick-format-unescaped": "^4.0.3", + "real-require": "^0.2.0", + "safe-stable-stringify": "^2.3.1", + "sonic-boom": "^4.0.1", + "thread-stream": "^4.0.0" + }, + "bin": { + "pino": "bin.js" + } + }, + "node_modules/pino-abstract-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pino-abstract-transport/-/pino-abstract-transport-3.0.0.tgz", + "integrity": "sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==", + "license": "MIT", + "dependencies": { + "split2": "^4.0.0" + } + }, + "node_modules/pino-std-serializers": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pino-std-serializers/-/pino-std-serializers-7.1.0.tgz", + "integrity": "sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==", + "license": "MIT" + }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/process-warning": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/process-warning/-/process-warning-5.0.0.tgz", + "integrity": "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/quick-format-unescaped": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/quick-format-unescaped/-/quick-format-unescaped-4.0.4.tgz", + "integrity": "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==", + "license": "MIT" + }, + "node_modules/real-require": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/real-require/-/real-require-0.2.0.tgz", + "integrity": "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==", + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/ret": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.5.0.tgz", + "integrity": "sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "license": "MIT" + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/safe-regex2": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/safe-regex2/-/safe-regex2-5.1.1.tgz", + "integrity": "sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT", + "dependencies": { + "ret": "~0.5.0" + }, + "bin": { + "safe-regex2": "bin/safe-regex2.js" + } + }, + "node_modules/safe-stable-stringify": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/safe-stable-stringify/-/safe-stable-stringify-2.5.0.tgz", + "integrity": "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/secure-json-parse": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/secure-json-parse/-/secure-json-parse-4.1.0.tgz", + "integrity": "sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/set-cookie-parser": { + "version": "2.7.2", + "resolved": "https://registry.npmjs.org/set-cookie-parser/-/set-cookie-parser-2.7.2.tgz", + "integrity": "sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==", + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/sonic-boom": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/sonic-boom/-/sonic-boom-4.2.1.tgz", + "integrity": "sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==", + "license": "MIT", + "dependencies": { + "atomic-sleep": "^1.0.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/thread-stream": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/thread-stream/-/thread-stream-4.0.0.tgz", + "integrity": "sha512-4iMVL6HAINXWf1ZKZjIPcz5wYaOdPhtO8ATvZ+Xqp3BTdaqtAwQkNmKORqcIo5YkQqGXq5cwfswDwMqqQNrpJA==", + "license": "MIT", + "dependencies": { + "real-require": "^0.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.1.2.tgz", + "integrity": "sha512-dAqSqE/RabpBKI8+h26GfLq6Vb3JVXs30XYQjdMjaj/c2tS8IYYMbIzP599KtRj7c57/wYApb3QjgRgXmrCukA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/toad-cache": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/toad-cache/-/toad-cache-3.7.0.tgz", + "integrity": "sha512-/m8M+2BJUpoJdgAHoG+baCwBT+tf2VraSfkBgl0Y00qIWt41DJ8R5B8nsEw0I58YwF5IZH6z24/2TobDKnqSWw==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.10", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.10.tgz", + "integrity": "sha512-rZuUu9j6J5uotLDs+cAA4O5H4K1SfPliUlQwqa6YEwSrWDZzP4rhm00oJR5snMewjxF5V/K3D4kctsUTsIU9Mw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.10", + "rolldown": "1.0.0-rc.17", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", + "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.5", + "@vitest/mocker": "4.1.5", + "@vitest/pretty-format": "4.1.5", + "@vitest/runner": "4.1.5", + "@vitest/snapshot": "4.1.5", + "@vitest/spy": "4.1.5", + "@vitest/utils": "4.1.5", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.5", + "@vitest/browser-preview": "4.1.5", + "@vitest/browser-webdriverio": "4.1.5", + "@vitest/coverage-istanbul": "4.1.5", + "@vitest/coverage-v8": "4.1.5", + "@vitest/ui": "4.1.5", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/examples/fastify-api/package.json b/examples/fastify-api/package.json new file mode 100644 index 0000000..95a9f97 --- /dev/null +++ b/examples/fastify-api/package.json @@ -0,0 +1,23 @@ +{ + "name": "@colql/example-fastify-api", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "dev": "tsx src/server.ts", + "dev:1m": "COLQL_EXAMPLE_SEED_SIZE=1000000 tsx src/server.ts", + "test": "vitest run", + "test:large": "tsx scripts/validate-large-dataset.ts", + "stress": "tsx scripts/stress.ts", + "memory:example": "node --expose-gc ./node_modules/tsx/dist/cli.mjs scripts/memory.ts" + }, + "dependencies": { + "fastify": "^5.6.2" + }, + "devDependencies": { + "@types/node": "^25.6.0", + "tsx": "^4.20.6", + "typescript": "^6.0.3", + "vitest": "^4.1.5" + } +} diff --git a/examples/fastify-api/scripts/helpers.ts b/examples/fastify-api/scripts/helpers.ts new file mode 100644 index 0000000..e46565a --- /dev/null +++ b/examples/fastify-api/scripts/helpers.ts @@ -0,0 +1,79 @@ +import type { FastifyInstance, InjectOptions } from "fastify"; + +export type TimedResult = { + readonly duration: number; + readonly value: T; +}; + +export type LatencySummary = { + readonly min: number; + readonly avg: number; + readonly max: number; + readonly p95: number; +}; + +export function time(fn: () => Promise): Promise> { + const start = performance.now(); + return fn().then((value) => ({ duration: performance.now() - start, value })); +} + +export async function injectJson( + app: FastifyInstance, + options: InjectOptions, +): Promise { + const response = await app.inject(options); + + if (response.statusCode < 200 || response.statusCode >= 300) { + throw new Error( + `${options.method ?? "GET"} ${String(options.url)} failed with ${response.statusCode}: ${response.body}`, + ); + } + + return response.json() as T; +} +export function summarize(values: readonly number[]): LatencySummary { + if (values.length === 0) { + throw new Error("Cannot summarize empty latency set."); + } + + const sorted = [...values].sort((left, right) => left - right); + const total = values.reduce((sum, value) => sum + value, 0); + const p95Index = Math.min( + sorted.length - 1, + Math.ceil(sorted.length * 0.95) - 1, + ); + + return { + min: sorted[0], + avg: total / values.length, + max: sorted[sorted.length - 1], + p95: sorted[p95Index], + }; +} + +export function formatMs(value: number): string { + return `${value.toFixed(2)}ms`; +} + +export function printLatency(label: string, values: readonly number[]): void { + const summary = summarize(values); + console.log( + `${label}: min=${formatMs(summary.min)} avg=${formatMs(summary.avg)} p95=${formatMs(summary.p95)} max=${formatMs(summary.max)}`, + ); +} + +export function forceGc(): void { + const gc = (globalThis as { gc?: () => void }).gc; + if (typeof gc === "function") { + gc(); + } +} + +export function printMemory(label: string): void { + forceGc(); + const memory = process.memoryUsage(); + const mb = (value: number) => `${(value / 1024 / 1024).toFixed(2)} MB`; + console.log( + `${label}: heapUsed=${mb(memory.heapUsed)} rss=${mb(memory.rss)} arrayBuffers=${mb(memory.arrayBuffers)}`, + ); +} diff --git a/examples/fastify-api/scripts/memory.ts b/examples/fastify-api/scripts/memory.ts new file mode 100644 index 0000000..0900a13 --- /dev/null +++ b/examples/fastify-api/scripts/memory.ts @@ -0,0 +1,40 @@ +import { buildApp } from "../src/app"; +import { injectJson, printMemory } from "./helpers"; + +const SEED_SIZE = Number(process.env.COLQL_EXAMPLE_MEMORY_SIZE ?? 1_000_000); + +async function main(): Promise { + printMemory("before seed"); + const app = buildApp({ seedSize: SEED_SIZE }); + printMemory("after seed"); + + try { + await injectJson(app, { + method: "PATCH", + url: "/users/by-country/TR", + payload: { active: true }, + }); + await injectJson(app, { method: "DELETE", url: "/users/inactive" }); + await injectJson(app, { + method: "POST", + url: "/users/bulk", + payload: { + users: [ + { id: SEED_SIZE + 1, age: 45, country: "TR", active: true, name: "Ada", score: 98.1 }, + { id: SEED_SIZE + 2, age: 52, country: "US", active: true, name: "Grace", score: 97.2 }, + ], + }, + }); + printMemory("after update/delete/insert"); + + for (let index = 0; index < 25; index += 1) { + await injectJson(app, { method: "GET", url: "/users/count?country=TR&minAge=25" }); + await injectJson(app, { method: "GET", url: "/users/count?search=da" }); + } + printMemory("after repeated queries"); + } finally { + await app.close(); + } +} + +await main(); diff --git a/examples/fastify-api/scripts/stress.ts b/examples/fastify-api/scripts/stress.ts new file mode 100644 index 0000000..75692a8 --- /dev/null +++ b/examples/fastify-api/scripts/stress.ts @@ -0,0 +1,36 @@ +import { buildApp } from "../src/app"; +import { injectJson, printLatency, time } from "./helpers"; + +const SEED_SIZE = Number(process.env.COLQL_EXAMPLE_STRESS_SIZE ?? 1_000_000); +const CONCURRENCY = Number(process.env.COLQL_EXAMPLE_STRESS_CONCURRENCY ?? 50); + +type CountResponse = { readonly count: number }; + +async function main(): Promise { + console.log(`Starting stress validation with ${SEED_SIZE.toLocaleString()} users and ${CONCURRENCY} concurrent requests.`); + const app = buildApp({ seedSize: SEED_SIZE }); + + try { + const baseline = await injectJson(app, { method: "GET", url: "/users/count?country=TR&minAge=25" }); + const started = performance.now(); + const responses = await Promise.all( + Array.from({ length: CONCURRENCY }, () => + time(() => injectJson(app, { method: "GET", url: "/users/count?country=TR&minAge=25" })), + ), + ); + const totalDuration = performance.now() - started; + const counts = new Set(responses.map((response) => response.value.count)); + + if (counts.size !== 1 || !counts.has(baseline.count)) { + throw new Error(`Concurrent responses returned inconsistent counts: ${[...counts].join(", ")}.`); + } + + printLatency("concurrent indexed structured query", responses.map((response) => response.duration)); + console.log(`total=${totalDuration.toFixed(2)}ms average=${(totalDuration / CONCURRENCY).toFixed(2)}ms`); + console.log("Stress validation completed."); + } finally { + await app.close(); + } +} + +await main(); diff --git a/examples/fastify-api/scripts/validate-large-dataset.ts b/examples/fastify-api/scripts/validate-large-dataset.ts new file mode 100644 index 0000000..caa4ab9 --- /dev/null +++ b/examples/fastify-api/scripts/validate-large-dataset.ts @@ -0,0 +1,221 @@ +import { buildApp } from "../src/app"; +import { injectJson, printLatency, printMemory, time } from "./helpers"; + +const SEED_SIZE = Number(process.env.COLQL_EXAMPLE_LARGE_SIZE ?? 1_000_000); + +type CountResponse = { readonly count: number }; +type UsersResponse = { + readonly users: readonly { + readonly id: number; + readonly name: string; + readonly age: number; + readonly country: string; + readonly active: boolean; + }[]; +}; +type MutationResponse = { readonly affectedRows: number }; +type QueryLogResponse = { readonly entries: readonly unknown[] }; +type LatencyCase = { + readonly label: string; + readonly url: string; + readonly values: number[]; +}; + +async function main(): Promise { + console.log( + `Starting Fastify ColQL app with ${SEED_SIZE.toLocaleString()} generated users.`, + ); + printMemory("before cold start"); + const coldStart = await time(async () => buildApp({ seedSize: SEED_SIZE })); + const app = coldStart.value; + console.log(`cold start: ${coldStart.duration.toFixed(2)}ms`); + printMemory("after cold start"); + + try { + const initial = await injectJson(app, { + method: "GET", + url: "/users/count", + }); + if (initial.count !== SEED_SIZE) { + throw new Error( + `Expected ${SEED_SIZE} seeded users, received ${initial.count}.`, + ); + } + + const initialCountry = await injectJson(app, { + method: "GET", + url: "/users/count?country=TR", + }); + const initialInactive = await injectJson(app, { + method: "GET", + url: "/users/count?active=false", + }); + const inactiveInUpdatedCountry = await injectJson(app, { + method: "GET", + url: "/users/count?country=TR&active=false", + }); + + const updated = await injectJson(app, { + method: "PATCH", + url: "/users/by-country/TR", + payload: { active: true, score: 99.9 }, + }); + if (updated.affectedRows !== initialCountry.count) { + throw new Error( + `Unexpected updateMany affectedRows: ${updated.affectedRows}.`, + ); + } + + const deleted = await injectJson(app, { + method: "DELETE", + url: "/users/inactive", + }); + const expectedDeleted = + initialInactive.count - inactiveInUpdatedCountry.count; + if (deleted.affectedRows !== expectedDeleted) { + throw new Error( + `Unexpected deleteMany affectedRows: ${deleted.affectedRows}; expected ${expectedDeleted}.`, + ); + } + + const insertMany = await injectJson<{ + readonly inserted: number; + readonly rowCount: number; + }>(app, { + method: "POST", + url: "/users/bulk", + payload: { + users: [ + { + id: SEED_SIZE + 1, + age: 45, + country: "TR", + active: true, + name: "Ada", + score: 98.1, + }, + { + id: SEED_SIZE + 2, + age: 52, + country: "US", + active: true, + name: "Grace", + score: 97.2, + }, + { + id: SEED_SIZE + 3, + age: 28, + country: "JP", + active: false, + name: "Ken", + score: 76.4, + }, + ], + }, + }); + if (insertMany.inserted !== 3) { + throw new Error( + `Unexpected insertMany inserted count: ${insertMany.inserted}.`, + ); + } + + const finalCount = await injectJson(app, { + method: "GET", + url: "/users/count", + }); + const expectedFinal = SEED_SIZE - expectedDeleted + 3; + if (finalCount.count !== expectedFinal) { + throw new Error( + `Expected final count ${expectedFinal}, received ${finalCount.count}.`, + ); + } + + const sample = await injectJson(app, { + method: "GET", + url: "/users?country=TR&minAge=44&limit=5", + }); + if ( + sample.users.length === 0 || + !sample.users.every((user) => user.country === "TR" && user.age > 44) + ) { + throw new Error("Filtered sampled users are not correct after mutation."); + } + + const inserted = await injectJson(app, { + method: "GET", + url: `/users?id=${SEED_SIZE + 2}`, + }); + if (!inserted.users.some((user) => user.id === SEED_SIZE + 2)) { + throw new Error("Inserted user was not queryable after insertMany."); + } + + const inactiveAfterDelete = await injectJson(app, { + method: "GET", + url: "/users/count?active=false", + }); + if (inactiveAfterDelete.count !== 1) { + throw new Error( + `Expected only the newly inserted inactive user after deleteMany, received ${inactiveAfterDelete.count}.`, + ); + } + + const latencyCases: LatencyCase[] = [ + { + label: "indexed structured query", + url: "/users/count?country=TR", + values: [], + }, + { + label: "range query", + url: "/users/count?minAge=60&maxAge=70", + values: [], + }, + { + label: "broad scan query", + url: "/users/count?active=true", + values: [], + }, + { + label: "callback filter query", + url: "/users/count?search=da", + values: [], + }, + ]; + + for (let index = 0; index < 25; index += 1) { + for (const latencyCase of latencyCases) { + latencyCase.values.push( + ( + await time(() => + injectJson(app, { + method: "GET", + url: latencyCase.url, + }), + ) + ).duration, + ); + } + } + + for (const latencyCase of latencyCases) { + printLatency(latencyCase.label, latencyCase.values); + } + + const queryLog = await injectJson(app, { + method: "GET", + url: "/debug/query-log", + }); + if (queryLog.entries.length === 0) { + throw new Error( + "Expected onQuery entries during large dataset validation.", + ); + } + + printMemory("after mutations and repeated queries"); + console.log("Large dataset validation completed."); + } finally { + await app.close(); + } +} + +await main(); diff --git a/examples/fastify-api/src/app.ts b/examples/fastify-api/src/app.ts new file mode 100644 index 0000000..c4b6c2a --- /dev/null +++ b/examples/fastify-api/src/app.ts @@ -0,0 +1,286 @@ +import Fastify, { type FastifyInstance } from "fastify"; +import { column, table, type QueryInfo, type RowForSchema } from "../../../dist/index.mjs"; + +const COUNTRIES = [ + "TR", + "US", + "DE", + "GB", + "FR", + "NL", + "ES", + "IT", + "CA", + "BR", + "JP", + "KR", + "IN", + "AU", + "SE", + "NO", + "DK", + "FI", + "PL", + "MX", +] as const; +const NAMES = [ + "Ada", + "Grace", + "Linus", + "Mina", + "Emre", + "Aylin", + "Nora", + "Ken", + "Alan", + "Katherine", + "Edsger", + "Barbara", + "Dennis", + "Radia", + "Margaret", + "Guido", + "Donald", + "Frances", + "Tim", + "Anita", + "Brendan", + "Sophie", + "James", + "Mary", + "Bjarne", + "Leslie", + "Yukihiro", + "Jean", + "Niklaus", + "Lynn", + "Martin", + "Evelyn", +] as const; + +const userSchema = { + id: column.uint32(), + age: column.uint8(), + country: column.dictionary(COUNTRIES), + active: column.boolean(), + name: column.dictionary(NAMES), + score: column.float64(), +}; + +type User = RowForSchema; +type Country = User["country"]; + +type UserInput = User; +type UserPatch = Partial>; + +type QueryLogEntry = QueryInfo & { + readonly operation: number; +}; + +const seedUsers: readonly User[] = [ + { id: 1, age: 29, country: "TR", active: true, name: "Ada", score: 91.5 }, + { id: 2, age: 41, country: "US", active: true, name: "Grace", score: 88.2 }, + { id: 3, age: 22, country: "DE", active: false, name: "Linus", score: 72.4 }, + { id: 4, age: 35, country: "TR", active: false, name: "Mina", score: 79.1 }, + { id: 5, age: 31, country: "GB", active: true, name: "Emre", score: 84.6 }, +]; + +function parseSeedSize(value: string | undefined): number { + if (value === undefined || value.trim() === "") { + return seedUsers.length; + } + + const parsed = Number(value); + if (!Number.isInteger(parsed) || parsed < 0) { + throw new Error(`COLQL_EXAMPLE_SEED_SIZE must be a non-negative integer. Received ${value}.`); + } + + return parsed; +} + +export function generateUsers(count: number): User[] { + return Array.from({ length: count }, (_unused, index) => { + const id = index + 1; + return { + id, + age: 18 + (index % 65), + country: COUNTRIES[index % COUNTRIES.length], + active: index % 3 !== 0, + name: NAMES[index % NAMES.length], + score: 50 + (index % 500) / 10, + }; + }); +} + +function createUserStore(initialUsers: readonly User[] = seedUsers) { + const queryLog: QueryLogEntry[] = []; + const users = table(userSchema, { + onQuery(info) { + queryLog.push({ ...info, operation: queryLog.length + 1 }); + }, + }); + + users.insertMany(initialUsers); + users.createIndex("country"); + users.createIndex("name"); + users.createSortedIndex("age"); + + return { users, queryLog }; +} + +function parseBoolean(value: unknown): boolean | undefined { + if (value === undefined) { + return undefined; + } + + if (value === "true" || value === true) { + return true; + } + + if (value === "false" || value === false) { + return false; + } + + return undefined; +} + +function parseNumber(value: unknown): number | undefined { + if (typeof value !== "string" || value.trim() === "") { + return undefined; + } + + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : undefined; +} + +function isCountry(value: unknown): value is Country { + return typeof value === "string" && (COUNTRIES as readonly string[]).includes(value); +} + +type UserQuery = { + readonly id?: string; + readonly country?: string; + readonly minAge?: string; + readonly maxAge?: string; + readonly active?: string; + readonly search?: string; + readonly limit?: string; +}; + +function applyUserFilters(store: ReturnType["users"], params: UserQuery) { + let usersQuery = store.query(); + + const where: { + id?: number; + country?: Country; + age?: { gt?: number; lt?: number }; + active?: boolean; + } = {}; + + const id = parseNumber(params.id); + if (id !== undefined) { + where.id = id; + } + + if (isCountry(params.country)) { + where.country = params.country; + } + + const minAge = parseNumber(params.minAge); + if (minAge !== undefined) { + where.age = { ...where.age, gt: minAge }; + } + + const maxAge = parseNumber(params.maxAge); + if (maxAge !== undefined) { + where.age = { ...where.age, lt: maxAge }; + } + + const active = parseBoolean(params.active); + if (active !== undefined) { + where.active = active; + } + + if (Object.keys(where).length > 0) { + usersQuery = usersQuery.where(where); + } + + if (params.search !== undefined && params.search.trim() !== "") { + const term = params.search.toLowerCase(); + usersQuery = usersQuery.filter((row) => row.name.toLowerCase().includes(term)); + } + + const limit = parseNumber(params.limit); + if (limit !== undefined) { + usersQuery = usersQuery.limit(limit); + } + + return usersQuery; +} + +function resolveInitialUsers(options: { readonly seed?: boolean; readonly seedSize?: number }): readonly User[] { + if (options.seed === false) { + return []; + } + + if (options.seedSize !== undefined) { + return generateUsers(options.seedSize); + } + + const envSeedSize = parseSeedSize(process.env.COLQL_EXAMPLE_SEED_SIZE); + return envSeedSize === seedUsers.length ? seedUsers : generateUsers(envSeedSize); +} + +export function buildApp(options: { readonly seed?: boolean; readonly seedSize?: number } = {}): FastifyInstance { + const app = Fastify({ logger: false }); + const { users, queryLog } = createUserStore(resolveInitialUsers(options)); + + app.get("/health", async () => ({ ok: true })); + + app.post<{ Body: UserInput }>("/users", async (request, reply) => { + users.insert(request.body); + return reply.code(201).send({ user: users.where("id", "=", request.body.id).first() }); + }); + + app.post<{ Body: { users: UserInput[] } }>("/users/bulk", async (request) => { + users.insertMany(request.body.users); + return { inserted: request.body.users.length, rowCount: users.rowCount }; + }); + + app.get<{ Querystring: UserQuery }>("/users", async (request) => { + const rows = applyUserFilters(users, request.query) + .select(["id", "name", "age", "country", "active"]) + .toArray(); + return { users: rows }; + }); + + app.get<{ Querystring: UserQuery }>("/users/count", async (request) => { + return { count: applyUserFilters(users, request.query).count() }; + }); + + app.patch<{ Params: { country: string }; Body: UserPatch }>("/users/by-country/:country", async (request) => { + if (!isCountry(request.params.country)) { + return { affectedRows: 0 }; + } + + return users.updateMany({ country: request.params.country }, request.body); + }); + + app.delete("/users/inactive", async () => users.deleteMany({ active: false })); + + app.get("/debug/query-log", async () => ({ entries: queryLog })); + + app.get("/debug/indexes", async () => ({ + equality: users.indexStats(), + sorted: users.sortedIndexStats(), + })); + + app.get("/debug/memory", async () => ({ + rowCount: users.rowCount, + capacity: users.capacity, + materializedRowCount: users.materializedRowCount, + scannedRowCount: users.scannedRowCount, + })); + + return app; +} diff --git a/examples/fastify-api/src/server.ts b/examples/fastify-api/src/server.ts new file mode 100644 index 0000000..01272c1 --- /dev/null +++ b/examples/fastify-api/src/server.ts @@ -0,0 +1,12 @@ +import { buildApp } from "./app"; + +const app = buildApp(); +const port = Number(process.env.PORT ?? 3000); + +try { + await app.listen({ port, host: "0.0.0.0" }); + console.log(`ColQL Fastify example listening on http://localhost:${port}`); +} catch (error) { + app.log.error(error); + process.exit(1); +} diff --git a/examples/fastify-api/test/app.test.ts b/examples/fastify-api/test/app.test.ts new file mode 100644 index 0000000..8fa8fbb --- /dev/null +++ b/examples/fastify-api/test/app.test.ts @@ -0,0 +1,191 @@ +import { afterEach, describe, expect, it } from "vitest"; +import type { FastifyInstance } from "fastify"; +import { buildApp, generateUsers } from "../src/app"; + +let app: FastifyInstance | undefined; + +function createApp() { + app = buildApp({ seed: true }); + return app; +} + +afterEach(async () => { + await app?.close(); + app = undefined; +}); + +describe("fastify ColQL example", () => { + it("generates deterministic larger seed data", () => { + const users = generateUsers(12); + + expect(users).toHaveLength(12); + expect(users[0]).toEqual({ id: 1, age: 18, country: "TR", active: false, name: "Ada", score: 50 }); + expect(users[4]).toEqual({ id: 5, age: 22, country: "FR", active: true, name: "Emre", score: 50.4 }); + }); + + it("can boot with a generated seed size", async () => { + const server = buildApp({ seedSize: 100 }); + app = server; + + const memory = await server.inject({ method: "GET", url: "/debug/memory" }); + expect(memory.json()).toEqual(expect.objectContaining({ rowCount: 100 })); + }); + + it("serves health checks", async () => { + const response = await createApp().inject({ method: "GET", url: "/health" }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ ok: true }); + }); + + it("inserts one user with insert(row)", async () => { + const server = createApp(); + const response = await server.inject({ + method: "POST", + url: "/users", + payload: { id: 10, age: 27, country: "US", active: true, name: "Nora", score: 93.2 }, + }); + + expect(response.statusCode).toBe(201); + expect(response.json()).toEqual({ + user: { id: 10, age: 27, country: "US", active: true, name: "Nora", score: 93.2 }, + }); + }); + + it("bulk inserts users with insertMany(rows)", async () => { + const server = createApp(); + const response = await server.inject({ + method: "POST", + url: "/users/bulk", + payload: { + users: [ + { id: 11, age: 24, country: "TR", active: true, name: "Aylin", score: 82 }, + { id: 12, age: 44, country: "GB", active: false, name: "Ken", score: 77 }, + ], + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ inserted: 2, rowCount: 7 }); + }); + + it("queries users with object where filters and projection", async () => { + const response = await createApp().inject({ + method: "GET", + url: "/users?country=TR&active=true", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + users: [{ id: 1, name: "Ada", age: 29, country: "TR", active: true }], + }); + }); + + it("queries users with range filters backed by sorted index planning", async () => { + const response = await createApp().inject({ + method: "GET", + url: "/users?minAge=30&maxAge=40", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + users: [ + { id: 4, name: "Mina", age: 35, country: "TR", active: false }, + { id: 5, name: "Emre", age: 31, country: "GB", active: true }, + ], + }); + }); + + it("uses callback filter search after structured filters", async () => { + const response = await createApp().inject({ + method: "GET", + url: "/users?country=TR&search=mi", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ + users: [{ id: 4, name: "Mina", age: 35, country: "TR", active: false }], + }); + }); + + it("counts filtered users", async () => { + const response = await createApp().inject({ + method: "GET", + url: "/users/count?active=true", + }); + + expect(response.statusCode).toBe(200); + expect(response.json()).toEqual({ count: 3 }); + }); + + it("updates matching users with updateMany(predicate, partialRow)", async () => { + const server = createApp(); + const update = await server.inject({ + method: "PATCH", + url: "/users/by-country/TR", + payload: { active: true }, + }); + + expect(update.statusCode).toBe(200); + expect(update.json()).toEqual({ affectedRows: 2 }); + + const users = await server.inject({ method: "GET", url: "/users?country=TR&active=true" }); + expect(users.json().users).toHaveLength(2); + }); + + it("deletes inactive users with deleteMany(predicate)", async () => { + const server = createApp(); + const deleted = await server.inject({ method: "DELETE", url: "/users/inactive" }); + + expect(deleted.statusCode).toBe(200); + expect(deleted.json()).toEqual({ affectedRows: 2 }); + + const count = await server.inject({ method: "GET", url: "/users/count" }); + expect(count.json()).toEqual({ count: 3 }); + }); + + it("records onQuery hook entries", async () => { + const server = createApp(); + await server.inject({ method: "GET", url: "/users?country=US" }); + await server.inject({ method: "GET", url: "/users/count?minAge=30" }); + + const response = await server.inject({ method: "GET", url: "/debug/query-log" }); + const body = response.json(); + + expect(response.statusCode).toBe(200); + expect(body.entries.length).toBeGreaterThanOrEqual(2); + expect(body.entries[0]).toEqual( + expect.objectContaining({ + duration: expect.any(Number), + rowsScanned: expect.any(Number), + indexUsed: expect.any(Boolean), + operation: 1, + }), + ); + }); + + it("exposes index stats and memory counters through debug endpoints", async () => { + const server = createApp(); + + const indexes = await server.inject({ method: "GET", url: "/debug/indexes" }); + expect(indexes.statusCode).toBe(200); + expect(indexes.json()).toEqual({ + equality: expect.arrayContaining([ + expect.objectContaining({ column: "country" }), + expect.objectContaining({ column: "name" }), + ]), + sorted: expect.arrayContaining([expect.objectContaining({ column: "age" })]), + }); + + const memory = await server.inject({ method: "GET", url: "/debug/memory" }); + expect(memory.statusCode).toBe(200); + expect(memory.json()).toEqual( + expect.objectContaining({ + rowCount: 5, + capacity: expect.any(Number), + materializedRowCount: expect.any(Number), + scannedRowCount: expect.any(Number), + }), + ); + }); +}); diff --git a/examples/fastify-api/tsconfig.json b/examples/fastify-api/tsconfig.json new file mode 100644 index 0000000..afc8ffd --- /dev/null +++ b/examples/fastify-api/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "Node", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "ignoreDeprecations": "6.0", + "types": ["node", "vitest"] + }, + "include": ["src", "test"] +} diff --git a/package-lock.json b/package-lock.json index 86d7dbc..8ecf7f3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@colql/colql", - "version": "0.1.0", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@colql/colql", - "version": "0.1.0", + "version": "0.2.0", "license": "MIT", "devDependencies": { "@eslint/js": "^10.0.1", diff --git a/package.json b/package.json index bb4792a..6e03a13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@colql/colql", - "version": "0.1.0", + "version": "0.2.0", "description": "Memory-efficient in-memory columnar query engine for TypeScript", "main": "dist/index.js", "module": "dist/index.mjs", diff --git a/src/index.ts b/src/index.ts index 21741bb..f1ad9da 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,4 +1,17 @@ export { column } from "./column"; export { table } from "./table"; export { ColQLError } from "./errors"; -export type { MutationResult, RowForSchema, Schema, Operator } from "./types"; +export type { + BooleanWherePredicate, + DictionaryWherePredicate, + MutationResult, + NumericWherePredicate, + ObjectWherePredicate, + Operator, + QueryHook, + QueryInfo, + RowPredicate, + RowForSchema, + Schema, + TableOptions, +} from "./types"; diff --git a/src/query.ts b/src/query.ts index ee6b296..077db62 100644 --- a/src/query.ts +++ b/src/query.ts @@ -1,6 +1,6 @@ import { BinaryHeap, type HeapItem } from "./heap"; import type { Table } from "./table"; -import type { ColumnValue, Filter, MutationResult, NumericColumnKey, Operator, RowForSchema, Schema, SelectedRow } from "./types"; +import type { ColumnValue, Filter, MutationResult, NumericColumnKey, ObjectWherePredicate, Operator, RowForSchema, RowPredicate, Schema, SelectedRow } from "./types"; import { ColQLError } from "./errors"; import { assertColumnExists, assertNonNegativeInteger, assertPositiveInteger } from "./validation"; @@ -18,6 +18,7 @@ type MutationSource = { export class Query implements Iterable { private readonly filters: readonly InternalFilter[]; private readonly plannedFilters: readonly InternalFilter[]; + private readonly rowPredicates: readonly RowPredicate[]; private readonly selectedColumns?: readonly (keyof TSchema)[]; private readonly limitValue?: number; private readonly offsetValue: number; @@ -26,6 +27,7 @@ export class Query implements Iterable private readonly source: Table, options: { filters?: readonly InternalFilter[]; + rowPredicates?: readonly RowPredicate[]; selectedColumns?: readonly (keyof TSchema)[]; limitValue?: number; offsetValue?: number; @@ -33,19 +35,36 @@ export class Query implements Iterable ) { this.filters = options.filters ?? []; this.plannedFilters = this.orderFilters(this.filters); + this.rowPredicates = options.rowPredicates ?? []; this.selectedColumns = options.selectedColumns; this.limitValue = options.limitValue; this.offsetValue = options.offsetValue ?? 0; } + where(predicate: ObjectWherePredicate): Query; where( columnName: Key, operator: TOperator, value: ValueForOperator, TOperator>, + ): Query; + where( + columnNameOrPredicate: Key | ObjectWherePredicate, + operator?: TOperator, + value?: ValueForOperator, TOperator>, ): Query { - const nextFilter = this.source.createFilter({ columnName, operator, value }); + if (arguments.length === 1) { + return this.whereObject(columnNameOrPredicate as ObjectWherePredicate); + } + + const columnName = columnNameOrPredicate as Key; + const nextFilter = this.source.createFilter({ + columnName, + operator: operator as TOperator, + value: value as ValueForOperator, TOperator>, + }); return new Query(this.source, { filters: [...this.filters, nextFilter], + rowPredicates: this.rowPredicates, selectedColumns: this.selectedColumns, limitValue: this.limitValue, offsetValue: this.offsetValue, @@ -66,12 +85,23 @@ export class Query implements Iterable return this.where(columnName, "not in", values); } + filter(predicate: RowPredicate): Query { + return new Query(this.source, { + filters: this.filters, + rowPredicates: [...this.rowPredicates, predicate], + selectedColumns: this.selectedColumns, + limitValue: this.limitValue, + offsetValue: this.offsetValue, + }); + } + select( columns: Keys, ): Query> { this.validateSelectedColumns(columns); return new Query(this.source, { filters: this.filters, + rowPredicates: this.rowPredicates, selectedColumns: columns, limitValue: this.limitValue, offsetValue: this.offsetValue, @@ -82,6 +112,7 @@ export class Query implements Iterable assertNonNegativeInteger(n, "limit"); return new Query(this.source, { filters: this.filters, + rowPredicates: this.rowPredicates, selectedColumns: this.selectedColumns, limitValue: n, offsetValue: this.offsetValue, @@ -92,6 +123,7 @@ export class Query implements Iterable assertNonNegativeInteger(n, "offset"); return new Query(this.source, { filters: this.filters, + rowPredicates: this.rowPredicates, selectedColumns: this.selectedColumns, limitValue: this.limitValue, offsetValue: n, @@ -99,18 +131,103 @@ export class Query implements Iterable } toArray(): TResult[] { + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.collectArray()); + } + + return this.collectArray(); + } + + private collectArray(): TResult[] { const rows: TResult[] = []; - this.forEach((row) => rows.push(row)); + this.forEachUninstrumented((row) => rows.push(row)); return rows; } first(): TResult | undefined { + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.firstUninstrumented()); + } + + return this.firstUninstrumented(); + } + + private firstUninstrumented(): TResult | undefined { const iterator = this[Symbol.iterator](); const next = iterator.next(); return next.done ? undefined : next.value; } count(): number { + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.countUninstrumented()); + } + + return this.countUninstrumented(); + } + + private countUninstrumented(): number { + if (this.rowPredicates.length === 0) { + return this.countStructuredOnly(); + } + + return this.countWithRowPredicates(); + } + + private countStructuredOnly(): number { + let seen = 0; + let produced = 0; + let scanned = 0; + + try { + const plan = this.source.getIndexedCandidatePlan(this.filters); + if (plan !== undefined) { + for (const rowIndex of plan.rowIndexes) { + if (this.limitValue !== undefined && produced >= this.limitValue) { + break; + } + + scanned += 1; + if (!this.matchesStructuredFilters(rowIndex)) { + continue; + } + + if (seen < this.offsetValue) { + seen += 1; + continue; + } + + produced += 1; + } + + return produced; + } + + for (let rowIndex = 0; rowIndex < this.source.rowCount; rowIndex += 1) { + if (this.limitValue !== undefined && produced >= this.limitValue) { + break; + } + + scanned += 1; + if (!this.matchesStructuredFilters(rowIndex)) { + continue; + } + + if (seen < this.offsetValue) { + seen += 1; + continue; + } + + produced += 1; + } + + return produced; + } finally { + this.source.recordRowScans(scanned); + } + } + + private countWithRowPredicates(): number { let seen = 0; let produced = 0; @@ -120,7 +237,7 @@ export class Query implements Iterable } this.source.recordRowScan(); - if (!this.matches(rowIndex)) { + if (!this.matchesWithRowPredicates(rowIndex)) { continue; } @@ -140,6 +257,14 @@ export class Query implements Iterable } isEmpty(): boolean { + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.isEmptyUninstrumented()); + } + + return this.isEmptyUninstrumented(); + } + + private isEmptyUninstrumented(): boolean { for (const _rowIndex of this.matchingRowIndexes()) { return false; } @@ -149,6 +274,14 @@ export class Query implements Iterable sum>(columnName: Key): number { this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.sumUninstrumented(columnName)); + } + + return this.sumUninstrumented(columnName); + } + + private sumUninstrumented>(columnName: Key): number { let total = 0; for (const rowIndex of this.matchingRowIndexes()) { @@ -160,6 +293,14 @@ export class Query implements Iterable avg>(columnName: Key): number | undefined { this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.avgUninstrumented(columnName)); + } + + return this.avgUninstrumented(columnName); + } + + private avgUninstrumented>(columnName: Key): number | undefined { let total = 0; let count = 0; @@ -173,6 +314,14 @@ export class Query implements Iterable min>(columnName: Key): number | undefined { this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.minUninstrumented(columnName)); + } + + return this.minUninstrumented(columnName); + } + + private minUninstrumented>(columnName: Key): number | undefined { let result: number | undefined; for (const rowIndex of this.matchingRowIndexes()) { @@ -185,6 +334,14 @@ export class Query implements Iterable max>(columnName: Key): number | undefined { this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.maxUninstrumented(columnName)); + } + + return this.maxUninstrumented(columnName); + } + + private maxUninstrumented>(columnName: Key): number | undefined { let result: number | undefined; for (const rowIndex of this.matchingRowIndexes()) { @@ -198,24 +355,49 @@ export class Query implements Iterable top>(n: number, columnName: Key): TResult[] { assertPositiveInteger(n, "top"); this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.topOrBottom(n, columnName, "top")); + } + return this.topOrBottom(n, columnName, "top"); } bottom>(n: number, columnName: Key): TResult[] { assertPositiveInteger(n, "bottom"); this.assertNumericColumn(columnName); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.topOrBottom(n, columnName, "bottom")); + } + return this.topOrBottom(n, columnName, "bottom"); } update(partialRow: Partial>): MutationResult { - return (this.source as unknown as MutationSource).updateRows(this.snapshotMatchingRowIndexes(), partialRow); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.updateUninstrumented(partialRow)); + } + + return this.updateUninstrumented(partialRow); } delete(): MutationResult { - return (this.source as unknown as MutationSource).deleteRows(this.snapshotMatchingRowIndexes()); + if (this.source.hasQueryHook()) { + return this.runTerminal(() => this.deleteUninstrumented()); + } + + return this.deleteUninstrumented(); } forEach(callback: (row: TResult, index: number) => void): void { + if (this.source.hasQueryHook()) { + this.runTerminal(() => this.forEachUninstrumented(callback)); + return; + } + + this.forEachUninstrumented(callback); + } + + private forEachUninstrumented(callback: (row: TResult, index: number) => void): void { let index = 0; for (const row of this) { callback(row, index); @@ -228,10 +410,23 @@ export class Query implements Iterable } __debugPlan(): ReturnType["getIndexDebugPlan"]> { + if (this.rowPredicates.length > 0) { + return this.source.getIndexDebugPlan([]); + } + return this.source.getIndexDebugPlan(this.filters); } *[Symbol.iterator](): Iterator { + if (this.rowPredicates.length === 0) { + yield* this.iterateStructuredOnly(); + return; + } + + yield* this.iterateWithRowPredicates(); + } + + private *iterateStructuredOnly(): IterableIterator { let seen = 0; let produced = 0; @@ -241,7 +436,31 @@ export class Query implements Iterable } this.source.recordRowScan(); - if (!this.matches(rowIndex)) { + if (!this.matchesStructuredFilters(rowIndex)) { + continue; + } + + if (seen < this.offsetValue) { + seen += 1; + continue; + } + + produced += 1; + yield this.source.materializeRow(rowIndex, this.selectedColumns) as TResult; + } + } + + private *iterateWithRowPredicates(): IterableIterator { + let seen = 0; + let produced = 0; + + for (const rowIndex of this.rowIndexesToScan()) { + if (this.limitValue !== undefined && produced >= this.limitValue) { + return; + } + + this.source.recordRowScan(); + if (!this.matchesWithRowPredicates(rowIndex)) { continue; } @@ -256,6 +475,39 @@ export class Query implements Iterable } private *matchingRowIndexes(): IterableIterator { + if (this.rowPredicates.length === 0) { + yield* this.matchingStructuredRowIndexes(); + return; + } + + yield* this.matchingRowIndexesWithPredicates(); + } + + private *matchingStructuredRowIndexes(): IterableIterator { + let seen = 0; + let produced = 0; + + for (const rowIndex of this.rowIndexesToScan()) { + if (this.limitValue !== undefined && produced >= this.limitValue) { + return; + } + + this.source.recordRowScan(); + if (!this.matchesStructuredFilters(rowIndex)) { + continue; + } + + if (seen < this.offsetValue) { + seen += 1; + continue; + } + + produced += 1; + yield rowIndex; + } + } + + private *matchingRowIndexesWithPredicates(): IterableIterator { let seen = 0; let produced = 0; @@ -265,7 +517,7 @@ export class Query implements Iterable } this.source.recordRowScan(); - if (!this.matches(rowIndex)) { + if (!this.matchesWithRowPredicates(rowIndex)) { continue; } @@ -283,7 +535,138 @@ export class Query implements Iterable return [...this.matchingRowIndexes()]; } + private updateUninstrumented(partialRow: Partial>): MutationResult { + return (this.source as unknown as MutationSource).updateRows(this.snapshotMatchingRowIndexes(), partialRow); + } + + private deleteUninstrumented(): MutationResult { + return (this.source as unknown as MutationSource).deleteRows(this.snapshotMatchingRowIndexes()); + } + + private runTerminal(operation: () => T): T { + const startScannedRows = this.source.scannedRowCount; + const start = Date.now(); + const indexUsed = this.usesIndexPlan(); + const result = operation(); + this.source.notifyQuery({ + duration: Date.now() - start, + rowsScanned: this.source.scannedRowCount - startScannedRows, + indexUsed, + }); + return result; + } + + private usesIndexPlan(): boolean { + return this.rowPredicates.length === 0 && this.source.getIndexDebugPlan(this.filters).mode === "index"; + } + + private whereObject(predicate: ObjectWherePredicate): Query { + this.assertObjectPredicate(predicate); + + let next: Query = this; + for (const [columnName, condition] of Object.entries(predicate)) { + if (condition === undefined) { + continue; + } + + assertColumnExists(this.source.schema, columnName, "where()"); + next = this.applyObjectCondition(next, columnName as keyof TSchema, condition); + } + + if (next === this) { + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + "Invalid where predicate: expected at least one column condition.", + ); + } + + return next; + } + + private assertObjectPredicate(predicate: unknown): asserts predicate is ObjectWherePredicate { + if (typeof predicate !== "object" || predicate === null || Array.isArray(predicate)) { + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + "Invalid where predicate: expected a non-null object.", + ); + } + + if (Object.keys(predicate).length === 0) { + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + "Invalid where predicate: expected at least one column condition.", + ); + } + } + + private applyObjectCondition( + query: Query, + columnName: keyof TSchema, + condition: unknown, + ): Query { + if (typeof condition !== "object" || condition === null || Array.isArray(condition)) { + return query.where(columnName, "=", condition as ColumnValue); + } + + const operatorEntries = Object.entries(condition); + if (operatorEntries.length === 0) { + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + `Invalid where predicate for column "${String(columnName)}": expected at least one operator.`, + ); + } + + let next = query; + for (const [operatorName, operatorValue] of operatorEntries) { + const operator = this.objectOperator(operatorName, columnName); + next = next.where( + columnName, + operator, + operatorValue as ValueForOperator, typeof operator>, + ); + } + + return next; + } + + private objectOperator(operatorName: string, columnName: keyof TSchema): Extract" | ">=" | "<" | "<=" | "in"> { + const isRangeOperator = operatorName === "gt" || operatorName === "gte" || operatorName === "lt" || operatorName === "lte"; + if (isRangeOperator && this.source.schema[columnName].kind !== "numeric") { + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + `Invalid where predicate operator "${operatorName}" for ${this.source.schema[columnName].kind} column "${String(columnName)}".`, + { columnName: String(columnName), operator: operatorName, kind: this.source.schema[columnName].kind }, + ); + } + + switch (operatorName) { + case "eq": + return "="; + case "gt": + return ">"; + case "gte": + return ">="; + case "lt": + return "<"; + case "lte": + return "<="; + case "in": + return "in"; + default: + throw new ColQLError( + "COLQL_INVALID_PREDICATE", + `Invalid where predicate operator "${operatorName}" for column "${String(columnName)}".`, + { columnName: String(columnName), operator: operatorName }, + ); + } + } + private *rowIndexesToScan(): IterableIterator { + if (this.rowPredicates.length > 0) { + yield* this.fullScanRowIndexes(); + return; + } + const plan = this.source.getIndexedCandidatePlan(this.filters); if (plan !== undefined) { for (const rowIndex of plan.rowIndexes) { @@ -292,6 +675,10 @@ export class Query implements Iterable return; } + yield* this.fullScanRowIndexes(); + } + + private *fullScanRowIndexes(): IterableIterator { for (let rowIndex = 0; rowIndex < this.source.rowCount; rowIndex += 1) { yield rowIndex; } @@ -337,7 +724,7 @@ export class Query implements Iterable .map((item) => this.source.materializeRow(item.rowIndex, this.selectedColumns) as TResult); } - private matches(rowIndex: number): boolean { + private matchesStructuredFilters(rowIndex: number): boolean { for (const filter of this.plannedFilters) { if (!this.source.matchesFilter(rowIndex, filter)) { return false; @@ -347,6 +734,20 @@ export class Query implements Iterable return true; } + private matchesWithRowPredicates(rowIndex: number): boolean { + if (!this.matchesStructuredFilters(rowIndex)) { + return false; + } + + for (const predicate of this.rowPredicates) { + if (!predicate(this.source.materializeRow(rowIndex))) { + return false; + } + } + + return true; + } + private orderFilters(filters: readonly InternalFilter[]): readonly InternalFilter[] { if (filters.length < 2) { return filters; diff --git a/src/table.ts b/src/table.ts index b8b476b..8c717bb 100644 --- a/src/table.ts +++ b/src/table.ts @@ -29,14 +29,19 @@ import type { Filter, MutationResult, NumericColumnKey, + ObjectWherePredicate, Operator, + QueryInfo, + QueryHook, + RowPredicate, RowForSchema, Schema, SelectedRow, + TableOptions, } from "./types"; const DEFAULT_CAPACITY = 1024; -const SERIALIZATION_VERSION = "@colql/colql@0.1.0"; +const SERIALIZATION_VERSION = "@colql/colql@0.2.0"; const SERIALIZATION_MAGIC = "COLQL003"; const MAGIC_BYTES = 8; const HEADER_LENGTH_BYTES = 4; @@ -75,7 +80,14 @@ type ValueForOperator = TOperator extends ? readonly TValue[] : TValue; -type PartialRowForSchema = Partial>; +type PartialRowForSchema = Partial< + RowForSchema +>; + +type TableConstructorOptions = TableOptions & { + readonly storages?: StorageMap; + readonly rowCount?: number; +}; export class Table { readonly schema: TSchema; @@ -85,16 +97,31 @@ export class Table { private materializedRows = 0; private scannedRows = 0; private readonly indexManager = new IndexManager(); + private readonly onQuery?: QueryHook; + constructor(schema: TSchema, options?: TableConstructorOptions); + constructor( + schema: TSchema, + initialCapacity?: number, + options?: TableConstructorOptions, + ); constructor( schema: TSchema, - initialCapacity = DEFAULT_CAPACITY, - options?: { - storages?: StorageMap; - rowCount?: number; - }, + initialCapacityOrOptions: + | number + | TableConstructorOptions = DEFAULT_CAPACITY, + options?: TableConstructorOptions, ) { assertValidSchema(schema); + const initialCapacity = + typeof initialCapacityOrOptions === "number" + ? initialCapacityOrOptions + : DEFAULT_CAPACITY; + const tableOptions = + typeof initialCapacityOrOptions === "number" + ? options + : initialCapacityOrOptions; + if (!Number.isInteger(initialCapacity) || initialCapacity < 1) { throw new ColQLError( "COLQL_INVALID_LIMIT", @@ -104,8 +131,10 @@ export class Table { this.schema = schema; this.currentCapacity = initialCapacity; - this.storages = options?.storages ?? this.createStorages(initialCapacity); - this.currentRowCount = options?.rowCount ?? 0; + this.storages = + tableOptions?.storages ?? this.createStorages(initialCapacity); + this.currentRowCount = tableOptions?.rowCount ?? 0; + this.onQuery = tableOptions?.onQuery; } get rowCount(): number { @@ -136,17 +165,23 @@ export class Table { this.scannedRows += 1; } + recordRowScans(count: number): void { + this.scannedRows += count; + } + + hasQueryHook(): boolean { + return this.onQuery !== undefined; + } + + notifyQuery(info: QueryInfo): void { + this.onQuery?.(info); + } + insert(row: RowForSchema): this { this.validateRow(row); this.ensureCapacity(this.currentRowCount + 1); - const rowIndex = this.currentRowCount; - for (const key of this.schemaKeys()) { - const value = row[key]; - this.storages[key].append(value); - } - - this.currentRowCount += 1; - this.addRowToIndexes(rowIndex); + this.appendRow(row); + this.addRowToIndexes(this.currentRowCount - 1); return this; } @@ -185,6 +220,17 @@ export class Table { return this.where(columnName, operator, value).delete(); } + updateMany( + predicate: ObjectWherePredicate, + partialRow: PartialRowForSchema, + ): MutationResult { + return this.where(predicate).update(partialRow); + } + + deleteMany(predicate: ObjectWherePredicate): MutationResult { + return this.where(predicate).delete(); + } + rebuildIndex(columnName: Key): this { assertColumnExists(this.schema, columnName, "rebuildIndex()"); this.indexManager.rebuild( @@ -196,7 +242,9 @@ export class Table { return this; } - rebuildSortedIndex>(columnName: Key): this { + rebuildSortedIndex>( + columnName: Key, + ): this { assertColumnExists(this.schema, columnName, "rebuildSortedIndex()"); this.indexManager.rebuildSorted( String(columnName), @@ -241,7 +289,9 @@ export class Table { } private deleteRows(rowIndexes: readonly number[]): MutationResult { - const indexes = this.uniqueRowIndexes(rowIndexes).sort((left, right) => right - left); + const indexes = this.uniqueRowIndexes(rowIndexes).sort( + (left, right) => right - left, + ); if (indexes.length === 0) { return { affectedRows: 0 }; } @@ -293,8 +343,23 @@ export class Table { } }); + if (rows.length === 0) { + return this; + } + + const firstRowIndex = this.currentRowCount; + this.ensureCapacity(this.currentRowCount + rows.length); for (const row of rows) { - this.insert(row); + this.appendRow(row); + } + + this.indexManager.markSortedDirty(); + for ( + let rowIndex = firstRowIndex; + rowIndex < this.currentRowCount; + rowIndex += 1 + ) { + this.addRowToEqualityIndexes(rowIndex); } return this; @@ -314,12 +379,36 @@ export class Table { return this.where(columnName, "not in", values); } + filter( + predicate: RowPredicate, + ): Query> { + return this.query().filter(predicate); + } + + where( + predicate: ObjectWherePredicate, + ): Query>; where( columnName: Key, operator: TOperator, value: ValueForOperator, TOperator>, + ): Query>; + where( + columnNameOrPredicate: Key | ObjectWherePredicate, + operator?: TOperator, + value?: ValueForOperator, TOperator>, ): Query> { - return this.query().where(columnName, operator, value); + if (arguments.length === 1) { + return this.query().where( + columnNameOrPredicate as ObjectWherePredicate, + ); + } + + return this.query().where( + columnNameOrPredicate as Key, + operator as TOperator, + value as ValueForOperator, TOperator>, + ); } select( @@ -859,7 +948,10 @@ export class Table { for (const key of keys) { const schemaKey = key as keyof TSchema; validateColumnValue(key, this.schema[schemaKey], record[key]); - values.push([schemaKey, record[key] as ColumnValue]); + values.push([ + schemaKey, + record[key] as ColumnValue, + ]); } return values; @@ -874,13 +966,24 @@ export class Table { } } + private appendRow(row: RowForSchema): void { + for (const key of this.schemaKeys()) { + this.storages[key].append(row[key]); + } + + this.currentRowCount += 1; + } + private uniqueRowIndexes(rowIndexes: readonly number[]): number[] { return [...new Set(rowIndexes)]; } private addRowToIndexes(rowIndex: number): void { this.indexManager.markSortedDirty(); + this.addRowToEqualityIndexes(rowIndex); + } + private addRowToEqualityIndexes(rowIndex: number): void { for (const key of this.schemaKeys()) { const columnName = String(key); if (!this.indexManager.has(columnName)) { @@ -1282,8 +1385,9 @@ export class Table { export function table( schema: TSchema, + options?: TableOptions, ): Table { - return new Table(schema); + return new Table(schema, options); } export namespace table { diff --git a/src/types.ts b/src/types.ts index 26e1b34..f4b38bc 100644 --- a/src/types.ts +++ b/src/types.ts @@ -60,6 +60,22 @@ export type MutationResult = { readonly affectedRows: number; }; +export type RowPredicate = ( + row: RowForSchema, +) => boolean; + +export type QueryInfo = { + readonly duration: number; + readonly rowsScanned: number; + readonly indexUsed: boolean; +}; + +export type QueryHook = (info: QueryInfo) => void; + +export type TableOptions = { + readonly onQuery?: QueryHook; +}; + export type SelectedRow< TSchema extends Schema, Keys extends readonly (keyof TSchema)[], @@ -76,3 +92,38 @@ export interface Filter>; } + +export type NumericWherePredicate = + | number + | { + readonly eq?: number; + readonly gt?: number; + readonly gte?: number; + readonly lt?: number; + readonly lte?: number; + readonly in?: readonly number[]; + }; + +export type BooleanWherePredicate = + | boolean + | { + readonly eq?: boolean; + readonly in?: readonly boolean[]; + }; + +export type DictionaryWherePredicate = + | TValue + | { + readonly eq?: TValue; + readonly in?: readonly TValue[]; + }; + +export type ObjectWherePredicate = { + readonly [Key in keyof TSchema]?: TSchema[Key] extends NumericColumnDefinition + ? NumericWherePredicate + : TSchema[Key] extends BooleanColumnDefinition + ? BooleanWherePredicate + : TSchema[Key] extends DictionaryColumnDefinition + ? DictionaryWherePredicate + : never; +}; diff --git a/tests/index-maintenance.test.ts b/tests/index-maintenance.test.ts index ac49e33..6d1b259 100644 --- a/tests/index-maintenance.test.ts +++ b/tests/index-maintenance.test.ts @@ -24,6 +24,38 @@ describe("index maintenance", () => { expect(users.where("id", "=", 2).first()).toEqual({ id: 2, status: "passive", age: 30 }); }); + it("keeps equality and sorted indexes correct after insertMany", () => { + const users = table({ id: column.uint32(), status: column.dictionary(["active", "passive"] as const), age: column.uint8() }); + users.insert({ id: 1, status: "active", age: 20 }); + users.createIndex("id").createIndex("status").createSortedIndex("age"); + + users.insertMany([ + { id: 2, status: "passive", age: 30 }, + { id: 3, status: "active", age: 40 }, + { id: 4, status: "passive", age: 50 }, + ]); + + expect(users.where("id", "in", [2, 4]).toArray()).toEqual([ + { id: 2, status: "passive", age: 30 }, + { id: 4, status: "passive", age: 50 }, + ]); + expect(users.where("status", "=", "active").toArray()).toEqual([ + { id: 1, status: "active", age: 20 }, + { id: 3, status: "active", age: 40 }, + ]); + expect(users.where("age", ">", 35).toArray()).toEqual([ + { id: 3, status: "active", age: 40 }, + { id: 4, status: "passive", age: 50 }, + ]); + expect(users.indexStats()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ column: "id", rowCount: 4 }), + expect.objectContaining({ column: "status", rowCount: 4 }), + ]), + ); + expect(users.sortedIndexStats()[0]).toEqual(expect.objectContaining({ column: "age", dirty: false, rowCount: 4 })); + }); + it("failed insert and insertMany do not corrupt indexes", () => { const users = table({ id: column.uint32(), age: column.uint8() }); users.insert({ id: 1, age: 20 }); @@ -38,6 +70,30 @@ describe("index maintenance", () => { expect(users.indexStats()[0].rowCount).toBe(1); }); + it("failed insertMany leaves equality and sorted indexes unchanged", () => { + const users = table({ id: column.uint32(), status: column.dictionary(["active", "passive"] as const), age: column.uint8() }); + users.insert({ id: 1, status: "active", age: 20 }); + users.createIndex("id").createIndex("status").createSortedIndex("age"); + const before = users.toArray(); + + expect(() => users.insertMany([ + { id: 2, status: "passive", age: 30 }, + { id: 3, status: "active", age: 300 }, + ])).toThrow(); + + expect(users.toArray()).toEqual(before); + expect(users.where("id", "=", 2).toArray()).toEqual([]); + expect(users.where("status", "=", "passive").toArray()).toEqual([]); + expect(users.where("age", ">=", 20).toArray()).toEqual(before); + expect(users.indexStats()).toEqual( + expect.arrayContaining([ + expect.objectContaining({ column: "id", rowCount: 1 }), + expect.objectContaining({ column: "status", rowCount: 1 }), + ]), + ); + expect(users.sortedIndexStats()[0]).toEqual(expect.objectContaining({ column: "age", dirty: false, rowCount: 1 })); + }); + it("does not serialize indexes and can rebuild after deserialization", () => { const users = table({ id: column.uint32(), status: column.dictionary(["active", "passive"] as const) }); users.insert({ id: 1, status: "active" }); diff --git a/tests/insert-validation.test.ts b/tests/insert-validation.test.ts index e1c04fb..cfcf78e 100644 --- a/tests/insert-validation.test.ts +++ b/tests/insert-validation.test.ts @@ -63,4 +63,21 @@ describe("insert validation", () => { expect(users.rowCount).toBe(0); }); + + it("insertMany leaves existing rows unchanged when any row is invalid", () => { + const users = usersTable(); + users.insert({ id: 1, age: 20, score: 1, status: "active", is_active: true }); + const before = users.toArray(); + + expectCode( + () => users.insertMany([ + { id: 2, age: 21, score: 2, status: "passive", is_active: false }, + { id: 3, age: 22, score: 3, status: "deleted" as "active", is_active: true }, + ]), + "COLQL_UNKNOWN_VALUE", + /Invalid row at index 1/, + ); + + expect(users.toArray()).toEqual(before); + }); }); diff --git a/tests/mutation.test.ts b/tests/mutation.test.ts index 653af79..e0f53d5 100644 --- a/tests/mutation.test.ts +++ b/tests/mutation.test.ts @@ -90,6 +90,17 @@ describe("mutations", () => { expect(users.toArray()).toEqual(before); }); + it("updateMany delegates to where(predicate).update()", () => { + const wrapperUsers = createUsers(); + const queryUsers = createUsers(); + + const wrapperResult = wrapperUsers.updateMany({ status: "active", age: { gte: 3 } }, { status: "archived", age: 77 }); + const queryResult = queryUsers.where({ status: "active", age: { gte: 3 } }).update({ status: "archived", age: 77 }); + + expect(wrapperResult).toEqual(queryResult); + expect(wrapperUsers.toArray()).toEqual(queryUsers.toArray()); + }); + it("returns zero for valid no-match predicate mutations", () => { const users = createUsers(); const before = users.toArray(); @@ -106,6 +117,17 @@ describe("mutations", () => { expect(users.toArray().map((row) => row.id)).toEqual([0, 2, 3, 5, 6, 8, 9, 11]); }); + it("deleteMany delegates to where(predicate).delete()", () => { + const wrapperUsers = createUsers(); + const queryUsers = createUsers(); + + const wrapperResult = wrapperUsers.deleteMany({ status: "passive", age: { lt: 10 } }); + const queryResult = queryUsers.where({ status: "passive", age: { lt: 10 } }).delete(); + + expect(wrapperResult).toEqual(queryResult); + expect(wrapperUsers.toArray()).toEqual(queryUsers.toArray()); + }); + it("query update and delete respect offset and limit but ignore select", () => { const users = createUsers(); diff --git a/tests/on-query.test.ts b/tests/on-query.test.ts new file mode 100644 index 0000000..1a19508 --- /dev/null +++ b/tests/on-query.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import { column, table } from "../src"; +import { Table } from "../src/table"; +import type { QueryInfo } from "../src"; + +const schema = { + id: column.uint32(), + age: column.uint8(), + status: column.dictionary(["active", "passive"] as const), +}; + +function seed(users: { insert: (row: { id: number; age: number; status: "active" | "passive" }) => unknown }) { + for (let id = 0; id < 10; id += 1) { + users.insert({ + id, + age: id, + status: id % 2 === 0 ? "active" : "passive", + }); + } +} + +describe("onQuery", () => { + it("reports terminal query info from table options", () => { + const events: QueryInfo[] = []; + const users = table(schema, { onQuery: (info) => events.push(info) }); + seed(users); + users.createIndex("id"); + + expect(events).toEqual([]); + + expect(users.where("id", "=", 4).count()).toBe(1); + expect(events).toHaveLength(1); + expect(events[0]).toEqual(expect.objectContaining({ rowsScanned: 1, indexUsed: true })); + expect(events[0].duration).toBeGreaterThanOrEqual(0); + + users.where("status", "=", "active").filter((row) => row.id < 4).toArray(); + expect(events).toHaveLength(2); + expect(events[1]).toEqual(expect.objectContaining({ rowsScanned: users.rowCount, indexUsed: false })); + }); + + it("does not instrument non-terminal query construction or streams", () => { + const events: QueryInfo[] = []; + const users = table(schema, { onQuery: (info) => events.push(info) }); + seed(users); + + const query = users.where({ status: "active" }).select(["id"]).limit(1); + query.stream(); + + expect(events).toEqual([]); + + expect(query.first()).toEqual({ id: 0 }); + expect(events).toHaveLength(1); + }); + + it("keeps constructor compatibility while allowing onQuery options", () => { + const events: QueryInfo[] = []; + const users = new Table(schema, 2, { onQuery: (info) => events.push(info) }); + seed(users); + + expect(users.capacity).toBeGreaterThanOrEqual(10); + expect(users.where({ age: { gt: 7 } }).count()).toBe(2); + expect(events).toHaveLength(1); + expect(events[0].rowsScanned).toBe(users.rowCount); + }); +}); diff --git a/tests/query-filter.test.ts b/tests/query-filter.test.ts new file mode 100644 index 0000000..7555eae --- /dev/null +++ b/tests/query-filter.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { column, table } from "../src"; + +function usersFixture(count = 100) { + const users = table({ + id: column.uint32(), + age: column.uint8(), + status: column.dictionary(["active", "passive"] as const), + }); + + for (let id = 0; id < count; id += 1) { + users.insert({ + id, + age: id, + status: id % 2 === 0 ? "active" : "passive", + }); + } + + return users; +} + +describe("query filter", () => { + it("runs callback filters after structured predicates", () => { + const users = usersFixture(10); + const seenIds: number[] = []; + + const rows = users + .where({ age: { gte: 5 } }) + .filter((row) => { + seenIds.push(row.id); + return row.id % 2 === 0; + }) + .toArray(); + + expect(seenIds).toEqual([5, 6, 7, 8, 9]); + expect(rows.map((row) => row.id)).toEqual([6, 8]); + }); + + it("forces a full scan and skips index planning when callback filters are present", () => { + const indexed = usersFixture(); + indexed.createIndex("id"); + indexed.resetScanCounter(); + + expect(indexed.where("id", "=", 42).count()).toBe(1); + expect(indexed.scannedRowCount).toBe(1); + + indexed.resetScanCounter(); + const query = indexed.where("id", "=", 42).filter((row) => row.status === "active"); + + expect(query.__debugPlan()).toEqual(expect.objectContaining({ mode: "scan" })); + expect(query.count()).toBe(1); + expect(indexed.scannedRowCount).toBe(indexed.rowCount); + }); + + it("supports table-level callback filters as a full-scan escape hatch", () => { + const users = usersFixture(); + users.createIndex("status"); + users.resetScanCounter(); + + const query = users.filter((row) => row.status === "active" && row.id < 4); + + expect(query.__debugPlan()).toEqual(expect.objectContaining({ mode: "scan" })); + expect(query.toArray().map((row) => row.id)).toEqual([0, 2]); + expect(users.scannedRowCount).toBe(users.rowCount); + }); +}); diff --git a/tests/query-validation.test.ts b/tests/query-validation.test.ts index 29cd4de..026433b 100644 --- a/tests/query-validation.test.ts +++ b/tests/query-validation.test.ts @@ -40,6 +40,17 @@ describe("query validation", () => { expectCode(() => users.whereNotIn("age", [1, 999]), "COLQL_OUT_OF_RANGE", /uint8 integer/); }); + it("validates object predicates", () => { + const users = usersTable(); + + expectCode(() => users.where({}), "COLQL_INVALID_PREDICATE", /at least one column condition/); + expectCode(() => users.where({ age: {} }), "COLQL_INVALID_PREDICATE", /at least one operator/); + expectCode(() => users.where({ age: { between: [18, 30] } } as never), "COLQL_INVALID_PREDICATE", /Invalid where predicate operator "between"/); + expectCode(() => users.where({ status: { gt: "active" } } as never), "COLQL_INVALID_PREDICATE", /dictionary column "status"/); + expectCode(() => users.where({ is_active: { lt: true } } as never), "COLQL_INVALID_PREDICATE", /boolean column "is_active"/); + expectCode(() => users.where({ status: { in: [] } }), "COLQL_TYPE_MISMATCH", /non-empty array/); + }); + it("validates select, limit, offset, and get", () => { const users = usersTable(); users.insert({ id: 1, age: 20, status: "active", is_active: true }); diff --git a/tests/query-where.test.ts b/tests/query-where.test.ts index 0d2bf0a..79f81ac 100644 --- a/tests/query-where.test.ts +++ b/tests/query-where.test.ts @@ -41,6 +41,38 @@ describe("query where", () => { ]); }); + it("supports object predicates as existing where conditions", () => { + const users = usersFixture(); + + expect(users.where({ age: { gt: 25 }, is_active: true }).toArray()).toEqual( + users.where("age", ">", 25).where("is_active", "=", true).toArray(), + ); + expect(users.where({ age: { gte: 18, lt: 40 }, status: { in: ["active", "passive"] } }).toArray()).toEqual( + users + .where("age", ">=", 18) + .where("age", "<", 40) + .where("status", "in", ["active", "passive"]) + .toArray(), + ); + expect(users.where({ status: { eq: "blocked" } }).first()).toEqual(users.where("status", "=", "blocked").first()); + }); + + it("supports object predicates on query chains", () => { + const users = usersFixture(); + + expect(users.where("age", ">=", 18).where({ status: "active", is_active: true }).toArray()).toEqual([ + { id: 3, age: 30, status: "active", is_active: true }, + ]); + }); + + it("preserves index planning by translating object predicates to existing filters", () => { + const users = usersFixture(); + users.createIndex("status").createSortedIndex("age"); + + expect(users.where({ status: "active" }).__debugPlan()).toEqual(users.where("status", "=", "active").__debugPlan()); + expect(users.where({ age: { gt: 25 } }).__debugPlan()).toEqual(users.where("age", ">", 25).__debugPlan()); + }); + it("supports in and not in operators", () => { const users = usersFixture(); expect(users.where("age", "in", [17, 30]).count()).toBe(2); diff --git a/tests/type-inference.test-d.ts b/tests/type-inference.test-d.ts index 230ca55..8ee6daf 100644 --- a/tests/type-inference.test-d.ts +++ b/tests/type-inference.test-d.ts @@ -1,5 +1,5 @@ import { column, table } from "../src"; -import type { MutationResult } from "../src"; +import type { MutationResult, QueryInfo } from "../src"; const users = table({ id: column.uint32(), @@ -33,6 +33,22 @@ users.select(["id", "age"]).top(1, "age"); users.whereIn("status", ["active"]); users.whereNotIn("age", [18, 21]); users.where("age", ">", 18).whereIn("status", ["passive"]).size(); +users.where({ age: { gt: 18, gte: 18, lt: 30, lte: 30, eq: 25, in: [18, 21] } }); +users.where({ age: 25, status: "active", is_active: true }); +users.where({ status: { eq: "passive", in: ["active"] }, is_active: { eq: false, in: [true, false] } }); +users.where("age", ">", 18).where({ status: "active" }).select(["id"]); +users.filter((row) => row.age > 18).where({ status: "active" }).toArray(); +users.where({ status: "active" }).filter((row) => row.is_active).select(["id"]); +table(users.getSchema(), { + onQuery(info: QueryInfo) { + const duration: number = info.duration; + const rowsScanned: number = info.rowsScanned; + const indexUsed: boolean = info.indexUsed; + void duration; + void rowsScanned; + void indexUsed; + }, +}); users.createIndex("id"); users.createIndex("status"); users.hasIndex("id"); @@ -51,6 +67,8 @@ const updateWhereResult: MutationResult = users.updateWhere("age", ">", 18, { st const queryUpdateResult: MutationResult = users.where("status", "=", "active").select(["id"]).limit(1).update({ age: 25 }); const deleteWhereResult: MutationResult = users.deleteWhere("status", "=", "passive"); const queryDeleteResult: MutationResult = users.where("age", ">", 18).offset(1).limit(1).delete(); +const updateManyResult: MutationResult = users.updateMany({ age: { gt: 18 }, status: "active" }, { is_active: false }); +const deleteManyResult: MutationResult = users.deleteMany({ status: { in: ["passive"] } }); users.rebuildIndex("id"); users.rebuildSortedIndex("age"); users.rebuildIndexes(); @@ -61,6 +79,8 @@ void updateWhereResult; void queryUpdateResult; void deleteWhereResult; void queryDeleteResult; +void updateManyResult; +void deleteManyResult; const row: { id: number; age: number; status: "active" | "passive"; is_active: boolean } = users.get(0); const serialized: ArrayBuffer = users.serialize(); const restored = table.deserialize(serialized); @@ -79,6 +99,33 @@ users.select(["missing"]); // @ts-expect-error wrong value type users.where("age", "=", "active"); +// @ts-expect-error object where rejects unknown columns +users.where({ missing: 1 }); + +// @ts-expect-error object where rejects wrong numeric value type +users.where({ age: "active" }); + +// @ts-expect-error object where rejects wrong numeric in value type +users.where({ age: { in: ["active"] } }); + +// @ts-expect-error object where rejects wrong dictionary value +users.where({ status: "deleted" }); + +// @ts-expect-error object where rejects wrong dictionary in value +users.where({ status: { in: ["deleted"] } }); + +// @ts-expect-error object where rejects range operators on dictionary columns +users.where({ status: { gt: "active" } }); + +// @ts-expect-error object where rejects range operators on boolean columns +users.where({ is_active: { lt: true } }); + +// @ts-expect-error filter callback receives full typed rows +users.filter((row) => row.missing === 1); + +// @ts-expect-error filter callback must return boolean +users.filter((row) => row.age); + // @ts-expect-error insert rejects missing fields users.insert({ id: 1, age: 25, status: "active" }); @@ -121,6 +168,15 @@ users.updateWhere("status", "=", "deleted", { age: 1 }); // @ts-expect-error updateWhere rejects wrong predicate value type users.updateWhere("age", "=", "active", { status: "active" }); +// @ts-expect-error updateMany rejects wrong predicate dictionary value +users.updateMany({ status: "deleted" }, { age: 1 }); + +// @ts-expect-error updateMany rejects unknown partial columns +users.updateMany({ age: { gt: 18 } }, { missing: 1 }); + +// @ts-expect-error deleteMany rejects range operators on dictionary columns +users.deleteMany({ status: { gt: "active" } }); + // @ts-expect-error query update rejects wrong dictionary value users.where("age", ">", 18).update({ status: "deleted" }); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..4caaca8 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + exclude: ["node_modules", "dist", "examples/**"], + }, +});