diff --git a/README.md b/README.md index a4ab232..ab82907 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,8 @@ The following TypeScript types are exported: | Int | `int` | `42` | | Float | `float` | `3.14` | | Number | `number` | `42` or `3.14` | + +> **Note:** `int`, `float`, and `number` all collapse to the `number` type in generated TypeScript declarations (`schemaToTypeString`). The distinction is preserved at parse time — `int` rejects non-integer values while `float`/`number` accept them. | Boolean | `boolean` | `true` or `false` | | Tuple | `[Type1; Type2; ...]` | `[hello; 42; true]` | | Array | `Type[]` | `[1; 2; 3]` | @@ -159,6 +161,16 @@ The following TypeScript types are exported: - In CSV schema rows, double-quoted string literals like `"active" | "inactive"` are handled automatically by the loader; avoid commas inside string literals (use single-quoted literals like `'a,b'` for those) - For CSV loading with reference resolution, see [csv-loader.md](./csv-loader.md) +## Migration from 1.x + +Version 2.0.0 introduces breaking changes to the schema DSL: + +1. **Composite values must be fully bracketed.** Tuple and array values now always require `[]` — `[a; 1]; [b; 2]` is no longer accepted; write `[[a; 1]; [b; 2]]`. +2. **The `[Type][]` array form is removed.** Use `Type[]` (e.g. `[string; number][]` instead of `[[string; number]][]`). +3. **`[single]` is now a 1-tuple, not an array.** Use `Type[]` for single-element arrays. +4. **Union resolution prefers reference members.** In a union containing references, reference members are tried before non-reference members regardless of author order (see the notes above). +5. **Schema cells may be fully quoted.** Double-quoted string literals like `"active" | "inactive"` in a CSV schema row are handled automatically by the loader. + ```javascript // rspack.config.js module.exports = { diff --git a/csv-loader.md b/csv-loader.md index 253acfc..4968c0d 100644 --- a/csv-loader.md +++ b/csv-loader.md @@ -149,6 +149,16 @@ Uses [typed-csv](https://github.com/your-repo/typed-csv) syntax: | Array | `string[]` | `[a; b; c]` | | Tuple | `[string; number]` | `[hello; 42]` | +> **Note:** `int` and `float` are accepted in schemas and validated at parse time, but both collapse to `number` in generated type declarations. + +## Migration from 1.x + +- Composite values must be fully bracketed: `[[a; 1]; [b; 2]]`, not `[a; 1]; [b; 2]`. +- The `[Type][]` array form is removed — use `Type[]`. +- `[single]` is a 1-tuple, not an array. +- In unions containing references, reference members are tried first. +- Double-quoted string literals (`"active" | "inactive"`) in a schema row are handled automatically. + ## License ISC diff --git a/docs/syntax-rework-plan.md b/docs/syntax-rework-plan.md index 5bedc0e..fd68842 100644 --- a/docs/syntax-rework-plan.md +++ b/docs/syntax-rework-plan.md @@ -87,7 +87,7 @@ All changes are **breaking** to the DSL → bump to `2.0.0`, update README + `cs 1. Composite values must be fully bracketed. 2. `[Type][]` array form removed — use `Type[]`. 3. `[single]` is now a 1-tuple, not an array. - 4. Union resolution now prefers non-reference members. + 4. Union resolution now prefers reference members (tried before non-reference members). 5. (If Phase 4b) schema cells may be fully quoted. - Update `AGENTS.md` gotchas: union ordering rule, quote handling. diff --git a/src/csv-loader/reference-resolver.ts b/src/csv-loader/reference-resolver.ts index 0e0ebb3..e09a077 100644 --- a/src/csv-loader/reference-resolver.ts +++ b/src/csv-loader/reference-resolver.ts @@ -12,6 +12,16 @@ import { parseCsv } from "./loader.js"; /** Cache for loaded referenced tables */ const referenceTableCache = new Map[]>(); +/** + * Cache for reverse-reference lookups: filePath -> (foreignKey -> (fkValue -> rows[])). + * Built once per referenced table + foreign key, mirroring what module-gen emits, + * so resolveReverseReference doesn't re-filter the whole table per row. + */ +const reverseLookupCache = new Map< + string, + Map[]>> +>(); + /** Set of file paths currently being loaded (to detect circular references) */ const loadingFiles = new Set(); @@ -39,6 +49,7 @@ export function loadReferenceTable( ): { lookup: Map>; refTable: Record[]; + refFilePath: string; } { const baseDir = refBaseDir || @@ -83,7 +94,51 @@ export function loadReferenceTable( } }); - return { lookup, refTable }; + return { lookup, refTable, refFilePath }; +} + +/** + * Build (and cache) a reverse lookup for a referenced table + foreign key: + * a map from the foreign-key value to the rows that reference it. + */ +function getReverseLookup( + schema: ReverseReferenceSchema, + refBaseDir: string | undefined, + defaultPrimaryKey: string, + currentFilePath: string | undefined, +): Map[]> { + const { refTable, refFilePath } = loadReferenceTable( + schema, + refBaseDir, + defaultPrimaryKey, + currentFilePath, + ); + + let byForeignKey = reverseLookupCache.get(refFilePath); + if (!byForeignKey) { + byForeignKey = new Map(); + reverseLookupCache.set(refFilePath, byForeignKey); + } + + const cached = byForeignKey.get(schema.foreignKey); + if (cached) return cached; + + const lookup = new Map[]>(); + for (const row of refTable) { + const fkValue = row[schema.foreignKey]; + const fkStr = + fkValue !== null && fkValue !== undefined && typeof fkValue === "object" + ? String((fkValue as Record)[defaultPrimaryKey]) + : String(fkValue); + const bucket = lookup.get(fkStr); + if (bucket) { + bucket.push(row); + } else { + lookup.set(fkStr, [row]); + } + } + byForeignKey.set(schema.foreignKey, lookup); + return lookup; } export function resolveReferenceId( @@ -333,21 +388,13 @@ export function resolveReverseReference( defaultPrimaryKey: string, currentFilePath: string | undefined, ): Record[] { - const { refTable } = loadReferenceTable( + const lookup = getReverseLookup( schema, refBaseDir, defaultPrimaryKey, currentFilePath, ); - const pkStr = String(pkValue); - return refTable.filter((row) => { - const fkValue = row[schema.foreignKey]; - const fkStr = - fkValue !== null && fkValue !== undefined && typeof fkValue === "object" - ? String((fkValue as Record)[defaultPrimaryKey]) - : String(fkValue); - return fkStr === pkStr; - }); + return lookup.get(String(pkValue)) ?? []; } export function resolveNestedReferences( diff --git a/src/csv-loader/tests/parseCsv-reverseRefs.test.ts b/src/csv-loader/tests/parseCsv-reverseRefs.test.ts index 889e5d9..6575270 100644 --- a/src/csv-loader/tests/parseCsv-reverseRefs.test.ts +++ b/src/csv-loader/tests/parseCsv-reverseRefs.test.ts @@ -43,6 +43,40 @@ describe("parseCsv - reverse reference resolution", () => { } }); + it("should resolve reverse reference when the foreign key is an object reference", () => { + // The referenced table's foreign key column is itself a reference, so the + // stored value is an object and the reverse lookup must key on its primary key. + const ordersCsvPath = path.join(fixturesDir, "rev_orders.csv"); + const ordersContent = [ + "id,customer,total", + "string,@users,number", + "1,1,100", + "2,1,50", + ].join("\n"); + fs.writeFileSync(ordersCsvPath, ordersContent); + + try { + const csv = [ + "id,name", + "string,string", + "# inject orders = ~rev_orders(customer)", + "1,Alice", + ].join("\n"); + + const result = parseCsv(csv, { + emitTypes: false, + currentFilePath: path.join(fixturesDir, "test.csv"), + }); + + const orders = result.data[0].orders as Record[]; + expect(orders).toHaveLength(2); + expect(orders[0].id).toBe("1"); + expect(orders[1].id).toBe("2"); + } finally { + fs.unlinkSync(ordersCsvPath); + } + }); + it("should return empty array for reverse reference with no matches", () => { const ordersCsvPath = path.join(fixturesDir, "rev_orders.csv"); const ordersContent = [