turbine-orm 0.14.0 → 0.15.0

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.
@@ -11,7 +11,8 @@
11
11
  * metadata — nothing is hardcoded.
12
12
  */
13
13
  import { postgresDialect } from '../dialect.js';
14
- import { CircularRelationError, NotFoundError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
14
+ import { CircularRelationError, NotFoundError, OptimisticLockError, RelationError, TimeoutError, ValidationError, wrapPgError, } from '../errors.js';
15
+ import { executeNestedCreate, executeNestedUpdate, hasRelationFields, } from '../nested-write.js';
15
16
  import { camelToSnake, snakeToCamel } from '../schema.js';
16
17
  import { escapeLike, LRUCache, OPERATOR_KEYS, sqlToPreparedName } from './utils.js';
17
18
  // ---------------------------------------------------------------------------
@@ -97,6 +98,28 @@ function findArrayUniqueKey(value) {
97
98
  }
98
99
  return null;
99
100
  }
101
+ /** Known text search operator keys */
102
+ const TEXT_SEARCH_KEYS = new Set(['search', 'config']);
103
+ /** Check if a value is a TextSearchFilter object */
104
+ function isTextSearchFilter(value) {
105
+ if (value === null ||
106
+ value === undefined ||
107
+ typeof value !== 'object' ||
108
+ Array.isArray(value) ||
109
+ value instanceof Date) {
110
+ return false;
111
+ }
112
+ const keys = Object.keys(value);
113
+ // Must have 'search' key and only known text search keys
114
+ return keys.includes('search') && keys.every((k) => TEXT_SEARCH_KEYS.has(k));
115
+ }
116
+ /**
117
+ * Validate a text search config name. Only alphanumeric characters and
118
+ * underscores are allowed to prevent SQL injection via the config parameter.
119
+ */
120
+ function validateTextSearchConfig(config) {
121
+ return /^[a-zA-Z0-9_]+$/.test(config);
122
+ }
100
123
  // biome-ignore lint/complexity/noBannedTypes: {} means "no relations known" — intentional for untyped table access
101
124
  export class QueryInterface {
102
125
  pool;
@@ -128,6 +151,10 @@ export class QueryInterface {
128
151
  columnArrayTypeMap;
129
152
  /** Tracks tables that have already triggered a deep-with warning (one-time) */
130
153
  deepWithWarned = new Set();
154
+ /** True when this QI runs inside an active transaction (set via _txScoped option). */
155
+ txScoped;
156
+ /** Original options reference — forwarded to child QIs in nested writes. */
157
+ options;
131
158
  constructor(pool, table, schema, middlewares, options) {
132
159
  this.pool = pool;
133
160
  this.table = table;
@@ -146,6 +173,8 @@ export class QueryInterface {
146
173
  this.preparedStatementsEnabled = options?.preparedStatements ?? true;
147
174
  this.sqlCacheEnabled = options?.sqlCache !== false;
148
175
  this.dialect = options?.dialect ?? postgresDialect;
176
+ this.txScoped = options?._txScoped ?? false;
177
+ this.options = options;
149
178
  // Pre-compute column type lookup maps (TASK-26)
150
179
  this.columnPgTypeMap = new Map();
151
180
  this.columnArrayTypeMap = new Map();
@@ -723,6 +752,9 @@ export class QueryInterface {
723
752
  // -------------------------------------------------------------------------
724
753
  async create(args) {
725
754
  return this.executeWithMiddleware('create', args, async () => {
755
+ if (hasRelationFields(args.data, this.tableMeta)) {
756
+ return this.nestedCreate(args);
757
+ }
726
758
  const deferred = this.buildCreate(args);
727
759
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
728
760
  return deferred.transform(result);
@@ -805,6 +837,9 @@ export class QueryInterface {
805
837
  // -------------------------------------------------------------------------
806
838
  async update(args) {
807
839
  return this.executeWithMiddleware('update', args, async () => {
840
+ if (hasRelationFields(args.data, this.tableMeta)) {
841
+ return this.nestedUpdate(args);
842
+ }
808
843
  const deferred = this.buildUpdate(args);
809
844
  const result = await this.queryWithTimeout(deferred.sql, deferred.params, args.timeout, deferred.preparedName);
810
845
  return deferred.transform(result);
@@ -813,32 +848,62 @@ export class QueryInterface {
813
848
  buildUpdate(args) {
814
849
  const dataObj = args.data;
815
850
  const whereObj = args.where;
851
+ const lock = args.optimisticLock;
816
852
  const setFp = this.fingerprintSet(dataObj);
817
853
  const whereFp = this.fingerprintWhere(whereObj);
818
- const ck = `u:${setFp}|${whereFp}`;
854
+ const ck = lock ? null : `u:${setFp}|${whereFp}`;
819
855
  const params = [];
820
- const entry = this.acquireSql(ck, () => {
856
+ const buildSql = () => {
821
857
  const freshParams = [];
822
858
  const setEntries = Object.entries(dataObj).filter(([, v]) => v !== undefined);
823
859
  const setClauses = setEntries.map(([k, v]) => this.buildSetClause(k, v, freshParams));
860
+ if (lock) {
861
+ const versionCol = this.toSqlColumn(lock.field);
862
+ setClauses.push(`${versionCol} = ${versionCol} + 1`);
863
+ }
824
864
  const whereClause = this.buildWhereClause(whereObj, freshParams);
825
- const whereSql = whereClause ? ` WHERE ${whereClause}` : '';
865
+ let whereSql = whereClause ? ` WHERE ${whereClause}` : '';
866
+ if (lock) {
867
+ const versionCol = this.toSqlColumn(lock.field);
868
+ freshParams.push(lock.expected);
869
+ const versionCheck = `${versionCol} = ${this.p(freshParams.length)}`;
870
+ whereSql = whereSql ? `${whereSql} AND ${versionCheck}` : ` WHERE ${versionCheck}`;
871
+ }
826
872
  this.assertMutationHasPredicate('update', whereSql, args.allowFullTableScan);
827
873
  return `UPDATE ${this.q(this.table)} SET ${setClauses.join(', ')}${whereSql}${this.dialect.buildReturningClause('*')}`;
828
- });
829
- // On cache hit, validate predicate
830
- if (whereFp === '') {
831
- this.assertMutationHasPredicate('update', '', args.allowFullTableScan);
874
+ };
875
+ let sql;
876
+ let preparedName;
877
+ if (ck) {
878
+ const entry = this.acquireSql(ck, buildSql);
879
+ sql = entry.sql;
880
+ preparedName = entry.name;
881
+ if (whereFp === '') {
882
+ this.assertMutationHasPredicate('update', '', args.allowFullTableScan);
883
+ }
832
884
  }
833
- // Collect params: SET first, then WHERE (same order as fresh build)
885
+ else {
886
+ sql = buildSql();
887
+ }
888
+ // Collect params: SET first, then WHERE, then version check (same order as fresh build)
834
889
  this.collectSetParams(dataObj, params);
835
890
  this.collectWhereParams(whereObj, params);
891
+ if (lock) {
892
+ params.push(lock.expected);
893
+ }
836
894
  return {
837
- sql: entry.sql,
895
+ sql,
838
896
  params,
839
897
  transform: (result) => {
840
898
  const row = result.rows[0];
841
899
  if (!row) {
900
+ if (lock) {
901
+ throw new OptimisticLockError({
902
+ table: this.table,
903
+ versionField: lock.field,
904
+ expectedVersion: lock.expected,
905
+ });
906
+ }
842
907
  throw new NotFoundError({
843
908
  table: this.table,
844
909
  where: args.where,
@@ -848,7 +913,75 @@ export class QueryInterface {
848
913
  return this.parseRow(row, this.table);
849
914
  },
850
915
  tag: `${this.table}.update`,
851
- preparedName: entry.name,
916
+ preparedName,
917
+ };
918
+ }
919
+ // -------------------------------------------------------------------------
920
+ // Nested write helpers (shared by create + update)
921
+ // -------------------------------------------------------------------------
922
+ async nestedCreate(args) {
923
+ const data = args.data;
924
+ if (this.txScoped) {
925
+ const ctx = this.buildNestedCtx();
926
+ return executeNestedCreate(ctx, this.table, data);
927
+ }
928
+ return this.runInImplicitTx(async (ctx) => {
929
+ const result = await executeNestedCreate(ctx, this.table, data);
930
+ return result;
931
+ });
932
+ }
933
+ async nestedUpdate(args) {
934
+ const data = args.data;
935
+ const where = args.where;
936
+ if (this.txScoped) {
937
+ const ctx = this.buildNestedCtx();
938
+ return executeNestedUpdate(ctx, this.table, where, data);
939
+ }
940
+ return this.runInImplicitTx(async (ctx) => {
941
+ const result = await executeNestedUpdate(ctx, this.table, where, data);
942
+ return result;
943
+ });
944
+ }
945
+ async runInImplicitTx(fn) {
946
+ const client = await this.pool.connect();
947
+ try {
948
+ await client.query('BEGIN');
949
+ const { TransactionClient } = await import('../client.js');
950
+ // biome-ignore lint/suspicious/noExplicitAny: MiddlewareFn and Middleware are structurally identical
951
+ const tx = new TransactionClient(client, this.schema, this.middlewares, this.options);
952
+ // biome-ignore lint/suspicious/noExplicitAny: TransactionClient satisfies NestedWriteContext['tx'] at runtime
953
+ const ctx = { schema: this.schema, tx: tx };
954
+ const result = await fn(ctx);
955
+ await client.query('COMMIT');
956
+ return result;
957
+ }
958
+ catch (err) {
959
+ try {
960
+ await client.query('ROLLBACK');
961
+ }
962
+ catch {
963
+ // Best-effort rollback — connection may have died.
964
+ }
965
+ throw err;
966
+ }
967
+ finally {
968
+ client.release();
969
+ }
970
+ }
971
+ buildNestedCtx() {
972
+ const pool = this.pool;
973
+ const schema = this.schema;
974
+ const middlewares = this.middlewares;
975
+ const opts = { ...this.options, _txScoped: true };
976
+ return {
977
+ schema,
978
+ tx: this.makeTxProxy(pool, schema, middlewares, opts),
979
+ };
980
+ }
981
+ // biome-ignore lint/suspicious/noExplicitAny: bridges MiddlewareFn[] ↔ Middleware[] and QI ↔ NestedWriteContext type gap
982
+ makeTxProxy(pool, schema, middlewares, opts) {
983
+ return {
984
+ table: (name) => new QueryInterface(pool, name, schema, middlewares, opts),
852
985
  };
853
986
  }
854
987
  // -------------------------------------------------------------------------
@@ -1538,6 +1671,12 @@ export class QueryInterface {
1538
1671
  parts.push(`${key}:arr(${this.fingerprintArrayFilter(value)})`);
1539
1672
  continue;
1540
1673
  }
1674
+ // Text search filter
1675
+ if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
1676
+ const cfg = value.config ?? 'english';
1677
+ parts.push(`${key}:fts(${cfg})`);
1678
+ continue;
1679
+ }
1541
1680
  // Plain equality
1542
1681
  parts.push(`${key}:eq`);
1543
1682
  }
@@ -1653,6 +1792,11 @@ export class QueryInterface {
1653
1792
  continue;
1654
1793
  }
1655
1794
  }
1795
+ // Text search filter
1796
+ if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
1797
+ params.push(value.search);
1798
+ continue;
1799
+ }
1656
1800
  // Operator objects
1657
1801
  if (isWhereOperator(value)) {
1658
1802
  this.collectOperatorParams(value, params);
@@ -2027,6 +2171,12 @@ export class QueryInterface {
2027
2171
  `(actual type: ${colType}); cannot apply array operator '${arrayKey}'.`);
2028
2172
  }
2029
2173
  }
2174
+ // Handle full-text search filter
2175
+ if (typeof value === 'object' && !Array.isArray(value) && isTextSearchFilter(value)) {
2176
+ const tsClause = this.buildTextSearchClause(column, value, params);
2177
+ andClauses.push(tsClause);
2178
+ continue;
2179
+ }
2030
2180
  // Handle operator objects
2031
2181
  if (isWhereOperator(value)) {
2032
2182
  const opClauses = this.buildOperatorClauses(column, value, params);
@@ -2669,6 +2819,18 @@ export class QueryInterface {
2669
2819
  }
2670
2820
  return clauses;
2671
2821
  }
2822
+ /**
2823
+ * Build SQL clause for full-text search using to_tsvector @@ to_tsquery.
2824
+ * The config name is validated to prevent injection (only alphanumeric + underscore).
2825
+ */
2826
+ buildTextSearchClause(column, filter, params) {
2827
+ const config = filter.config ?? 'english';
2828
+ if (!validateTextSearchConfig(config)) {
2829
+ throw new ValidationError(`[turbine] Invalid text search config "${config}": only alphanumeric characters and underscores are allowed.`);
2830
+ }
2831
+ params.push(filter.search);
2832
+ return `to_tsvector('${config}', ${column}) @@ to_tsquery('${config}', ${this.p(params.length)})`;
2833
+ }
2672
2834
  /**
2673
2835
  * Get the Postgres array type for a column (used by UNNEST in createMany).
2674
2836
  * Uses pre-computed Map for O(1) lookup instead of linear scan.
@@ -5,7 +5,7 @@
5
5
  * `import { … } from './query/index.js'` is a drop-in replacement for the
6
6
  * former monolithic `import { … } from './query.js'`.
7
7
  */
8
- export type { AggregateArgs, AggregateResult, ArrayFilter, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, OrderDirection, RelationDescriptor, RelationFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
8
+ export type { AggregateArgs, AggregateResult, ArrayFilter, ConnectOrCreateOp, CountArgs, CreateArgs, CreateManyArgs, DeleteArgs, DeleteManyArgs, FieldResult, FindManyArgs, FindManyStreamArgs, FindUniqueArgs, GroupByArgs, JsonFilter, NestedCreateOp, NestedUpdateOp, OmitResult, OrderDirection, QueryResult, RelationDescriptor, RelationFilter, SelectResult, TextSearchFilter, TypedWithClause, UpdateArgs, UpdateInput, UpdateManyArgs, UpdateOperatorInput, UpsertArgs, WhereClause, WhereOperator, WhereValue, WithClause, WithOptions, WithResult, } from './types.js';
9
9
  export type { BuiltStatement, BulkInsertStatementInput, ColumnDefinitionInput, ColumnTypeInput, CreateIndexStatementInput, CreateTableStatementInput, Dialect, InsertStatementInput, UpsertStatementInput, } from '../dialect.js';
10
10
  export { postgresDialect } from '../dialect.js';
11
11
  export type { SqlCacheEntry } from './utils.js';
@@ -26,8 +26,9 @@ export interface WhereOperator<V = unknown> {
26
26
  * - An operator object ({ gt: 5, lte: 10 })
27
27
  * - A JSONB filter object ({ contains, equals, path, hasKey })
28
28
  * - An array filter object ({ has, hasEvery, hasSome, isEmpty })
29
+ * - A text search filter object ({ search, config? })
29
30
  */
30
- export type WhereValue<V = unknown> = V | WhereOperator<V> | JsonFilter | ArrayFilter | null;
31
+ export type WhereValue<V = unknown> = V | WhereOperator<V> | JsonFilter | ArrayFilter | TextSearchFilter | null;
31
32
  /**
32
33
  * Where clause type: each field can be a plain value, null, or operator object.
33
34
  * Special keys: OR for disjunctive conditions.
@@ -162,18 +163,40 @@ export type WithResult<T, R extends object, W> = [keyof R] extends [never] ? T :
162
163
  with?: infer NestedW;
163
164
  } ? NestedW extends object ? ApplyCardinality<R[K], WithResult<RelationTarget<R[K]>, RelationRelations<R[K]> & object, NestedW>> : ApplyCardinality<R[K], RelationTarget<R[K]>> : ApplyCardinality<R[K], RelationTarget<R[K]>>;
164
165
  } : T;
165
- export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
166
+ /** Extract keys from a boolean record where the value is `true`. */
167
+ type TrueKeys<S extends Record<string, boolean>> = {
168
+ [K in keyof S]: S[K] extends true ? K : never;
169
+ }[keyof S];
170
+ /** Pick only the fields from T that are selected (value = true) in S. */
171
+ export type SelectResult<T, S extends Record<string, boolean> | undefined> = S extends Record<string, boolean> ? Pick<T, Extract<keyof T, TrueKeys<S>>> : T;
172
+ /** Omit the fields from T that are marked (value = true) in O. */
173
+ export type OmitResult<T, O extends Record<string, boolean> | undefined> = O extends Record<string, boolean> ? Omit<T, Extract<keyof T, TrueKeys<O>>> : T;
174
+ /**
175
+ * Apply select or omit field narrowing to a base type. Select takes priority —
176
+ * when both are provided, only select is applied (matching runtime behavior).
177
+ */
178
+ export type FieldResult<T, S extends Record<string, boolean> | undefined, O extends Record<string, boolean> | undefined> = S extends Record<string, boolean> ? SelectResult<T, S> : OmitResult<T, O>;
179
+ /**
180
+ * Compute the full query result type: apply field narrowing to the base entity,
181
+ * then add relation additions from the `with` clause. Relations are unaffected
182
+ * by select/omit (they are separate JSON subqueries at the SQL level).
183
+ *
184
+ * Short-circuits to plain WithResult when neither select nor omit is provided,
185
+ * preserving exact type equality with the pre-narrowing era.
186
+ */
187
+ export type QueryResult<T, R extends object, W, S extends Record<string, boolean> | undefined, O extends Record<string, boolean> | undefined> = S extends undefined ? O extends undefined ? WithResult<T, R, W> : O extends Record<string, boolean> ? Omit<WithResult<T, R, W>, Extract<keyof T, TrueKeys<O>>> : WithResult<T, R, W> : S extends Record<string, boolean> ? Pick<WithResult<T, R, W>, Extract<keyof T, TrueKeys<S>> | Exclude<keyof WithResult<T, R, W>, keyof T>> : WithResult<T, R, W>;
188
+ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
166
189
  where: WhereClause<T>;
167
- select?: Record<string, boolean>;
168
- omit?: Record<string, boolean>;
190
+ select?: S;
191
+ omit?: O;
169
192
  with?: W;
170
193
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
171
194
  timeout?: number;
172
195
  }
173
- export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> {
196
+ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> {
174
197
  where?: WhereClause<T>;
175
- select?: Record<string, boolean>;
176
- omit?: Record<string, boolean>;
198
+ select?: S;
199
+ omit?: O;
177
200
  orderBy?: Record<string, OrderDirection>;
178
201
  limit?: number;
179
202
  offset?: number;
@@ -187,7 +210,7 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
187
210
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
188
211
  timeout?: number;
189
212
  }
190
- export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>> extends FindManyArgs<T, R, W> {
213
+ export interface FindManyStreamArgs<T, R extends object = {}, W extends TypedWithClause<R> = TypedWithClause<R>, S extends Record<string, boolean> | undefined = undefined, O extends Record<string, boolean> | undefined = undefined> extends FindManyArgs<T, R, W, S, O> {
191
214
  /**
192
215
  * Number of rows to fetch per internal FETCH batch (default: 1000).
193
216
  *
@@ -256,6 +279,27 @@ export interface UpdateArgs<T> {
256
279
  * when an unconditional mutation is the intended behaviour.
257
280
  */
258
281
  allowFullTableScan?: boolean;
282
+ /**
283
+ * Optimistic locking — prevents lost updates in concurrent scenarios.
284
+ * Specify the version field and its expected value. The update adds a
285
+ * WHERE check on the version and auto-increments it. If the row was
286
+ * modified by another transaction, throws `OptimisticLockError`.
287
+ *
288
+ * @example
289
+ * ```ts
290
+ * await db.posts.update({
291
+ * where: { id: 1 },
292
+ * data: { title: 'new title' },
293
+ * optimisticLock: { field: 'version', expected: 3 },
294
+ * });
295
+ * // Generates: UPDATE posts SET title=$1, version=version+1
296
+ * // WHERE id=$2 AND version=$3 RETURNING *
297
+ * ```
298
+ */
299
+ optimisticLock?: {
300
+ field: keyof T & string;
301
+ expected: number;
302
+ };
259
303
  }
260
304
  export interface UpdateManyArgs<T> {
261
305
  where: WhereClause<T>;
@@ -286,6 +330,23 @@ export interface UpsertArgs<T> {
286
330
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
287
331
  timeout?: number;
288
332
  }
333
+ export interface ConnectOrCreateOp<T> {
334
+ where: Partial<T>;
335
+ create: Partial<T>;
336
+ }
337
+ export interface NestedCreateOp<T> {
338
+ create?: Partial<T> | Partial<T>[];
339
+ connect?: Partial<T> | Partial<T>[];
340
+ connectOrCreate?: ConnectOrCreateOp<T> | ConnectOrCreateOp<T>[];
341
+ }
342
+ export interface NestedUpdateOp<T> {
343
+ create?: Partial<T> | Partial<T>[];
344
+ connect?: Partial<T> | Partial<T>[];
345
+ connectOrCreate?: ConnectOrCreateOp<T> | ConnectOrCreateOp<T>[];
346
+ disconnect?: Partial<T> | Partial<T>[];
347
+ set?: Partial<T>[];
348
+ delete?: Partial<T> | Partial<T>[];
349
+ }
289
350
  export interface CountArgs<T> {
290
351
  where?: WhereClause<T>;
291
352
  /** Query timeout in milliseconds. Rejects with an error if exceeded. */
@@ -361,5 +422,12 @@ export interface ArrayFilter {
361
422
  /** Check if array is empty: array_length(column, 1) IS NULL */
362
423
  isEmpty?: boolean;
363
424
  }
425
+ /** Full-text search filter using PostgreSQL to_tsvector / to_tsquery */
426
+ export interface TextSearchFilter {
427
+ /** The search query string passed to to_tsquery */
428
+ search: string;
429
+ /** PostgreSQL text search configuration name (defaults to 'english') */
430
+ config?: string;
431
+ }
364
432
  export {};
365
433
  //# sourceMappingURL=types.d.ts.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.14.0",
3
+ "version": "0.15.0",
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": {
@@ -49,7 +49,7 @@
49
49
  "status": "tsx src/cli/index.ts status",
50
50
  "examples": "tsx examples/examples.ts",
51
51
  "test": "tsx --test src/test/*.test.ts",
52
- "test:unit": "tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts src/test/cockroachdb-adapter.test.ts src/test/yugabytedb-adapter.test.ts src/test/pg-compat.test.ts src/test/relation-filter-validation.test.ts src/test/client-coverage.test.ts src/test/schema-diff.test.ts src/test/composite-fk.test.ts",
52
+ "test:unit": "tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts src/test/cockroachdb-adapter.test.ts src/test/yugabytedb-adapter.test.ts src/test/pg-compat.test.ts src/test/relation-filter-validation.test.ts src/test/client-coverage.test.ts src/test/schema-diff.test.ts src/test/composite-fk.test.ts src/test/retry.test.ts src/test/text-search.test.ts src/test/optimistic-lock.test.ts src/test/sql-safety-property.test.ts",
53
53
  "test:coverage": "c8 tsx --test src/test/schema-builder.test.ts src/test/errors.test.ts src/test/stress.test.ts src/test/migrate.test.ts src/test/update-operators.test.ts src/test/empty-where-guard.test.ts src/test/cli.test.ts src/test/serverless.test.ts src/test/pipeline.test.ts src/test/pipeline-submittable.test.ts src/test/with-inference.test.ts src/test/operator-validation.test.ts src/test/unlimited-warning.test.ts src/test/generate-relations.test.ts src/test/stream-and-parse.test.ts src/test/sql-cache.test.ts src/test/dialect.test.ts src/test/studio.test.ts src/test/sql-injection.test.ts src/test/cli-flags.test.ts",
54
54
  "lint": "biome check src/",
55
55
  "lint:fix": "biome check --write src/",