turbine-orm 0.13.0 → 0.13.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -8,6 +8,7 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.postgresDialect = void 0;
11
+ const errors_js_1 = require("./errors.js");
11
12
  /** PostgreSQL implementation of the dialect contract. */
12
13
  exports.postgresDialect = {
13
14
  name: 'postgresql',
@@ -41,7 +42,7 @@ exports.postgresDialect = {
41
42
  },
42
43
  buildBulkInsertStatement(input) {
43
44
  if (!input.columnArrayTypes || input.columnArrayTypes.length !== input.columns.length) {
44
- throw new Error('PostgreSQL bulk insert requires one array type per column');
45
+ throw new errors_js_1.ValidationError('PostgreSQL bulk insert requires one array type per column');
45
46
  }
46
47
  const columnArrays = input.columns.map((_, columnIndex) => input.rowValues.map((row) => row[columnIndex]));
47
48
  const unnestArgs = input.columns.map((_, i) => `${this.paramPlaceholder(i + 1)}::${input.columnArrayTypes[i]}`);
@@ -1538,8 +1538,7 @@ class QueryInterface {
1538
1538
  }
1539
1539
  // Array filter
1540
1540
  if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
1541
- const aKeys = Object.keys(value).sort();
1542
- parts.push(`${key}:arr(${aKeys.join(',')})`);
1541
+ parts.push(`${key}:arr(${this.fingerprintArrayFilter(value)})`);
1543
1542
  continue;
1544
1543
  }
1545
1544
  // Plain equality
@@ -1547,6 +1546,15 @@ class QueryInterface {
1547
1546
  }
1548
1547
  return parts.join('&');
1549
1548
  }
1549
+ /**
1550
+ * Produce a value-invariant fingerprint for array filters while preserving
1551
+ * parameterless boolean operators that change SQL shape.
1552
+ */
1553
+ fingerprintArrayFilter(filter) {
1554
+ const keys = Object.keys(filter).sort();
1555
+ const suffix = filter.isEmpty === undefined ? '' : `:empty=${filter.isEmpty ? 'true' : 'false'}`;
1556
+ return `${keys.join(',')}${suffix}`;
1557
+ }
1550
1558
  /**
1551
1559
  * Fingerprint a relation filter sub-where for some/every/none.
1552
1560
  */
@@ -2653,12 +2661,12 @@ class QueryInterface {
2653
2661
  clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
2654
2662
  }
2655
2663
  if (filter.isEmpty === true) {
2656
- // array_length(column, 1) IS NULL
2657
- clauses.push(`array_length(${column}, 1) IS NULL`);
2664
+ // Treat NULL and empty arrays as empty for Prisma-compatible ergonomics.
2665
+ clauses.push(`COALESCE(cardinality(${column}), 0) = 0`);
2658
2666
  }
2659
2667
  else if (filter.isEmpty === false) {
2660
- // array_length(column, 1) IS NOT NULL
2661
- clauses.push(`array_length(${column}, 1) IS NOT NULL`);
2668
+ // Require at least one element; excludes both NULL and ARRAY[] values.
2669
+ clauses.push(`cardinality(${column}) > 0`);
2662
2670
  }
2663
2671
  return clauses;
2664
2672
  }
package/dist/dialect.js CHANGED
@@ -5,6 +5,7 @@
5
5
  * PostgreSQL-native by default, but query generation now depends on this
6
6
  * contract for the SQL primitives that vary across MySQL and SQLite.
7
7
  */
8
+ import { ValidationError } from './errors.js';
8
9
  /** PostgreSQL implementation of the dialect contract. */
9
10
  export const postgresDialect = {
10
11
  name: 'postgresql',
@@ -38,7 +39,7 @@ export const postgresDialect = {
38
39
  },
39
40
  buildBulkInsertStatement(input) {
40
41
  if (!input.columnArrayTypes || input.columnArrayTypes.length !== input.columns.length) {
41
- throw new Error('PostgreSQL bulk insert requires one array type per column');
42
+ throw new ValidationError('PostgreSQL bulk insert requires one array type per column');
42
43
  }
43
44
  const columnArrays = input.columns.map((_, columnIndex) => input.rowValues.map((row) => row[columnIndex]));
44
45
  const unnestArgs = input.columns.map((_, i) => `${this.paramPlaceholder(i + 1)}::${input.columnArrayTypes[i]}`);
@@ -258,6 +258,11 @@ export declare class QueryInterface<T extends object, R extends object = {}> {
258
258
  * @internal Exposed as package-private for testing via class access.
259
259
  */
260
260
  fingerprintWhere(where: Record<string, unknown>): string;
261
+ /**
262
+ * Produce a value-invariant fingerprint for array filters while preserving
263
+ * parameterless boolean operators that change SQL shape.
264
+ */
265
+ private fingerprintArrayFilter;
261
266
  /**
262
267
  * Fingerprint a relation filter sub-where for some/every/none.
263
268
  */
@@ -1535,8 +1535,7 @@ export class QueryInterface {
1535
1535
  }
1536
1536
  // Array filter
1537
1537
  if (typeof value === 'object' && !Array.isArray(value) && isArrayFilter(value)) {
1538
- const aKeys = Object.keys(value).sort();
1539
- parts.push(`${key}:arr(${aKeys.join(',')})`);
1538
+ parts.push(`${key}:arr(${this.fingerprintArrayFilter(value)})`);
1540
1539
  continue;
1541
1540
  }
1542
1541
  // Plain equality
@@ -1544,6 +1543,15 @@ export class QueryInterface {
1544
1543
  }
1545
1544
  return parts.join('&');
1546
1545
  }
1546
+ /**
1547
+ * Produce a value-invariant fingerprint for array filters while preserving
1548
+ * parameterless boolean operators that change SQL shape.
1549
+ */
1550
+ fingerprintArrayFilter(filter) {
1551
+ const keys = Object.keys(filter).sort();
1552
+ const suffix = filter.isEmpty === undefined ? '' : `:empty=${filter.isEmpty ? 'true' : 'false'}`;
1553
+ return `${keys.join(',')}${suffix}`;
1554
+ }
1547
1555
  /**
1548
1556
  * Fingerprint a relation filter sub-where for some/every/none.
1549
1557
  */
@@ -2650,12 +2658,12 @@ export class QueryInterface {
2650
2658
  clauses.push(`${column} && ${this.p(params.length)}::${elementType}[]`);
2651
2659
  }
2652
2660
  if (filter.isEmpty === true) {
2653
- // array_length(column, 1) IS NULL
2654
- clauses.push(`array_length(${column}, 1) IS NULL`);
2661
+ // Treat NULL and empty arrays as empty for Prisma-compatible ergonomics.
2662
+ clauses.push(`COALESCE(cardinality(${column}), 0) = 0`);
2655
2663
  }
2656
2664
  else if (filter.isEmpty === false) {
2657
- // array_length(column, 1) IS NOT NULL
2658
- clauses.push(`array_length(${column}, 1) IS NOT NULL`);
2665
+ // Require at least one element; excludes both NULL and ARRAY[] values.
2666
+ clauses.push(`cardinality(${column}) > 0`);
2659
2667
  }
2660
2668
  return clauses;
2661
2669
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. 1 dependency, ~110KB",
5
5
  "type": "module",
6
6
  "exports": {
@@ -41,7 +41,7 @@
41
41
  ],
42
42
  "sideEffects": false,
43
43
  "scripts": {
44
- "prebuild": "node scripts/build-studio-ui.mjs",
44
+ "prebuild": "npm run gen:studio",
45
45
  "build": "tsc && tsc --project tsconfig.cjs.json && echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json",
46
46
  "dev": "tsc --watch",
47
47
  "typecheck": "tsc --noEmit --project tsconfig.test.json",
@@ -63,7 +63,12 @@
63
63
  "db:reset": "psql $DATABASE_URL -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;' && npm run db:seed",
64
64
  "site:dev": "cd site && npm run dev",
65
65
  "site:build": "cd site && npm run build",
66
- "site:deploy": "cd site && vercel --prod"
66
+ "site:deploy": "cd site && vercel --prod",
67
+ "gen:studio": "node scripts/build-studio-ui.mjs",
68
+ "pretypecheck": "npm run gen:studio",
69
+ "pretest": "npm run gen:studio",
70
+ "pretest:unit": "npm run gen:studio",
71
+ "pretest:coverage": "npm run gen:studio"
67
72
  },
68
73
  "engines": {
69
74
  "node": ">=18.0.0"