Skip to content

Commit

Permalink
v9.1.0 🚀 - S.unnest
Browse files Browse the repository at this point in the history
  • Loading branch information
DZakh committed Jan 21, 2025
1 parent 21a3f32 commit 109f740
Show file tree
Hide file tree
Showing 7 changed files with 179 additions and 1 deletion.
60 changes: 60 additions & 0 deletions docs/js-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -414,6 +414,66 @@ S.arrayMinLength(S.array(S.string) 5); // Array must be 5 or more items long
S.arrayLength(S.array(S.string) 5); // Array must be exactly 5 items long
```

### Unnest

```ts
const schema = S.unnest(
S.schema({
id: S.string,
name: S.nullable(S.string),
deleted: S.boolean,
})
);

const value = S.reverseConvertOrThrow(
[
{ id: "0", name: "Hello", deleted: false },
{ id: "1", name: undefined, deleted: true },
],
schema
);
// [["0", "1"], ["Hello", null], [false, true]]
```

The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field.

The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.

<details>

<summary>
Checkout the compiled code yourself:
</summary>

```javascript
(i) => {
let v1 = [new Array(i.length), new Array(i.length), new Array(i.length)];
for (let v0 = 0; v0 < i.length; ++v0) {
let v3 = i[v0];
try {
let v4 = v3["name"],
v5;
if (v4 !== void 0) {
v5 = v4;
} else {
v5 = null;
}
v1[0][v0] = v3["id"];
v1[1][v0] = v5;
v1[2][v0] = v3["deleted"];
} catch (v2) {
if (v2 && v2.s === s) {
v2.path = "" + "[\"'+v0+'\"]" + v2.path;
}
throw v2;
}
}
return v1;
};
```

</details>

## Tuples

Unlike arrays, tuples have a fixed number of elements and each element can have a different type.
Expand Down
55 changes: 55 additions & 0 deletions docs/rescript-usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- [Enums](#enums)
- [`array`](#array)
- [`list`](#list)
- [`unnest`](#unnest)
- [`tuple`](#tuple)
- [`tuple1` - `tuple3`](#tuple1---tuple3)
- [`dict`](#dict)
Expand Down Expand Up @@ -892,6 +893,60 @@ let schema = S.list(S.string)

The `S.list` schema represents an array of data of a specific type which is transformed to ReScript's list data-structure.

### **`unnest`**

`S.t<'value> => S.t<array<'value>>`

```rescript
let schema = S.unnest(S.schema(s => {
id: s.matches(S.string),
name: s.matches(S.null(S.string)),
deleted: s.matches(S.bool),
}))
[{id: "0", name: Some("Hello"), deleted: false}, {id: "1", name: None, deleted: true}]->S.reverseConvertOrThrow(schema)
// [["0", "1"], ["Hello", null], [false, true]]
```

The helper function is inspired by the article [Boosting Postgres INSERT Performance by 2x With UNNEST](https://www.timescale.com/blog/boosting-postgres-insert-performance). It allows you to flatten a nested array of objects into arrays of values by field.

The main concern of the approach described in the article is usability. And ReScript Schema completely solves the problem, providing a simple and intuitive API that is even more performant than `S.array`.

<details>

<summary>
Checkout the compiled code yourself:
</summary>

```javascript
(i) => {
let v1 = [new Array(i.length), new Array(i.length), new Array(i.length)];
for (let v0 = 0; v0 < i.length; ++v0) {
let v3 = i[v0];
try {
let v4 = v3["name"],
v5;
if (v4 !== void 0) {
v5 = v4;
} else {
v5 = null;
}
v1[0][v0] = v3["id"];
v1[1][v0] = v5;
v1[2][v0] = v3["deleted"];
} catch (v2) {
if (v2 && v2.s === s) {
v2.path = "" + "[\"'+v0+'\"]" + v2.path;
}
throw v2;
}
}
return v1;
};
```

</details>

### **`tuple`**

`(S.Tuple.s => 'value) => S.t<'value>`
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "rescript-schema",
"version": "9.0.1",
"version": "9.1.0",
"description": "🧬 The fastest parser in the entire JavaScript ecosystem with a focus on small bundle size and top-notch DX",
"keywords": [
"ReScript",
Expand Down
38 changes: 38 additions & 0 deletions packages/tests/src/core/S_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1708,6 +1708,44 @@ test("Tuple types", (t) => {
t.pass();
});

test("Unnest schema", (t) => {
const schema = S.unnest(
S.schema({
id: S.string,
name: S.nullable(S.string),
deleted: S.boolean,
})
);

const value = S.reverseConvertOrThrow(
[
{ id: "0", name: "Hello", deleted: false },
{ id: "1", name: undefined, deleted: true },
],
schema
);

let expected: typeof value = [
["0", "1"],
["Hello", null],
[false, true],
];

t.deepEqual(value, expected);

expectType<
SchemaEqual<
typeof schema,
{
id: string;
name: string | undefined;
deleted: boolean;
}[],
(string[] | boolean[] | (string | null)[])[]
>
>(true);
});

test("Tuple with transform to object", (t) => {
let pointSchema = S.tuple((s) => {
s.tag(0, "point");
Expand Down
15 changes: 15 additions & 0 deletions packages/tests/src/core/S_unnest_test.res
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ test("Successfully parses and reverse converts a simple object with unnest", t =
%raw(`[["a", "b"], [0, 1]]`),
(),
)

let example = S.unnest(
S.schema(s =>
{
"id": s.matches(S.string),
"name": s.matches(S.null(S.string)),
"deleted": s.matches(S.bool),
}
),
)
t->U.assertCompiledCode(
~schema=example,
~op=#ReverseConvert,
`i=>{let v1=[new Array(i.length),new Array(i.length),new Array(i.length),];for(let v0=0;v0<i.length;++v0){let v3=i[v0];try{let v4=v3["name"],v5;if(v4!==void 0){v5=v4}else{v5=null}v1[0][v0]=v3["id"];v1[1][v0]=v5;v1[2][v0]=v3["deleted"];}catch(v2){if(v2&&v2.s===s){v2.path=""+\'["\'+v0+\'"]\'+v2.path}throw v2}}return v1}`,
)
})

test("Transforms nullable fields", t => {
Expand Down
9 changes: 9 additions & 0 deletions src/S.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,15 @@ export const array: <Output, Input>(
schema: Schema<Output, Input>
) => Schema<Output[], Input[]>;

export const unnest: <Output, Input extends Record<string, unknown>>(
schema: Schema<Output, Input>
) => Schema<
Output[],
{
[K in keyof Input]: Input[K][];
}[keyof Input][]
>;

export const record: <Output, Input>(
schema: Schema<Output, Input>
) => Schema<Record<string, Output>, Record<string, Input>>;
Expand Down
1 change: 1 addition & 0 deletions src/S.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const optional = S.js_optional;
export const nullable = S.$$null;
export const nullish = S.nullable;
export const array = S.array;
export const unnest = S.unnest;
export const record = S.dict;
export const jsonString = S.jsonString;
export const union = S.js_union;
Expand Down

1 comment on commit 109f740

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Benchmark suite Current: 109f740 Previous: b80c9c0 Ratio
S.schema - make 1371367 ops/sec (±1.24%) 1381695 ops/sec (±0.64%) 1.01
S.schema - make + parse 108961 ops/sec (±0.54%) 106535 ops/sec (±1.27%) 0.98
S.schema - parse 51323974 ops/sec (±1.56%) 50661345 ops/sec (±1.65%) 0.99
S.schema - parse strict 24429605 ops/sec (±1.21%) 24030078 ops/sec (±1.57%) 0.98
S.schema - make + reverse 938685 ops/sec (±1.21%) 941104 ops/sec (±0.37%) 1.00
S.schema - make + reverse convert 186620 ops/sec (±0.89%) 187149 ops/sec (±0.49%) 1.00
S.schema - reverse convert 67993967 ops/sec (±1.96%) 91393061 ops/sec (±2.40%) 1.34
S.schema - reverse convert (compiled) 141557453 ops/sec (±4.35%) 139157834 ops/sec (±3.60%) 0.98
S.schema - assert 67811645 ops/sec (±2.17%) 54052579 ops/sec (±1.42%) 0.80
S.schema - assert (compiled) 73516193 ops/sec (±4.59%) 79145212 ops/sec (±1.65%) 1.08
S.schema - assert strict 24895839 ops/sec (±1.15%) 22623377 ops/sec (±1.92%) 0.91
S.object - make 1008038 ops/sec (±0.24%) 959359 ops/sec (±0.18%) 0.95
S.object - make + parse 87097 ops/sec (±1.50%) 89245 ops/sec (±0.43%) 1.02
S.object - parse 37581228 ops/sec (±1.47%) 37681265 ops/sec (±1.66%) 1.00
S.object - make + reverse 129566 ops/sec (±1.43%) 131924 ops/sec (±1.37%) 1.02
S.object - make + reverse convert 82336 ops/sec (±0.95%) 85551 ops/sec (±1.16%) 1.04
S.object - reverse convert 46367794 ops/sec (±1.75%) 47021369 ops/sec (±1.92%) 1.01
S.string - parse 68482422 ops/sec (±2.09%) 67636391 ops/sec (±2.08%) 0.99
S.string - reverse convert 71508363 ops/sec (±2.13%) 66255926 ops/sec (±3.14%) 0.93

This comment was automatically generated by workflow using github-action-benchmark.

Please sign in to comment.