@prisma-next/family-sql 0.5.0-dev.3 → 0.5.0-dev.30

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 (36) hide show
  1. package/README.md +2 -2
  2. package/dist/control-adapter.d.mts +12 -1
  3. package/dist/control-adapter.d.mts.map +1 -1
  4. package/dist/control.d.mts +2 -2
  5. package/dist/control.mjs +1024 -11
  6. package/dist/control.mjs.map +1 -1
  7. package/dist/migration.d.mts +1 -1
  8. package/dist/schema-verify.d.mts +2 -2
  9. package/dist/{types-C6K4mxDM.d.mts → types-sZihdnGx.d.mts} +14 -5
  10. package/dist/types-sZihdnGx.d.mts.map +1 -0
  11. package/dist/verify-BdES8wgQ.mjs +82 -0
  12. package/dist/verify-BdES8wgQ.mjs.map +1 -0
  13. package/dist/verify-sql-schema-Ovz7RXR5.mjs.map +1 -1
  14. package/dist/{verify-sql-schema-BBhkqEDo.d.mts → verify-sql-schema-_EoNcGIq.d.mts} +2 -2
  15. package/dist/{verify-sql-schema-BBhkqEDo.d.mts.map → verify-sql-schema-_EoNcGIq.d.mts.map} +1 -1
  16. package/dist/verify.d.mts +16 -20
  17. package/dist/verify.d.mts.map +1 -1
  18. package/dist/verify.mjs +2 -2
  19. package/package.json +18 -18
  20. package/src/core/control-adapter.ts +12 -0
  21. package/src/core/control-instance.ts +43 -15
  22. package/src/core/migrations/types.ts +7 -0
  23. package/src/core/operation-preview.ts +62 -0
  24. package/src/core/psl-contract-infer/default-mapping.ts +56 -0
  25. package/src/core/psl-contract-infer/name-transforms.ts +178 -0
  26. package/src/core/psl-contract-infer/postgres-default-mapping.ts +16 -0
  27. package/src/core/psl-contract-infer/postgres-type-map.ts +165 -0
  28. package/src/core/psl-contract-infer/printer-config.ts +55 -0
  29. package/src/core/psl-contract-infer/raw-default-parser.ts +91 -0
  30. package/src/core/psl-contract-infer/relation-inference.ts +196 -0
  31. package/src/core/psl-contract-infer/sql-schema-ir-to-psl-ast.ts +832 -0
  32. package/src/core/verify.ts +46 -108
  33. package/src/exports/verify.ts +1 -1
  34. package/dist/types-C6K4mxDM.d.mts.map +0 -1
  35. package/dist/verify-4GshvY4p.mjs +0 -122
  36. package/dist/verify-4GshvY4p.mjs.map +0 -1
@@ -0,0 +1,165 @@
1
+ import type {
2
+ EnumInfo,
3
+ PslNativeTypeAttribute,
4
+ PslTypeMap,
5
+ PslTypeResolution,
6
+ } from './printer-config';
7
+
8
+ const POSTGRES_TO_PSL: Record<string, string> = {
9
+ text: 'String',
10
+ bool: 'Boolean',
11
+ boolean: 'Boolean',
12
+ int4: 'Int',
13
+ integer: 'Int',
14
+ int8: 'BigInt',
15
+ bigint: 'BigInt',
16
+ float8: 'Float',
17
+ 'double precision': 'Float',
18
+ numeric: 'Decimal',
19
+ decimal: 'Decimal',
20
+ timestamptz: 'DateTime',
21
+ 'timestamp with time zone': 'DateTime',
22
+ jsonb: 'Json',
23
+ bytea: 'Bytes',
24
+ };
25
+
26
+ const PRESERVED_NATIVE_TYPES: Record<
27
+ string,
28
+ { readonly pslType: string; readonly attributeName: string }
29
+ > = {
30
+ 'character varying': { pslType: 'String', attributeName: 'db.VarChar' },
31
+ character: { pslType: 'String', attributeName: 'db.Char' },
32
+ char: { pslType: 'String', attributeName: 'db.Char' },
33
+ varchar: { pslType: 'String', attributeName: 'db.VarChar' },
34
+ uuid: { pslType: 'String', attributeName: 'db.Uuid' },
35
+ int2: { pslType: 'Int', attributeName: 'db.SmallInt' },
36
+ smallint: { pslType: 'Int', attributeName: 'db.SmallInt' },
37
+ float4: { pslType: 'Float', attributeName: 'db.Real' },
38
+ real: { pslType: 'Float', attributeName: 'db.Real' },
39
+ timestamp: { pslType: 'DateTime', attributeName: 'db.Timestamp' },
40
+ 'timestamp without time zone': { pslType: 'DateTime', attributeName: 'db.Timestamp' },
41
+ date: { pslType: 'DateTime', attributeName: 'db.Date' },
42
+ time: { pslType: 'DateTime', attributeName: 'db.Time' },
43
+ 'time without time zone': { pslType: 'DateTime', attributeName: 'db.Time' },
44
+ timetz: { pslType: 'DateTime', attributeName: 'db.Timetz' },
45
+ 'time with time zone': { pslType: 'DateTime', attributeName: 'db.Timetz' },
46
+ json: { pslType: 'Json', attributeName: 'db.Json' },
47
+ };
48
+
49
+ const PARAMETERIZED_NATIVE_TYPES: Record<
50
+ string,
51
+ { readonly pslType: string; readonly attributeName: string }
52
+ > = {
53
+ 'character varying': { pslType: 'String', attributeName: 'db.VarChar' },
54
+ character: { pslType: 'String', attributeName: 'db.Char' },
55
+ char: { pslType: 'String', attributeName: 'db.Char' },
56
+ varchar: { pslType: 'String', attributeName: 'db.VarChar' },
57
+ numeric: { pslType: 'Decimal', attributeName: 'db.Numeric' },
58
+ timestamp: { pslType: 'DateTime', attributeName: 'db.Timestamp' },
59
+ timestamptz: { pslType: 'DateTime', attributeName: 'db.Timestamptz' },
60
+ time: { pslType: 'DateTime', attributeName: 'db.Time' },
61
+ timetz: { pslType: 'DateTime', attributeName: 'db.Timetz' },
62
+ };
63
+
64
+ const PARAMETERIZED_TYPE_PATTERN = /^(.+?)\((.+)\)$/;
65
+
66
+ const ENUM_CODEC_ID = 'pg/enum@1';
67
+
68
+ function getOwnMappingValue(map: Record<string, string>, key: string): string | undefined {
69
+ return Object.hasOwn(map, key) ? map[key] : undefined;
70
+ }
71
+
72
+ function getOwnRecordValue<T>(map: Record<string, T>, key: string): T | undefined {
73
+ return Object.hasOwn(map, key) ? map[key] : undefined;
74
+ }
75
+
76
+ function createNativeTypeAttribute(name: string, args?: readonly string[]): PslNativeTypeAttribute {
77
+ return args && args.length > 0 ? { name, args } : { name };
78
+ }
79
+
80
+ function splitTypeParameterList(params: string): readonly string[] {
81
+ return params
82
+ .split(',')
83
+ .map((part) => part.trim())
84
+ .filter((part) => part.length > 0);
85
+ }
86
+
87
+ export function createPostgresTypeMap(enumTypeNames?: ReadonlySet<string>): PslTypeMap {
88
+ return {
89
+ resolve(nativeType: string): PslTypeResolution {
90
+ if (enumTypeNames?.has(nativeType)) {
91
+ return { pslType: nativeType, nativeType };
92
+ }
93
+
94
+ const paramMatch = nativeType.match(PARAMETERIZED_TYPE_PATTERN);
95
+ if (paramMatch) {
96
+ const [, baseType = nativeType, params = ''] = paramMatch;
97
+ const template = getOwnRecordValue(PARAMETERIZED_NATIVE_TYPES, baseType);
98
+ if (template) {
99
+ return {
100
+ pslType: template.pslType,
101
+ nativeType,
102
+ typeParams: { baseType, params },
103
+ nativeTypeAttribute: createNativeTypeAttribute(
104
+ template.attributeName,
105
+ splitTypeParameterList(params),
106
+ ),
107
+ };
108
+ }
109
+ }
110
+
111
+ const preservedType = getOwnRecordValue(PRESERVED_NATIVE_TYPES, nativeType);
112
+ if (preservedType) {
113
+ return {
114
+ pslType: preservedType.pslType,
115
+ nativeType,
116
+ nativeTypeAttribute: createNativeTypeAttribute(preservedType.attributeName),
117
+ };
118
+ }
119
+
120
+ const pslType = getOwnMappingValue(POSTGRES_TO_PSL, nativeType);
121
+ if (pslType) {
122
+ return {
123
+ pslType,
124
+ nativeType,
125
+ };
126
+ }
127
+
128
+ return { unsupported: true, nativeType };
129
+ },
130
+ };
131
+ }
132
+
133
+ export function extractEnumInfo(annotations?: Record<string, unknown>): EnumInfo {
134
+ const pgAnnotations = annotations?.['pg'] as Record<string, unknown> | undefined;
135
+ const storageTypes = pgAnnotations?.['storageTypes'] as
136
+ | Record<string, { codecId: string; nativeType: string; typeParams?: Record<string, unknown> }>
137
+ | undefined;
138
+
139
+ const typeNames = new Set<string>();
140
+ const definitions = new Map<string, readonly string[]>();
141
+
142
+ if (storageTypes) {
143
+ for (const [key, typeInstance] of Object.entries(storageTypes)) {
144
+ if (typeInstance.codecId === ENUM_CODEC_ID) {
145
+ typeNames.add(key);
146
+ const values = typeInstance.typeParams?.['values'];
147
+ if (Array.isArray(values)) {
148
+ definitions.set(key, values as string[]);
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ return { typeNames, definitions };
155
+ }
156
+
157
+ export function extractEnumTypeNames(annotations?: Record<string, unknown>): ReadonlySet<string> {
158
+ return extractEnumInfo(annotations).typeNames;
159
+ }
160
+
161
+ export function extractEnumDefinitions(
162
+ annotations?: Record<string, unknown>,
163
+ ): ReadonlyMap<string, readonly string[]> {
164
+ return extractEnumInfo(annotations).definitions;
165
+ }
@@ -0,0 +1,55 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+ import type { DefaultMappingOptions } from './default-mapping';
3
+
4
+ /**
5
+ * Internal printer-shaped configuration, used by the SQL family's
6
+ * `sqlSchemaIrToPslAst` helper (M2). The framework-level psl-printer no longer
7
+ * exposes these — they're consumed only inside the SQL family.
8
+ */
9
+
10
+ export type PslNativeTypeAttribute = {
11
+ readonly name: string;
12
+ readonly args?: readonly string[];
13
+ };
14
+
15
+ export type PslTypeResolution =
16
+ | {
17
+ readonly pslType: string;
18
+ readonly nativeType: string;
19
+ readonly typeParams?: Record<string, unknown>;
20
+ readonly nativeTypeAttribute?: PslNativeTypeAttribute;
21
+ }
22
+ | {
23
+ readonly unsupported: true;
24
+ readonly nativeType: string;
25
+ };
26
+
27
+ export interface PslTypeMap {
28
+ resolve(nativeType: string, annotations?: Record<string, unknown>): PslTypeResolution;
29
+ }
30
+
31
+ export interface EnumInfo {
32
+ readonly typeNames: ReadonlySet<string>;
33
+ readonly definitions: ReadonlyMap<string, readonly string[]>;
34
+ }
35
+
36
+ export interface PslPrinterOptions {
37
+ readonly typeMap: PslTypeMap;
38
+ readonly defaultMapping?: DefaultMappingOptions;
39
+ readonly enumInfo?: EnumInfo;
40
+ readonly parseRawDefault?: (rawDefault: string, nativeType?: string) => ColumnDefault | undefined;
41
+ }
42
+
43
+ export type RelationField = {
44
+ readonly fieldName: string;
45
+ readonly typeName: string;
46
+ readonly referencedTableName?: string | undefined;
47
+ readonly optional: boolean;
48
+ readonly list: boolean;
49
+ readonly relationName?: string | undefined;
50
+ readonly fkName?: string | undefined;
51
+ readonly fields?: readonly string[] | undefined;
52
+ readonly references?: readonly string[] | undefined;
53
+ readonly onDelete?: string | undefined;
54
+ readonly onUpdate?: string | undefined;
55
+ };
@@ -0,0 +1,91 @@
1
+ import type { ColumnDefault } from '@prisma-next/contract/types';
2
+
3
+ const NEXTVAL_PATTERN = /^nextval\s*\(/i;
4
+ const NOW_FUNCTION_PATTERN = /^(now\s*\(\s*\)|CURRENT_TIMESTAMP)$/i;
5
+ const CLOCK_TIMESTAMP_PATTERN = /^clock_timestamp\s*\(\s*\)$/i;
6
+ const TIMESTAMP_CAST_SUFFIX = /::timestamp(?:tz|\s+(?:with|without)\s+time\s+zone)?$/i;
7
+ const TEXT_CAST_SUFFIX = /::text$/i;
8
+ const NOW_LITERAL_PATTERN = /^'now'$/i;
9
+ const UUID_PATTERN = /^gen_random_uuid\s*\(\s*\)$/i;
10
+ const UUID_OSSP_PATTERN = /^uuid_generate_v4\s*\(\s*\)$/i;
11
+ const NULL_PATTERN = /^NULL(?:::.+)?$/i;
12
+ const TRUE_PATTERN = /^true$/i;
13
+ const FALSE_PATTERN = /^false$/i;
14
+ const NUMERIC_PATTERN = /^-?\d+(\.\d+)?$/;
15
+ const JSON_CAST_SUFFIX = /::jsonb?$/i;
16
+ const STRING_LITERAL_PATTERN = /^'((?:[^']|'')*)'(?:::(?:"[^"]+"|[\w\s]+)(?:\(\d+\))?)?$/;
17
+
18
+ function canonicalizeTimestampDefault(expr: string): string | undefined {
19
+ if (NOW_FUNCTION_PATTERN.test(expr)) return 'now()';
20
+ if (CLOCK_TIMESTAMP_PATTERN.test(expr)) return 'clock_timestamp()';
21
+
22
+ if (!TIMESTAMP_CAST_SUFFIX.test(expr)) return undefined;
23
+
24
+ let inner = expr.replace(TIMESTAMP_CAST_SUFFIX, '').trim();
25
+ if (inner.startsWith('(') && inner.endsWith(')')) {
26
+ inner = inner.slice(1, -1).trim();
27
+ }
28
+
29
+ if (NOW_FUNCTION_PATTERN.test(inner)) return 'now()';
30
+ if (CLOCK_TIMESTAMP_PATTERN.test(inner)) return 'clock_timestamp()';
31
+
32
+ inner = inner.replace(TEXT_CAST_SUFFIX, '').trim();
33
+ if (NOW_LITERAL_PATTERN.test(inner)) return 'now()';
34
+
35
+ return undefined;
36
+ }
37
+
38
+ export function parseRawDefault(
39
+ rawDefault: string,
40
+ nativeType?: string,
41
+ ): ColumnDefault | undefined {
42
+ const trimmed = rawDefault.trim();
43
+ const normalizedType = nativeType?.toLowerCase();
44
+
45
+ if (NEXTVAL_PATTERN.test(trimmed)) {
46
+ return { kind: 'function', expression: 'autoincrement()' };
47
+ }
48
+
49
+ const canonicalTimestamp = canonicalizeTimestampDefault(trimmed);
50
+ if (canonicalTimestamp) {
51
+ return { kind: 'function', expression: canonicalTimestamp };
52
+ }
53
+
54
+ if (UUID_PATTERN.test(trimmed) || UUID_OSSP_PATTERN.test(trimmed)) {
55
+ return { kind: 'function', expression: 'gen_random_uuid()' };
56
+ }
57
+
58
+ if (NULL_PATTERN.test(trimmed)) {
59
+ return { kind: 'literal', value: null };
60
+ }
61
+
62
+ if (TRUE_PATTERN.test(trimmed)) {
63
+ return { kind: 'literal', value: true };
64
+ }
65
+
66
+ if (FALSE_PATTERN.test(trimmed)) {
67
+ return { kind: 'literal', value: false };
68
+ }
69
+
70
+ if (NUMERIC_PATTERN.test(trimmed)) {
71
+ return { kind: 'literal', value: Number(trimmed) };
72
+ }
73
+
74
+ const stringMatch = trimmed.match(STRING_LITERAL_PATTERN);
75
+ if (stringMatch?.[1] !== undefined) {
76
+ const unescaped = stringMatch[1].replace(/''/g, "'");
77
+ if (normalizedType === 'json' || normalizedType === 'jsonb') {
78
+ if (JSON_CAST_SUFFIX.test(trimmed)) {
79
+ return { kind: 'function', expression: trimmed };
80
+ }
81
+ try {
82
+ return { kind: 'literal', value: JSON.parse(unescaped) };
83
+ } catch {
84
+ // Fall through to the string form for malformed/non-JSON values.
85
+ }
86
+ }
87
+ return { kind: 'literal', value: unescaped };
88
+ }
89
+
90
+ return { kind: 'function', expression: trimmed };
91
+ }
@@ -0,0 +1,196 @@
1
+ import type { SqlForeignKeyIR, SqlTableIR } from '@prisma-next/sql-schema-ir/types';
2
+ import { deriveBackRelationFieldName, deriveRelationFieldName, pluralize } from './name-transforms';
3
+ import type { RelationField } from './printer-config';
4
+
5
+ const DEFAULT_ON_DELETE = 'noAction';
6
+ const DEFAULT_ON_UPDATE = 'noAction';
7
+
8
+ const REFERENTIAL_ACTION_PSL: Record<string, string> = {
9
+ noAction: 'NoAction',
10
+ restrict: 'Restrict',
11
+ cascade: 'Cascade',
12
+ setNull: 'SetNull',
13
+ setDefault: 'SetDefault',
14
+ };
15
+
16
+ export type InferredRelations = {
17
+ readonly relationsByTable: ReadonlyMap<string, readonly RelationField[]>;
18
+ };
19
+
20
+ export function inferRelations(
21
+ tables: Record<string, SqlTableIR>,
22
+ modelNameMap: ReadonlyMap<string, string>,
23
+ ): InferredRelations {
24
+ const relationsByTable = new Map<string, RelationField[]>();
25
+
26
+ const fkCountByPair = new Map<string, number>();
27
+ for (const table of Object.values(tables)) {
28
+ for (const fk of table.foreignKeys) {
29
+ const pairKey = `${table.name}→${fk.referencedTable}`;
30
+ fkCountByPair.set(pairKey, (fkCountByPair.get(pairKey) ?? 0) + 1);
31
+ }
32
+ }
33
+
34
+ const usedFieldNames = new Map<string, Set<string>>();
35
+ for (const table of Object.values(tables)) {
36
+ const names = new Set<string>();
37
+ for (const col of Object.values(table.columns)) {
38
+ names.add(col.name);
39
+ }
40
+ usedFieldNames.set(table.name, names);
41
+ }
42
+
43
+ for (const table of Object.values(tables)) {
44
+ for (const fk of table.foreignKeys) {
45
+ const childTableName = table.name;
46
+ const parentTableName = fk.referencedTable;
47
+ const childUsed = usedFieldNames.get(childTableName) as Set<string>;
48
+ const childModelName = modelNameMap.get(childTableName) ?? childTableName;
49
+ const parentModelName = modelNameMap.get(parentTableName) ?? parentTableName;
50
+ const pairKey = `${childTableName}→${parentTableName}`;
51
+ const isSelfRelation = childTableName === parentTableName;
52
+ const needsRelationName = (fkCountByPair.get(pairKey) as number) > 1 || isSelfRelation;
53
+
54
+ const isOneToOne = detectOneToOne(fk, table);
55
+
56
+ const childRelFieldName = resolveUniqueFieldName(
57
+ deriveRelationFieldName(fk.columns, parentTableName),
58
+ childUsed,
59
+ parentModelName,
60
+ );
61
+ const relationName = needsRelationName
62
+ ? deriveRelationName(fk, childRelFieldName, parentModelName, isSelfRelation)
63
+ : undefined;
64
+ const childOptional = fk.columns.some(
65
+ (columnName) => table.columns[columnName]?.nullable ?? false,
66
+ );
67
+
68
+ const childRelField = buildChildRelationField(
69
+ childRelFieldName,
70
+ parentModelName,
71
+ fk,
72
+ childOptional,
73
+ relationName,
74
+ );
75
+
76
+ addRelationField(relationsByTable, childTableName, childRelField);
77
+ childUsed.add(childRelFieldName);
78
+
79
+ const parentUsed = usedFieldNames.get(parentTableName) ?? new Set();
80
+ usedFieldNames.set(parentTableName, parentUsed);
81
+
82
+ const backRelFieldName = resolveUniqueFieldName(
83
+ deriveBackRelationFieldName(childModelName, isOneToOne),
84
+ parentUsed,
85
+ childModelName,
86
+ );
87
+
88
+ const backRelField: RelationField = {
89
+ fieldName: backRelFieldName,
90
+ typeName: childModelName,
91
+ optional: isOneToOne,
92
+ list: !isOneToOne,
93
+ relationName,
94
+ };
95
+
96
+ addRelationField(relationsByTable, parentTableName, backRelField);
97
+ parentUsed.add(backRelFieldName);
98
+ }
99
+ }
100
+
101
+ return { relationsByTable };
102
+ }
103
+
104
+ function detectOneToOne(fk: SqlForeignKeyIR, table: SqlTableIR): boolean {
105
+ const fkCols = [...fk.columns].sort();
106
+
107
+ if (table.primaryKey) {
108
+ const pkCols = [...table.primaryKey.columns].sort();
109
+ if (pkCols.length === fkCols.length && pkCols.every((c, i) => c === fkCols[i])) {
110
+ return true;
111
+ }
112
+ }
113
+
114
+ for (const unique of table.uniques) {
115
+ const uniqueCols = [...unique.columns].sort();
116
+ if (uniqueCols.length === fkCols.length && uniqueCols.every((c, i) => c === fkCols[i])) {
117
+ return true;
118
+ }
119
+ }
120
+
121
+ return false;
122
+ }
123
+
124
+ function deriveRelationName(
125
+ fk: SqlForeignKeyIR,
126
+ childRelationFieldName: string,
127
+ parentModelName: string,
128
+ isSelfRelation: boolean,
129
+ ): string {
130
+ if (fk.name) {
131
+ return fk.name;
132
+ }
133
+ if (isSelfRelation) {
134
+ return `${childRelationFieldName.charAt(0).toUpperCase() + childRelationFieldName.slice(1)}${pluralize(parentModelName)}`;
135
+ }
136
+ return fk.columns.join('_');
137
+ }
138
+
139
+ function buildChildRelationField(
140
+ fieldName: string,
141
+ parentModelName: string,
142
+ fk: SqlForeignKeyIR,
143
+ optional: boolean,
144
+ relationName?: string,
145
+ ): RelationField {
146
+ const onDelete = fk.onDelete && fk.onDelete !== DEFAULT_ON_DELETE ? fk.onDelete : undefined;
147
+ const onUpdate = fk.onUpdate && fk.onUpdate !== DEFAULT_ON_UPDATE ? fk.onUpdate : undefined;
148
+
149
+ return {
150
+ fieldName,
151
+ typeName: parentModelName,
152
+ referencedTableName: fk.referencedTable,
153
+ optional,
154
+ list: false,
155
+ relationName,
156
+ fkName: fk.name,
157
+ fields: fk.columns,
158
+ references: fk.referencedColumns,
159
+ onDelete: onDelete ? REFERENTIAL_ACTION_PSL[onDelete] : undefined,
160
+ onUpdate: onUpdate ? REFERENTIAL_ACTION_PSL[onUpdate] : undefined,
161
+ };
162
+ }
163
+
164
+ function resolveUniqueFieldName(
165
+ desired: string,
166
+ usedNames: ReadonlySet<string>,
167
+ fallbackSuffix: string,
168
+ ): string {
169
+ if (!usedNames.has(desired)) {
170
+ return desired;
171
+ }
172
+
173
+ const withSuffix = `${desired}${fallbackSuffix}`;
174
+ if (!usedNames.has(withSuffix)) {
175
+ return withSuffix;
176
+ }
177
+
178
+ let counter = 2;
179
+ while (usedNames.has(`${desired}${counter}`)) {
180
+ counter++;
181
+ }
182
+ return `${desired}${counter}`;
183
+ }
184
+
185
+ function addRelationField(
186
+ map: Map<string, RelationField[]>,
187
+ tableName: string,
188
+ field: RelationField,
189
+ ): void {
190
+ const existing = map.get(tableName);
191
+ if (existing) {
192
+ existing.push(field);
193
+ } else {
194
+ map.set(tableName, [field]);
195
+ }
196
+ }