turbine-orm 0.40.1 → 0.41.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.
Files changed (63) hide show
  1. package/README.md +22 -4
  2. package/dist/cjs/cli/config.js +3 -0
  3. package/dist/cjs/cli/index.js +179 -0
  4. package/dist/cjs/cli/prisma-report.js +216 -0
  5. package/dist/cjs/cli/prisma-resolve.js +335 -0
  6. package/dist/cjs/cli/prisma-schema.js +484 -0
  7. package/dist/cjs/client.js +1 -0
  8. package/dist/cjs/generate.js +279 -22
  9. package/dist/cjs/index.js +3 -2
  10. package/dist/cjs/introspect.js +203 -26
  11. package/dist/cjs/mssql.js +9 -10
  12. package/dist/cjs/mysql.js +3 -9
  13. package/dist/cjs/powdb-introspect.js +5 -10
  14. package/dist/cjs/powql.js +13 -0
  15. package/dist/cjs/prisma-compat.js +1147 -0
  16. package/dist/cjs/query/aggregates.js +67 -7
  17. package/dist/cjs/query/builder.js +388 -17
  18. package/dist/cjs/query/compound-unique.js +0 -0
  19. package/dist/cjs/query/relations.js +7 -5
  20. package/dist/cjs/query/warn-registry.js +98 -0
  21. package/dist/cjs/query/writes.js +13 -5
  22. package/dist/cjs/schema.js +47 -0
  23. package/dist/cjs/sqlite.js +4 -9
  24. package/dist/cli/config.d.ts +26 -0
  25. package/dist/cli/config.js +3 -0
  26. package/dist/cli/index.d.ts +11 -0
  27. package/dist/cli/index.js +180 -1
  28. package/dist/cli/prisma-report.d.ts +19 -0
  29. package/dist/cli/prisma-report.js +211 -0
  30. package/dist/cli/prisma-resolve.d.ts +87 -0
  31. package/dist/cli/prisma-resolve.js +330 -0
  32. package/dist/cli/prisma-schema.d.ts +116 -0
  33. package/dist/cli/prisma-schema.js +479 -0
  34. package/dist/cli/ui.d.ts +1 -1
  35. package/dist/client.d.ts +18 -2
  36. package/dist/client.js +1 -0
  37. package/dist/generate.d.ts +80 -1
  38. package/dist/generate.js +277 -25
  39. package/dist/index.d.ts +2 -2
  40. package/dist/index.js +1 -1
  41. package/dist/introspect.d.ts +92 -2
  42. package/dist/introspect.js +198 -26
  43. package/dist/mssql.js +10 -11
  44. package/dist/mysql.js +4 -10
  45. package/dist/powdb-introspect.js +5 -10
  46. package/dist/powql.js +13 -0
  47. package/dist/prisma-compat.d.ts +281 -0
  48. package/dist/prisma-compat.js +1143 -0
  49. package/dist/query/aggregates.js +67 -7
  50. package/dist/query/builder.d.ts +77 -4
  51. package/dist/query/builder.js +390 -19
  52. package/dist/query/compound-unique.d.ts +49 -0
  53. package/dist/query/compound-unique.js +0 -0
  54. package/dist/query/deferred.d.ts +18 -0
  55. package/dist/query/relations.js +7 -5
  56. package/dist/query/types.d.ts +70 -9
  57. package/dist/query/warn-registry.d.ts +57 -0
  58. package/dist/query/warn-registry.js +92 -0
  59. package/dist/query/writes.js +13 -5
  60. package/dist/schema.d.ts +75 -0
  61. package/dist/schema.js +46 -0
  62. package/dist/sqlite.js +5 -10
  63. package/package.json +6 -1
@@ -15,13 +15,30 @@ export type OrderDirection = 'asc' | 'desc';
15
15
  * relation (`WHERE fk = ANY($1)`), stitching children client-side. D levels
16
16
  * cost D extra round-trips, but each is a single key-set lookup and rows come
17
17
  * back flat (a win when FK columns are unindexed or result sets are huge).
18
+ * - `'auto'` (the implicit default on SQL engines since 0.41): per relation,
19
+ * use `'join'` unless the introspected metadata PROVES the probe columns are
20
+ * unindexed, in which case that relation falls back to the batched loader
21
+ * (where a correlated per-parent scan would be pathological). Requires
22
+ * DB-backed index metadata (a generated / introspected client); a code-first
23
+ * `defineSchema`-only client has no index info to prove anything, so `'auto'`
24
+ * behaves exactly like `'join'` there. An EXPLICIT `'join'` or `'batched'` at
25
+ * the client or query level always wins and disables the per-relation
26
+ * fallback. Output is byte-for-byte identical to `'join'` (the batched loader
27
+ * guarantees the same result SHAPE); child-array order for a to-many relation
28
+ * WITHOUT an `orderBy` may differ from the join plan (order was never
29
+ * guaranteed without `orderBy`, see `stableRelationOrder` to pin it).
30
+ * Composite-key relations stay on the join plan (the batched loader does not
31
+ * support them). Engagement emits a once-per-relation dev note and a
32
+ * `strategy` tag on the query event.
18
33
  *
19
34
  * Precedence: per-query arg > client `relationLoadStrategy` config > the engine
20
- * default. On SQL engines the default is `'join'`; on PowDB the default is the
35
+ * default. On SQL engines the default is `'auto'`; on PowDB the default is the
21
36
  * batched loaders (an ineligible relation falls back to them per-relation and
22
- * silently even when `'join'` is requested).
37
+ * silently even when `'join'` is requested). On PowDB, `'auto'` resolves to
38
+ * PowDB's own existing default (loaders / nested projections); it never selects
39
+ * a distinct PowQL code path.
23
40
  */
24
- export type RelationLoadStrategy = 'join' | 'batched';
41
+ export type RelationLoadStrategy = 'join' | 'batched' | 'auto';
25
42
  /**
26
43
  * Reference to ANOTHER COLUMN of the same table inside a where operator,
27
44
  * enabling column-to-column comparison:
@@ -330,6 +347,8 @@ export interface FindUniqueArgs<T, R extends object = {}, W extends TypedWithCla
330
347
  timeout?: number;
331
348
  /** Override the client's relation-loading strategy for this query. See {@link RelationLoadStrategy}. */
332
349
  relationLoadStrategy?: RelationLoadStrategy;
350
+ /** Override the client's {@link TurbineConfig.stableRelationOrder} for this query. */
351
+ stableRelationOrder?: boolean;
333
352
  /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
334
353
  skipGlobalFilters?: SkipGlobalFilters;
335
354
  /** Include PII-tagged columns in the result. See {@link FindManyArgs.includePii}. */
@@ -353,6 +372,15 @@ export interface FindManyArgs<T, R extends object = {}, W extends TypedWithClaus
353
372
  timeout?: number;
354
373
  /** Override the client's relation-loading strategy for this query. See {@link RelationLoadStrategy}. */
355
374
  relationLoadStrategy?: RelationLoadStrategy;
375
+ /**
376
+ * Override the client's {@link TurbineConfig.stableRelationOrder} for this
377
+ * query. When `true`, every to-many `with` relation that has no explicit
378
+ * `orderBy` is loaded ordered by the target table's primary key ascending, so
379
+ * unordered child arrays come back in a deterministic order. An explicit
380
+ * per-relation `orderBy` always wins. Off by default; when off the emitted SQL
381
+ * is byte-identical to before.
382
+ */
383
+ stableRelationOrder?: boolean;
356
384
  /** Opt out of configured {@link GlobalFilters}. See {@link SkipGlobalFilters}. */
357
385
  skipGlobalFilters?: SkipGlobalFilters;
358
386
  /**
@@ -732,8 +760,16 @@ export interface GroupByArgs<T> {
732
760
  * (`SELECT DISTINCT ON … ORDER BY …` row source). See {@link GroupByDistinctOn}.
733
761
  */
734
762
  distinctOn?: GroupByDistinctOn<T>;
735
- /** Include count of each group */
736
- _count?: true;
763
+ /**
764
+ * Count each group. `true` (or omitted) → `_count: number` (COUNT(*)). The
765
+ * record form counts per selection: the reserved `_all: true` key → COUNT(*),
766
+ * and each entity field key → COUNT(that column), yielding
767
+ * `_count: { _all: n, field: n }` (Prisma parity). Ordering/HAVING on `_count`
768
+ * stays available whenever COUNT(*) is selected (`true`, omitted, or `_all`).
769
+ */
770
+ _count?: true | ({
771
+ _all?: true;
772
+ } & Partial<Record<keyof T & string, boolean>>);
737
773
  /** Sum of numeric fields (or JSON paths: see {@link JsonPathAggregateTarget}) in each group */
738
774
  _sum?: GroupByAggregateSpec<T>;
739
775
  /** Average of numeric fields (or JSON paths) in each group */
@@ -817,14 +853,39 @@ type GroupByMinMaxPart<T, A, Key extends '_min' | '_max'> = A extends {
817
853
  */
818
854
  export type GroupByResult<T, A> = {
819
855
  [K in GroupByFieldKeys<T, A>]: T[K];
820
- } & {
856
+ } & GroupByCountPart<A> & GroupBySumAvgPart<A, '_sum'> & GroupBySumAvgPart<A, '_avg'> & GroupByMinMaxPart<T, A, '_min'> & GroupByMinMaxPart<T, A, '_max'>;
857
+ /**
858
+ * The `_count` block on a groupBy result row. Scalar `_count: true` (or an
859
+ * omitted `_count`, which still selects COUNT(*) by default) yields a plain
860
+ * `number`; the record form (`_count: { _all: true, field: true }`) yields a
861
+ * per-selection object `{ _all: number, field: number }` (Prisma parity). Kept
862
+ * additive: existing `_count: true` / no-`_count` calls still infer `number`.
863
+ */
864
+ type GroupByCountPart<A> = A extends {
865
+ _count: infer C;
866
+ } ? C extends true ? {
867
+ _count: number;
868
+ } : [C] extends [object] ? {
869
+ _count: {
870
+ [K in keyof C & string]: number;
871
+ };
872
+ } : {
873
+ _count: number;
874
+ } : {
821
875
  _count: number;
822
- } & GroupBySumAvgPart<A, '_sum'> & GroupBySumAvgPart<A, '_avg'> & GroupByMinMaxPart<T, A, '_min'> & GroupByMinMaxPart<T, A, '_max'>;
876
+ };
823
877
  /** Arguments for the standalone aggregate method */
824
878
  export interface AggregateArgs<T> {
825
879
  where?: WhereClause<T>;
826
- /** Count all rows matching the filter */
827
- _count?: true | Partial<Record<keyof T & string, boolean>>;
880
+ /**
881
+ * Count rows. `true` `_count: number` (COUNT(*)). The record form counts per
882
+ * selection: the reserved `_all: true` key → COUNT(*), and each entity field
883
+ * key → COUNT(that column) (non-null count), yielding
884
+ * `_count: { _all: n, field: n }` (Prisma parity).
885
+ */
886
+ _count?: true | ({
887
+ _all?: true;
888
+ } & Partial<Record<keyof T & string, boolean>>);
828
889
  /** Sum of numeric fields */
829
890
  _sum?: Partial<Record<keyof T & string, boolean>>;
830
891
  /** Average of numeric fields */
@@ -0,0 +1,57 @@
1
+ /**
2
+ * turbine-orm, process-wide once-per-key dev-warning dedupe registry.
3
+ *
4
+ * Several dev-only diagnostics (the missing-FK-index warning in relations.ts,
5
+ * the `relationLoadStrategy: 'auto'` engagement note, the deep-`with` warning)
6
+ * must fire AT MOST ONCE per distinct key for the life of the process. A
7
+ * module-level `Set` almost does this, but it is defeated by the two field
8
+ * realities this package actually ships into:
9
+ *
10
+ * 1. **Dual-package loading.** Turbine ships ESM (`dist/`) AND CJS
11
+ * (`dist/cjs/`). A mixed `require`/`import` graph (a compat layer, a tool
12
+ * that loads both) instantiates the module twice, giving two independent
13
+ * `Set`s that each warn once, a double warning.
14
+ * 2. **Bundler / HMR re-evaluation.** Under Next.js dev the module is
15
+ * re-evaluated per recompile, resetting a module-level `Set` and making the
16
+ * warning appear to fire every time.
17
+ *
18
+ * Hanging the registry off `globalThis` under a `Symbol.for(...)` key gives every
19
+ * module copy in the realm ONE shared registry (cross-copy identity without
20
+ * polluting enumerable globals), and `globalThis` survives webpack recompiles
21
+ * because the realm persists, which is exactly what fixes the every-recompile
22
+ * firing in dev servers. Per-process firing (worker threads, separate processes)
23
+ * is acceptable and stays.
24
+ *
25
+ * Bounded: each namespace stops recording AND stops warning once it reaches
26
+ * {@link WARN_ONCE_CAP} distinct keys. A schema with 500+ distinct unindexed
27
+ * relations has long since gotten the message, and the cap prevents unbounded
28
+ * growth if metadata objects are churned dynamically. (Clearing on overflow
29
+ * would be wrong, it would re-warn.)
30
+ */
31
+ /** Per-namespace cap on distinct recorded keys (see module doc). */
32
+ export declare const WARN_ONCE_CAP = 500;
33
+ /**
34
+ * Record `(ns, key)` and report whether THIS call is the first to see it
35
+ * process-wide. Returns `true` exactly once per distinct key (the caller should
36
+ * emit its warning then), `false` on every subsequent call for that key, and
37
+ * `false` once the namespace has recorded {@link WARN_ONCE_CAP} distinct keys
38
+ * (bounded growth; the warning simply stops rather than re-firing).
39
+ */
40
+ export declare function shouldWarnOnce(ns: string, key: string): boolean;
41
+ /** True when `(ns, key)` has already been recorded (no mutation). */
42
+ export declare function hasWarnedOnce(ns: string, key: string): boolean;
43
+ /**
44
+ * @internal Test-only: clear one namespace, or the whole registry when `ns` is
45
+ * omitted. Lets a single test process verify that a warning fires once and then
46
+ * re-verify after a reset without spawning a new process.
47
+ */
48
+ export declare function resetWarnOnce(ns?: string): void;
49
+ /** Namespace constants so callers never typo a bare string. */
50
+ export declare const WARN_NS: {
51
+ /** Missing-FK-index runtime warning (relations.ts `buildRelationSubquery`). */
52
+ readonly unindexedRelation: "unindexedRelation";
53
+ /** `relationLoadStrategy: 'auto'` batched-fallback engagement note. */
54
+ readonly autoStrategy: "autoStrategy";
55
+ /** Deep-`with` (depth > 5) advisory (builder.ts `findMany`). */
56
+ readonly deepWith: "deepWith";
57
+ };
@@ -0,0 +1,92 @@
1
+ /**
2
+ * turbine-orm, process-wide once-per-key dev-warning dedupe registry.
3
+ *
4
+ * Several dev-only diagnostics (the missing-FK-index warning in relations.ts,
5
+ * the `relationLoadStrategy: 'auto'` engagement note, the deep-`with` warning)
6
+ * must fire AT MOST ONCE per distinct key for the life of the process. A
7
+ * module-level `Set` almost does this, but it is defeated by the two field
8
+ * realities this package actually ships into:
9
+ *
10
+ * 1. **Dual-package loading.** Turbine ships ESM (`dist/`) AND CJS
11
+ * (`dist/cjs/`). A mixed `require`/`import` graph (a compat layer, a tool
12
+ * that loads both) instantiates the module twice, giving two independent
13
+ * `Set`s that each warn once, a double warning.
14
+ * 2. **Bundler / HMR re-evaluation.** Under Next.js dev the module is
15
+ * re-evaluated per recompile, resetting a module-level `Set` and making the
16
+ * warning appear to fire every time.
17
+ *
18
+ * Hanging the registry off `globalThis` under a `Symbol.for(...)` key gives every
19
+ * module copy in the realm ONE shared registry (cross-copy identity without
20
+ * polluting enumerable globals), and `globalThis` survives webpack recompiles
21
+ * because the realm persists, which is exactly what fixes the every-recompile
22
+ * firing in dev servers. Per-process firing (worker threads, separate processes)
23
+ * is acceptable and stays.
24
+ *
25
+ * Bounded: each namespace stops recording AND stops warning once it reaches
26
+ * {@link WARN_ONCE_CAP} distinct keys. A schema with 500+ distinct unindexed
27
+ * relations has long since gotten the message, and the cap prevents unbounded
28
+ * growth if metadata objects are churned dynamically. (Clearing on overflow
29
+ * would be wrong, it would re-warn.)
30
+ */
31
+ const REGISTRY_KEY = Symbol.for('turbine.warnOnce.registry');
32
+ /** Per-namespace cap on distinct recorded keys (see module doc). */
33
+ export const WARN_ONCE_CAP = 500;
34
+ function registry() {
35
+ const g = globalThis;
36
+ let reg = g[REGISTRY_KEY];
37
+ if (!reg) {
38
+ reg = Object.create(null);
39
+ g[REGISTRY_KEY] = reg;
40
+ }
41
+ return reg;
42
+ }
43
+ function namespaceSet(ns) {
44
+ const reg = registry();
45
+ let set = reg[ns];
46
+ if (!set) {
47
+ set = new Set();
48
+ reg[ns] = set;
49
+ }
50
+ return set;
51
+ }
52
+ /**
53
+ * Record `(ns, key)` and report whether THIS call is the first to see it
54
+ * process-wide. Returns `true` exactly once per distinct key (the caller should
55
+ * emit its warning then), `false` on every subsequent call for that key, and
56
+ * `false` once the namespace has recorded {@link WARN_ONCE_CAP} distinct keys
57
+ * (bounded growth; the warning simply stops rather than re-firing).
58
+ */
59
+ export function shouldWarnOnce(ns, key) {
60
+ const set = namespaceSet(ns);
61
+ if (set.has(key))
62
+ return false;
63
+ if (set.size >= WARN_ONCE_CAP)
64
+ return false;
65
+ set.add(key);
66
+ return true;
67
+ }
68
+ /** True when `(ns, key)` has already been recorded (no mutation). */
69
+ export function hasWarnedOnce(ns, key) {
70
+ return namespaceSet(ns).has(key);
71
+ }
72
+ /**
73
+ * @internal Test-only: clear one namespace, or the whole registry when `ns` is
74
+ * omitted. Lets a single test process verify that a warning fires once and then
75
+ * re-verify after a reset without spawning a new process.
76
+ */
77
+ export function resetWarnOnce(ns) {
78
+ if (ns === undefined) {
79
+ globalThis[REGISTRY_KEY] = undefined;
80
+ return;
81
+ }
82
+ registry()[ns] = undefined;
83
+ }
84
+ /** Namespace constants so callers never typo a bare string. */
85
+ export const WARN_NS = {
86
+ /** Missing-FK-index runtime warning (relations.ts `buildRelationSubquery`). */
87
+ unindexedRelation: 'unindexedRelation',
88
+ /** `relationLoadStrategy: 'auto'` batched-fallback engagement note. */
89
+ autoStrategy: 'autoStrategy',
90
+ /** Deep-`with` (depth > 5) advisory (builder.ts `findMany`). */
91
+ deepWith: 'deepWith',
92
+ };
@@ -12,6 +12,7 @@
12
12
  */
13
13
  import { NotFoundError, OptimisticLockError, ValidationError } from '../errors.js';
14
14
  import { camelToSnake, snakeToCamel } from '../schema.js';
15
+ import { expandCompoundUniqueWhere } from './compound-unique.js';
15
16
  import { UPDATE_OPERATOR_KEYS } from './filters.js';
16
17
  import * as whereMod from './where.js';
17
18
  /**
@@ -133,7 +134,10 @@ export function buildUpdate(qi, args) {
133
134
  qi.currentSkip = args.skipGlobalFilters;
134
135
  const dataObj = args.data;
135
136
  assertNoGeneratedColumns(qi, dataObj, 'update');
136
- const userWhere = args.where;
137
+ // Prisma compound-unique selector (e.g. `{ orgId_userId: { orgId, userId } }`)
138
+ // → the column conjunction, before the empty-`where` guard so the expanded
139
+ // members count as a real predicate.
140
+ const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
137
141
  const lock = args.optimisticLock;
138
142
  // The empty-`where` guard checks the USER predicate only — a global filter
139
143
  // must never turn an unguarded mass update into an allowed one.
@@ -239,9 +243,11 @@ export function buildUpdate(qi, args) {
239
243
  export function buildDelete(qi, args) {
240
244
  assertWritable(qi, 'delete');
241
245
  qi.currentSkip = args.skipGlobalFilters;
246
+ // Prisma compound-unique selector → the column conjunction (before the guard).
247
+ const userWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
242
248
  // Guard the USER predicate (a global filter must not satisfy the guard).
243
- whereMod.assertMutationHasPredicate(qi, 'delete', whereMod.userPredicateIsEmpty(qi, args.where) ? '' : ' WHERE x', args.allowFullTableScan);
244
- const whereObj = (whereMod.mergeGlobalFilter(qi, args.where) ?? {});
249
+ whereMod.assertMutationHasPredicate(qi, 'delete', whereMod.userPredicateIsEmpty(qi, userWhere) ? '' : ' WHERE x', args.allowFullTableScan);
250
+ const whereObj = (whereMod.mergeGlobalFilter(qi, userWhere) ?? {});
245
251
  const whereFp = whereMod.fingerprintWhere(qi, whereObj);
246
252
  const ck = `d:${whereFp}${whereMod.globalFilterCacheSegment(qi)}`;
247
253
  const params = [];
@@ -292,6 +298,8 @@ export function buildUpsert(qi, args) {
292
298
  assertNoGeneratedColumns(qi, args.create, 'upsert');
293
299
  assertNoGeneratedColumns(qi, args.update, 'upsert');
294
300
  qi.currentSkip = args.skipGlobalFilters;
301
+ // Prisma compound-unique selector on the conflict target → its member columns.
302
+ const upsertWhere = expandCompoundUniqueWhere(qi.tableMeta, args.where);
295
303
  // Build the INSERT part from create data
296
304
  const createEntries = Object.entries(args.create).filter(([, v]) => v !== undefined);
297
305
  const columns = createEntries.map(([k]) => qi.toSqlColumn(k));
@@ -299,7 +307,7 @@ export function buildUpsert(qi, args) {
299
307
  // Enum columns get an explicit `::"EnumName"` cast (see enumTypeForColumn).
300
308
  const placeholders = createEntries.map(([k], i) => `${qi.p(i + 1)}${whereMod.enumCastSuffix(qi, qi.toColumn(k))}`);
301
309
  // The conflict target comes from `where` keys — must be unique/PK columns
302
- const conflictKeys = Object.keys(args.where).filter((k) => args.where[k] !== undefined);
310
+ const conflictKeys = Object.keys(upsertWhere).filter((k) => upsertWhere[k] !== undefined);
303
311
  const conflictColumns = conflictKeys.map((k) => qi.toSqlColumn(k));
304
312
  // Build the UPDATE SET part
305
313
  const updateEntries = Object.entries(args.update).filter(([, v]) => v !== undefined);
@@ -350,7 +358,7 @@ export function buildUpsert(qi, args) {
350
358
  reselect: qi.dialect.resultStrategy === 'reselect'
351
359
  ? async (exec) => {
352
360
  await exec(sql, params);
353
- const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, args.where) ?? {}));
361
+ const sel = buildReselectByWhere(qi, (whereMod.mergeGlobalFilter(qi, upsertWhere) ?? {}));
354
362
  return exec(sel.sql, sel.params);
355
363
  }
356
364
  : undefined,
package/dist/schema.d.ts CHANGED
@@ -220,3 +220,78 @@ export declare function camelToSnake(s: string): string;
220
220
  export declare function snakeToPascal(s: string): string;
221
221
  /** Naive singularize: "posts" → "post", "categories" → "category" */
222
222
  export declare function singularize(s: string): string;
223
+ /**
224
+ * Return a copy of `schema` in which every column's TypeScript **field** name is
225
+ * the raw database column name (snake_case) instead of the camelCased default,
226
+ * i.e. `user_id` stays `user_id` rather than becoming `userId`.
227
+ *
228
+ * This is a PURE, generate-time transform with ZERO runtime changes: it only
229
+ * rewrites `column.field` and rebuilds each table's `columnMap` /
230
+ * `reverseColumnMap` as IDENTITY maps (`user_id → user_id`). Because every
231
+ * runtime surface resolves column names through those maps (`toColumn`,
232
+ * `parseRow`, relation `json_build_object` keys, batched-loader stitching,
233
+ * positional decoding, aggregate/groupBy naming), the generated client returns
234
+ * rows keyed by DB column names and accepts DB column names in
235
+ * `where`/`orderBy`/`select` with no code path aware of the difference.
236
+ *
237
+ * Deliberately left untouched (the flag is literally about column names):
238
+ * - `name`, `allColumns`, `dateColumns`, `dialectTypes`, `pgTypes`,
239
+ * `primaryKey`, `uniqueColumns`, `indexes`, `checks`, `isView` (all already
240
+ * keyed by snake_case column names);
241
+ * - `relations` (relation PROPERTY names are synthetic introspection names
242
+ * with no DB column equivalent, and `foreignKey`/`referenceKey`/`through`
243
+ * already hold DB column names);
244
+ * - entity type names, table accessors, and `enums`.
245
+ *
246
+ * Every non-PII field of each {@link ColumnMetadata} (`pii`, `pgType`,
247
+ * `dialectType`, `nullable`, …) is preserved. Exported from the package root so
248
+ * runtime-introspection and serverless users can apply the same identity mapping
249
+ * to a schema they build at runtime, e.g. `turbineHttp(pool,
250
+ * withDbFieldNames(schema))`.
251
+ */
252
+ export declare function withDbFieldNames(schema: SchemaMetadata): SchemaMetadata;
253
+ /**
254
+ * A typed name map from a Prisma schema onto a Turbine client, produced by
255
+ * `turbine migrate-from-prisma` (`generatePrismaMap` writes it as a
256
+ * `prisma-map.ts` module next to the generated client). It is library-side so
257
+ * the phase-2 `turbine-orm/prisma-compat` runtime adapter can consume the same
258
+ * shape to translate Prisma model/field/relation/compound-unique names onto the
259
+ * Turbine surface without re-parsing anything.
260
+ *
261
+ * Every name it carries was RESOLVED against live introspected metadata: a model
262
+ * or field that could not be matched is omitted from the map and listed in the
263
+ * migration report instead, so the map only ever contains verified mappings.
264
+ */
265
+ export interface PrismaCompatMap {
266
+ /** Prisma model name → its resolved mapping. */
267
+ models: Record<string, PrismaModelMap>;
268
+ /** Prisma enum name → resolved database enum-type name. */
269
+ enums: Record<string, string>;
270
+ }
271
+ /** One Prisma model's resolved mapping onto a Turbine table + client accessor. */
272
+ export interface PrismaModelMap {
273
+ /** Resolved snake_case database table name. */
274
+ table: string;
275
+ /** camelCase `TurbineClient` accessor (`db.<accessor>`). */
276
+ accessor: string;
277
+ /** Prisma field name → Turbine field name (camelCase). Relation fields excluded. */
278
+ fields: Record<string, string>;
279
+ /** Prisma relation-field name → resolved Turbine relation + cardinality. */
280
+ relations: Record<string, PrismaRelationMap>;
281
+ /**
282
+ * Prisma compound-unique/compound-id selector name → the Turbine field names
283
+ * (in declared order). The selector name is Prisma's: the explicit
284
+ * `@@unique(name:)` / `@@id(name:)` argument, else the field names joined with
285
+ * `_`. Consumed by the phase-2 client to translate
286
+ * `where: { <selector>: { ... } }`, including custom `@@unique(name:)` names
287
+ * the core `findUnique`-family derivation cannot know.
288
+ */
289
+ compoundUniques: Record<string, string[]>;
290
+ }
291
+ /** A resolved Prisma relation field → Turbine relation. */
292
+ export interface PrismaRelationMap {
293
+ /** Turbine relation name (the `with` clause key). */
294
+ name: string;
295
+ /** `'one'` (to-one) or `'many'` (to-many, including m2m). */
296
+ cardinality: 'one' | 'many';
297
+ }
package/dist/schema.js CHANGED
@@ -146,3 +146,49 @@ export function singularize(s) {
146
146
  return s.slice(0, -1);
147
147
  return s;
148
148
  }
149
+ // ---------------------------------------------------------------------------
150
+ // keepColumnNames transform (F4)
151
+ // ---------------------------------------------------------------------------
152
+ /**
153
+ * Return a copy of `schema` in which every column's TypeScript **field** name is
154
+ * the raw database column name (snake_case) instead of the camelCased default,
155
+ * i.e. `user_id` stays `user_id` rather than becoming `userId`.
156
+ *
157
+ * This is a PURE, generate-time transform with ZERO runtime changes: it only
158
+ * rewrites `column.field` and rebuilds each table's `columnMap` /
159
+ * `reverseColumnMap` as IDENTITY maps (`user_id → user_id`). Because every
160
+ * runtime surface resolves column names through those maps (`toColumn`,
161
+ * `parseRow`, relation `json_build_object` keys, batched-loader stitching,
162
+ * positional decoding, aggregate/groupBy naming), the generated client returns
163
+ * rows keyed by DB column names and accepts DB column names in
164
+ * `where`/`orderBy`/`select` with no code path aware of the difference.
165
+ *
166
+ * Deliberately left untouched (the flag is literally about column names):
167
+ * - `name`, `allColumns`, `dateColumns`, `dialectTypes`, `pgTypes`,
168
+ * `primaryKey`, `uniqueColumns`, `indexes`, `checks`, `isView` (all already
169
+ * keyed by snake_case column names);
170
+ * - `relations` (relation PROPERTY names are synthetic introspection names
171
+ * with no DB column equivalent, and `foreignKey`/`referenceKey`/`through`
172
+ * already hold DB column names);
173
+ * - entity type names, table accessors, and `enums`.
174
+ *
175
+ * Every non-PII field of each {@link ColumnMetadata} (`pii`, `pgType`,
176
+ * `dialectType`, `nullable`, …) is preserved. Exported from the package root so
177
+ * runtime-introspection and serverless users can apply the same identity mapping
178
+ * to a schema they build at runtime, e.g. `turbineHttp(pool,
179
+ * withDbFieldNames(schema))`.
180
+ */
181
+ export function withDbFieldNames(schema) {
182
+ const tables = {};
183
+ for (const [tableKey, table] of Object.entries(schema.tables)) {
184
+ const columns = table.columns.map((col) => ({ ...col, field: col.name }));
185
+ const columnMap = {};
186
+ const reverseColumnMap = {};
187
+ for (const col of columns) {
188
+ columnMap[col.name] = col.name;
189
+ reverseColumnMap[col.name] = col.name;
190
+ }
191
+ tables[tableKey] = { ...table, columns, columnMap, reverseColumnMap };
192
+ }
193
+ return { ...schema, tables };
194
+ }
package/dist/sqlite.js CHANGED
@@ -49,7 +49,7 @@ import { createRequire } from 'node:module';
49
49
  import { TurbineClient } from './client.js';
50
50
  import { postgresDialect, } from './dialect.js';
51
51
  import { ConnectionError } from './errors.js';
52
- import { deriveEngineRelations } from './introspect.js';
52
+ import { applyTableFilters, deriveEngineRelations } from './introspect.js';
53
53
  import { isDateType, snakeToCamel, } from './schema.js';
54
54
  let cachedDatabaseSync;
55
55
  /**
@@ -550,15 +550,10 @@ function pragma(db, sql) {
550
550
  */
551
551
  export function introspectSqliteDatabase(db, options = {}) {
552
552
  // ----- Tables (skip SQLite internal + the migration tracking table) -----
553
- let tableNames = pragma(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").map((r) => r.name);
554
- if (options.include?.length) {
555
- const inc = new Set(options.include);
556
- tableNames = tableNames.filter((t) => inc.has(t));
557
- }
558
- if (options.exclude?.length) {
559
- const exc = new Set(options.exclude);
560
- tableNames = tableNames.filter((t) => !exc.has(t));
561
- }
553
+ const candidateTables = pragma(db, "SELECT name FROM sqlite_master WHERE type = 'table' AND name NOT LIKE 'sqlite_%' ORDER BY name").map((r) => r.name);
554
+ // include / exclude + default bookkeeping-table exclusions (F12), shared with
555
+ // every other introspector via applyTableFilters.
556
+ const tableNames = applyTableFilters(candidateTables, options);
562
557
  const tableSet = new Set(tableNames);
563
558
  const columnsByTable = new Map();
564
559
  const pkByTable = new Map();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "turbine-orm",
3
- "version": "0.40.1",
3
+ "version": "0.41.0",
4
4
  "description": "Postgres-native TypeScript ORM — runs on Neon, Vercel Postgres, Cloudflare, Supabase. Streaming cursors, typed errors, single-query nested relations. One dependency, no WASM engine",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,6 +14,11 @@
14
14
  "import": "./dist/serverless.js",
15
15
  "require": "./dist/cjs/serverless.js"
16
16
  },
17
+ "./prisma-compat": {
18
+ "types": "./dist/prisma-compat.d.ts",
19
+ "import": "./dist/prisma-compat.js",
20
+ "require": "./dist/cjs/prisma-compat.js"
21
+ },
17
22
  "./sqlite": {
18
23
  "types": "./dist/sqlite.d.ts",
19
24
  "import": "./dist/sqlite.js",