@an5/orm 1.0.8 → 1.1.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 (73) hide show
  1. package/README.md +174 -175
  2. package/dist/an5Metadata.js +64 -0
  3. package/dist/an5Orm.d.ts +92 -3
  4. package/dist/an5Orm.js +697 -206
  5. package/dist/config.d.ts +9 -1
  6. package/dist/config.js +13 -18
  7. package/dist/errors.d.ts +1 -1
  8. package/dist/errors.js +1 -1
  9. package/dist/generator/src/api.d.ts +35 -0
  10. package/dist/generator/src/api.js +58 -0
  11. package/dist/generator/src/code-generator.d.ts +14 -13
  12. package/dist/generator/src/code-generator.js +249 -225
  13. package/dist/generator/src/config.d.ts +162 -0
  14. package/dist/generator/src/config.js +354 -0
  15. package/dist/generator/src/dialect.d.ts +88 -0
  16. package/dist/generator/src/dialect.js +318 -0
  17. package/dist/generator/src/dotnet-generator.d.ts +21 -11
  18. package/dist/generator/src/dotnet-generator.js +1053 -546
  19. package/dist/generator/src/field-types.d.ts +96 -0
  20. package/dist/generator/src/field-types.js +433 -0
  21. package/dist/generator/src/golang-generator.d.ts +36 -0
  22. package/dist/generator/src/golang-generator.js +1229 -0
  23. package/dist/generator/src/index.d.ts +1 -1
  24. package/dist/generator/src/index.js +137 -88
  25. package/dist/generator/src/metadata-generator.d.ts +11 -8
  26. package/dist/generator/src/metadata-generator.js +105 -90
  27. package/dist/generator/src/parser.d.ts +37 -11
  28. package/dist/generator/src/parser.js +208 -206
  29. package/dist/generator/src/push-apply.d.ts +37 -0
  30. package/dist/generator/src/push-apply.js +109 -0
  31. package/dist/generator/src/push-schema.d.ts +93 -0
  32. package/dist/generator/src/push-schema.js +189 -0
  33. package/dist/generator/src/python-generator.d.ts +37 -9
  34. package/dist/generator/src/python-generator.js +428 -105
  35. package/dist/generator/src/rust-generator.d.ts +38 -0
  36. package/dist/generator/src/rust-generator.js +971 -0
  37. package/dist/generator/src/suggest.d.ts +17 -0
  38. package/dist/generator/src/suggest.js +45 -0
  39. package/dist/generator/src/type-kinds.d.ts +27 -0
  40. package/dist/generator/src/type-kinds.js +72 -0
  41. package/dist/generator/src/types.d.ts +60 -27
  42. package/dist/generator/src/types.js +41 -2
  43. package/dist/index.d.ts +3 -2
  44. package/dist/index.js +19 -8
  45. package/dist/logger.js +1 -1
  46. package/dist/migration-core.d.ts +102 -0
  47. package/dist/migration-core.js +731 -0
  48. package/dist/orm.d.ts +147 -0
  49. package/dist/orm.js +1654 -0
  50. package/dist/provider-support.d.ts +27 -0
  51. package/dist/provider-support.js +50 -0
  52. package/dist/scripts/release-cli.d.ts +2 -2
  53. package/dist/scripts/release-cli.js +115 -115
  54. package/dist/sql-utils.d.ts +3 -1
  55. package/dist/sql-utils.js +94 -24
  56. package/generator/src/api.ts +45 -0
  57. package/generator/src/code-generator.ts +259 -230
  58. package/generator/src/config.ts +496 -0
  59. package/generator/src/dialect.ts +415 -0
  60. package/generator/src/dotnet-generator.ts +1071 -544
  61. package/generator/src/field-types.ts +473 -0
  62. package/generator/src/golang-generator.ts +1267 -0
  63. package/generator/src/index.ts +146 -116
  64. package/generator/src/metadata-generator.ts +116 -101
  65. package/generator/src/parser.ts +234 -202
  66. package/generator/src/push-apply.ts +148 -0
  67. package/generator/src/push-schema.ts +250 -0
  68. package/generator/src/python-generator.ts +480 -114
  69. package/generator/src/rust-generator.ts +974 -0
  70. package/generator/src/suggest.ts +41 -0
  71. package/generator/src/type-kinds.ts +78 -0
  72. package/generator/src/types.ts +77 -29
  73. package/package.json +32 -6
@@ -1,206 +1,208 @@
1
- "use strict";
2
- var __importDefault = (this && this.__importDefault) || function (mod) {
3
- return (mod && mod.__esModule) ? mod : { "default": mod };
4
- };
5
- Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.SchemaParser = void 0;
7
- exports.sqlTypeToTs = sqlTypeToTs;
8
- const fs_1 = __importDefault(require("fs"));
9
- const path_1 = __importDefault(require("path"));
10
- // SQL Server type → TypeScript type mapping
11
- const AN5_TO_TS = {
12
- // String types
13
- 'NVARCHAR': 'string',
14
- 'VARCHAR': 'string',
15
- 'CHAR': 'string',
16
- 'NCHAR': 'string',
17
- 'TEXT': 'string',
18
- 'NTEXT': 'string',
19
- 'XML': 'string',
20
- // Numeric types
21
- 'INT': 'number',
22
- 'SMALLINT': 'number',
23
- 'TINYINT': 'number',
24
- 'BIGINT': 'number | bigint',
25
- 'FLOAT': 'number',
26
- 'REAL': 'number',
27
- 'DECIMAL': 'number',
28
- 'NUMERIC': 'number',
29
- 'MONEY': 'number',
30
- 'SMALLMONEY': 'number',
31
- // Boolean
32
- 'BIT': 'boolean',
33
- // Date types
34
- 'DATETIME': 'Date',
35
- 'DATETIME2': 'Date',
36
- 'SMALLDATETIME': 'Date',
37
- 'DATE': 'Date',
38
- 'TIME': 'Date',
39
- 'DATETIMEOFFSET': 'Date',
40
- // Binary types
41
- 'VARBINARY': 'Buffer',
42
- 'BINARY': 'Buffer',
43
- 'IMAGE': 'Buffer',
44
- // Other
45
- 'UNIQUEIDENTIFIER': 'string',
46
- 'SQL_VARIANT': 'any',
47
- 'ROWVERSION': 'Buffer',
48
- 'HIERARCHYID': 'string',
49
- 'GEOGRAPHY': 'string',
50
- 'GEOMETRY': 'string',
51
- 'VECTOR': 'number[] | string',
52
- };
53
- // Parse base type from "NVARCHAR(255)" → "NVARCHAR"
54
- function parseSqlType(raw) {
55
- const match = raw.match(/^(\w+)(?:\((.+)\))?$/);
56
- if (!match)
57
- return { base: raw, params: '' };
58
- return { base: match[1].toUpperCase(), params: match[2] || '' };
59
- }
60
- // Map SQL Server type to TypeScript type
61
- function sqlTypeToTs(sqlType) {
62
- const { base } = parseSqlType(sqlType);
63
- return AN5_TO_TS[base] || 'any';
64
- }
65
- class SchemaParser {
66
- constructor(schemaDir) {
67
- this.schemaDir = schemaDir;
68
- this.schemaText = '';
69
- }
70
- async parse() {
71
- this.loadSchema();
72
- const lines = this.schemaText.split('\n');
73
- const models = [];
74
- let currentModel = null;
75
- for (let line of lines) {
76
- line = line.trim();
77
- if (!line || line.startsWith('//'))
78
- continue;
79
- const modelHeaderMatch = line.match(/^model\s+(\w+)\s*\{/);
80
- if (modelHeaderMatch) {
81
- const modelName = modelHeaderMatch[1];
82
- currentModel = {
83
- name: modelName,
84
- tableName: modelName.toLowerCase() + 's',
85
- schemaName: 'dbo',
86
- fields: [],
87
- relations: []
88
- };
89
- models.push(currentModel);
90
- continue;
91
- }
92
- if (line === '}') {
93
- currentModel = null;
94
- continue;
95
- }
96
- if (currentModel) {
97
- this.parseModelLine(line, currentModel);
98
- }
99
- }
100
- this.postProcessRelations(models);
101
- return models;
102
- }
103
- loadSchema() {
104
- if (fs_1.default.existsSync(this.schemaDir)) {
105
- const files = fs_1.default.readdirSync(this.schemaDir).filter(f => f.endsWith('.an5'));
106
- for (const file of files) {
107
- this.schemaText += fs_1.default.readFileSync(path_1.default.join(this.schemaDir, file), 'utf8') + '\n';
108
- }
109
- }
110
- else {
111
- throw new Error(`No schema directory found at ${this.schemaDir}`);
112
- }
113
- }
114
- parseModelLine(line, model) {
115
- if (line.startsWith('@@map')) {
116
- const mapMatch = line.match(/@@map\("(.+)"\)/);
117
- if (mapMatch)
118
- model.tableName = mapMatch[1];
119
- return;
120
- }
121
- if (line.startsWith('@@schema')) {
122
- const schemaMatch = line.match(/@@schema\("(.+)"\)/);
123
- if (schemaMatch)
124
- model.schemaName = schemaMatch[1];
125
- return;
126
- }
127
- if (line.startsWith('@@unique')) {
128
- const uniqueMatch = line.match(/@@unique\(\[([\w,\s]+)\]\)/);
129
- if (uniqueMatch) {
130
- const fields = uniqueMatch[1].split(',').map(f => f.trim());
131
- model.compoundUniques = model.compoundUniques || [];
132
- model.compoundUniques.push(fields);
133
- }
134
- return;
135
- }
136
- if (line.startsWith('@@description')) {
137
- const descMatch = line.match(/@@description\("(.+)"\)/);
138
- if (descMatch)
139
- model.description = descMatch[1];
140
- return;
141
- }
142
- if (line.startsWith('@@'))
143
- return;
144
- const parts = line.split(/\s+/);
145
- const fieldName = parts[0];
146
- let fieldType = parts[1];
147
- if (!fieldName || !fieldType)
148
- return;
149
- const isArray = fieldType.endsWith('[]');
150
- const isOptional = fieldType.endsWith('?');
151
- const cleanType = fieldType.replace('[]', '').replace('?', '');
152
- // Parse SQL Server type (e.g., "NVARCHAR(255)" → base="NVARCHAR")
153
- const { base: sqlBase } = parseSqlType(cleanType);
154
- let tsType = 'any';
155
- let isRelation = false;
156
- // Check if it's a known SQL Server type
157
- if (AN5_TO_TS[sqlBase]) {
158
- tsType = sqlTypeToTs(cleanType);
159
- }
160
- else if (cleanType[0] === cleanType[0].toUpperCase() && !cleanType.includes('(')) {
161
- // Uppercase without parens = likely a relation to another model
162
- tsType = cleanType;
163
- isRelation = true;
164
- }
165
- if (isRelation) {
166
- let foreignKey = '', localKey = '', relationName = '';
167
- const nameMatch = line.match(/@relation\("(\w+)"/);
168
- if (nameMatch)
169
- relationName = nameMatch[1];
170
- const relationMatch = line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
171
- if (relationMatch) {
172
- foreignKey = relationMatch[1] || '';
173
- localKey = relationMatch[2] || '';
174
- }
175
- model.relations.push({ name: fieldName, type: tsType, isArray, isOptional, foreignKey, localKey, relationName });
176
- }
177
- else {
178
- const hasDefault = line.includes('@default') || line.includes('@updatedAt') || line.includes('@id');
179
- const isId = line.includes('@id');
180
- let description;
181
- const descMatch = line.match(/@description\("(.+)"\)/);
182
- if (descMatch)
183
- description = descMatch[1];
184
- model.fields.push({ name: fieldName, type: tsType, sqlType: cleanType, isOptional, hasDefault, isId, description });
185
- }
186
- }
187
- postProcessRelations(models) {
188
- for (const model of models) {
189
- for (const rel of model.relations) {
190
- if (!rel.foreignKey || !rel.localKey) {
191
- const targetModel = models.find(m => m.name === rel.type);
192
- if (targetModel) {
193
- let opposite = rel.relationName ?
194
- targetModel.relations.find(r => r.type === model.name && r.relationName === rel.relationName && r.foreignKey && r.localKey) :
195
- targetModel.relations.find(r => r.type === model.name && r.foreignKey && r.localKey);
196
- if (opposite) {
197
- rel.foreignKey = opposite.foreignKey;
198
- rel.localKey = opposite.localKey;
199
- }
200
- }
201
- }
202
- }
203
- }
204
- }
205
- }
206
- exports.SchemaParser = SchemaParser;
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.SchemaParser = void 0;
7
+ exports.sqlTypeToTs = sqlTypeToTs;
8
+ const fs_1 = __importDefault(require("fs"));
9
+ const path_1 = __importDefault(require("path"));
10
+ const field_types_1 = require("./field-types");
11
+ /**
12
+ * Maps a column type of a provider to its TypeScript type.
13
+ *
14
+ * `provider` defaults to SQL Server so existing callers keep their behaviour;
15
+ * the generator passes the provider derived from the connection string.
16
+ */
17
+ function sqlTypeToTs(sqlType, provider = field_types_1.DEFAULT_PROVIDER) {
18
+ return (0, field_types_1.resolveFieldType)(sqlType, provider)?.ts ?? 'any';
19
+ }
20
+ class SchemaParser {
21
+ /**
22
+ * @param schemaDir directory holding the `.an5` files
23
+ * @param provider target database; decides which field types are valid
24
+ */
25
+ constructor(schemaDir, provider = field_types_1.DEFAULT_PROVIDER) {
26
+ this.schemaDir = schemaDir;
27
+ this.provider = provider;
28
+ this.schemaText = '';
29
+ this.issues = [];
30
+ this.modelRefs = [];
31
+ this.models = [];
32
+ }
33
+ async parse() {
34
+ this.issues = [];
35
+ this.modelRefs = [];
36
+ this.models = [];
37
+ this.loadSchema();
38
+ const lines = this.schemaText.split('\n');
39
+ const models = [];
40
+ let currentModel = null;
41
+ for (let line of lines) {
42
+ line = line.trim();
43
+ if (!line || line.startsWith('//'))
44
+ continue;
45
+ const modelHeaderMatch = line.match(/^model\s+(\w+)\s*\{/);
46
+ if (modelHeaderMatch) {
47
+ const modelName = modelHeaderMatch[1];
48
+ currentModel = {
49
+ name: modelName,
50
+ tableName: modelName.toLowerCase() + 's',
51
+ // SQL Server's default schema, and only SQL Server's. For SQLite or MySQL
52
+ // an unqualified name is what the connection resolves, and for PostgreSQL
53
+ // it follows `search_path` — the same rule `db:push` now uses, so the
54
+ // generated client and the pushed table cannot drift apart.
55
+ schemaName: this.provider === 'mssql' ? 'dbo' : '',
56
+ provider: this.provider,
57
+ fields: [],
58
+ relations: []
59
+ };
60
+ models.push(currentModel);
61
+ continue;
62
+ }
63
+ if (line === '}') {
64
+ currentModel = null;
65
+ continue;
66
+ }
67
+ if (currentModel) {
68
+ this.parseModelLine(line, currentModel);
69
+ }
70
+ }
71
+ this.models = models;
72
+ this.resolveModelRefs();
73
+ // Every bad type is reported at once, so one pass over the schema is enough
74
+ // instead of re-running the generator after every line.
75
+ if (this.issues.length > 0) {
76
+ throw new field_types_1.FieldTypeError(this.provider, this.issues);
77
+ }
78
+ this.postProcessRelations(models);
79
+ return models;
80
+ }
81
+ loadSchema() {
82
+ if (fs_1.default.existsSync(this.schemaDir)) {
83
+ const files = fs_1.default.readdirSync(this.schemaDir).filter(f => f.endsWith('.an5'));
84
+ for (const file of files) {
85
+ this.schemaText += fs_1.default.readFileSync(path_1.default.join(this.schemaDir, file), 'utf8') + '\n';
86
+ }
87
+ }
88
+ else {
89
+ throw new Error(`No schema directory found at ${this.schemaDir}`);
90
+ }
91
+ }
92
+ parseModelLine(line, model) {
93
+ if (line.startsWith('@@map')) {
94
+ const mapMatch = line.match(/@@map\("(.+)"\)/);
95
+ if (mapMatch)
96
+ model.tableName = mapMatch[1];
97
+ return;
98
+ }
99
+ if (line.startsWith('@@schema')) {
100
+ // `(.*)` rather than `(.+)`: `@@schema("")` is how a model says "no
101
+ // schema", needed for databases that have no schema concept. With `(.+)`
102
+ // the empty string does not match, the directive is silently ignored and
103
+ // the model keeps `dbo`.
104
+ const schemaMatch = line.match(/@@schema\("(.*)"\)/);
105
+ if (schemaMatch)
106
+ model.schemaName = schemaMatch[1].trim();
107
+ return;
108
+ }
109
+ if (line.startsWith('@@unique')) {
110
+ const uniqueMatch = line.match(/@@unique\(\[([\w,\s]+)\]\)/);
111
+ if (uniqueMatch) {
112
+ const fields = uniqueMatch[1].split(',').map(f => f.trim());
113
+ model.compoundUniques = model.compoundUniques || [];
114
+ model.compoundUniques.push(fields);
115
+ }
116
+ return;
117
+ }
118
+ if (line.startsWith('@@description')) {
119
+ const descMatch = line.match(/@@description\("(.+)"\)/);
120
+ if (descMatch)
121
+ model.description = descMatch[1];
122
+ return;
123
+ }
124
+ if (line.startsWith('@@'))
125
+ return;
126
+ const head = (0, field_types_1.readFieldLineHead)(line);
127
+ if (!head)
128
+ return;
129
+ // No table entry for this provider. This used to fall through to
130
+ // `tsType = 'any'` (or, worse, become a relation to a model that does not
131
+ // exist) and the client was still generated; the failure only surfaced
132
+ // later at the database layer, in a message unrelated to the schema.
133
+ const resolved = (0, field_types_1.resolveFieldType)(head.type, this.provider);
134
+ if (!resolved) {
135
+ this.modelRefs.push({ model, name: head.name, type: head.type, isArray: head.isArray, isOptional: head.isOptional, line });
136
+ return;
137
+ }
138
+ const hasDefault = line.includes('@default') || line.includes('@updatedAt') || line.includes('@id');
139
+ const isId = line.includes('@id');
140
+ let description;
141
+ const descMatch = line.match(/@description\("(.+)"\)/);
142
+ if (descMatch)
143
+ description = descMatch[1];
144
+ model.fields.push({ name: head.name, type: resolved.ts, sqlType: head.type, isOptional: head.isOptional, hasDefault, isId, description });
145
+ }
146
+ /**
147
+ * Settles the fields that matched no type: a name matching a model in the
148
+ * schema is a relation, anything else is a bad type — reported with the
149
+ * provider name and a suggestion.
150
+ *
151
+ * The decision has to wait for the whole schema, because a relation may point
152
+ * at a model declared later. Every upper-case token used to count as a
153
+ * relation, so `INTEGER` written for SQL Server became a relation to a model
154
+ * named `INTEGER` and nothing ever complained.
155
+ */
156
+ resolveModelRefs() {
157
+ const modelNames = this.models.map((model) => model.name);
158
+ for (const ref of this.modelRefs) {
159
+ if (!modelNames.includes(ref.type)) {
160
+ this.issues.push({
161
+ path: `${ref.model.name}.${ref.name}`,
162
+ message: (0, field_types_1.unknownFieldTypeMessage)(ref.type, this.provider, modelNames),
163
+ });
164
+ continue;
165
+ }
166
+ let foreignKey = '', localKey = '', relationName = '';
167
+ const nameMatch = ref.line.match(/@relation\("(\w+)"/);
168
+ if (nameMatch)
169
+ relationName = nameMatch[1];
170
+ const relationMatch = ref.line.match(/@relation\((?:.*fields:\s*\[(\w+)\],)?\s*(?:.*references:\s*\[(\w+)\],?)?.*\)/);
171
+ if (relationMatch) {
172
+ foreignKey = relationMatch[1] || '';
173
+ localKey = relationMatch[2] || '';
174
+ }
175
+ const description = ref.line.match(/@description\("(.+)"\)/)?.[1];
176
+ ref.model.relations.push({
177
+ name: ref.name,
178
+ type: ref.type,
179
+ isArray: ref.isArray,
180
+ isOptional: ref.isOptional,
181
+ foreignKey,
182
+ localKey,
183
+ relationName,
184
+ ...(description ? { description } : {}),
185
+ });
186
+ }
187
+ this.modelRefs = [];
188
+ }
189
+ postProcessRelations(models) {
190
+ for (const model of models) {
191
+ for (const rel of model.relations) {
192
+ if (!rel.foreignKey || !rel.localKey) {
193
+ const targetModel = models.find(m => m.name === rel.type);
194
+ if (targetModel) {
195
+ let opposite = rel.relationName ?
196
+ targetModel.relations.find(r => r.type === model.name && r.relationName === rel.relationName && r.foreignKey && r.localKey) :
197
+ targetModel.relations.find(r => r.type === model.name && r.foreignKey && r.localKey);
198
+ if (opposite) {
199
+ rel.foreignKey = opposite.foreignKey;
200
+ rel.localKey = opposite.localKey;
201
+ }
202
+ }
203
+ }
204
+ }
205
+ }
206
+ }
207
+ }
208
+ exports.SchemaParser = SchemaParser;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Applying a schema to a database: what `db:push` does once the schema is read.
3
+ *
4
+ * Kept out of `push.ts` so the sequence can be run against a real database in a
5
+ * test — the CLI needs a live server, and a test that re-implements the sequence
6
+ * instead of calling it proves only that the copy works.
7
+ *
8
+ * The database is two functions rather than the adapter, which keeps this testable
9
+ * and keeps the driver out of the decision making.
10
+ */
11
+ import { PushDialect } from './dialect';
12
+ import { PushModel } from './push-schema';
13
+ /** The two things this needs from a database connection. */
14
+ export interface PushDatabase {
15
+ /** Runs a query and returns its rows; existence checks are `length > 0`. */
16
+ query(sql: string): Promise<readonly unknown[]>;
17
+ /** Runs one statement. */
18
+ execute(sql: string): Promise<void>;
19
+ /** Progress, so the CLI prints what it is doing. */
20
+ log(message: string): void;
21
+ }
22
+ export interface ApplyResult {
23
+ /** How many tables were created, and how many altered. */
24
+ tablesCreated: number;
25
+ tablesAltered: number;
26
+ columnsAdded: number;
27
+ indexesCreated: number;
28
+ uniqueConstraintsAdded: number;
29
+ }
30
+ /**
31
+ * Creates whatever the schema has and the database does not.
32
+ *
33
+ * The order is deliberate: a table first, then missing columns, then unique
34
+ * constraints and indexes — a unique over a column that does not exist yet fails.
35
+ * Everything is checked before it is created, so running it twice changes nothing.
36
+ */
37
+ export declare function applySchema(models: readonly PushModel[], dialect: PushDialect, db: PushDatabase): Promise<ApplyResult>;
@@ -0,0 +1,109 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applySchema = applySchema;
4
+ const push_schema_1 = require("./push-schema");
5
+ /**
6
+ * Creates whatever the schema has and the database does not.
7
+ *
8
+ * The order is deliberate: a table first, then missing columns, then unique
9
+ * constraints and indexes — a unique over a column that does not exist yet fails.
10
+ * Everything is checked before it is created, so running it twice changes nothing.
11
+ */
12
+ async function applySchema(models, dialect, db) {
13
+ const result = {
14
+ tablesCreated: 0,
15
+ tablesAltered: 0,
16
+ columnsAdded: 0,
17
+ indexesCreated: 0,
18
+ uniqueConstraintsAdded: 0,
19
+ };
20
+ for (const model of models) {
21
+ const tableName = (0, push_schema_1.qualifiedTableName)(model);
22
+ const sqlTableName = dialect.quoteTable(tableName);
23
+ const quoteFields = (fields) => fields.map((field) => dialect.quote(field));
24
+ db.log(`Processing table ${sqlTableName}...`);
25
+ const safeName = (0, push_schema_1.safeIdentifierName)(model.tableName);
26
+ // SQLite has no named unique constraints — a UNIQUE there becomes an index the
27
+ // engine names itself — so the unique index carries our name instead, and that
28
+ // name is what the existence check can look for.
29
+ const compoundUniques = model.compoundUniques.map((directive, idx) => ({
30
+ name: (0, push_schema_1.directiveName)(directive, model.tableName, 'compound', idx),
31
+ fields: directive.fields,
32
+ directive,
33
+ }));
34
+ const tableExists = (await db.query(dialect.tableExists(tableName))).length > 0;
35
+ if (!tableExists) {
36
+ db.log(`Creating table ${sqlTableName}...`);
37
+ const columnDefs = model.fields.map((field) => dialect.columnDefinition(field));
38
+ for (const entry of compoundUniques) {
39
+ if (!dialect.supportsNamedUniqueConstraint)
40
+ continue;
41
+ const fieldsStr = quoteFields(entry.fields).join(', ');
42
+ columnDefs.push(`CONSTRAINT ${dialect.quote(entry.name)} UNIQUE (${fieldsStr})`);
43
+ }
44
+ await db.execute(dialect.createTable(sqlTableName, columnDefs));
45
+ db.log(`✅ Table ${sqlTableName} created.`);
46
+ result.tablesCreated += 1;
47
+ }
48
+ else {
49
+ for (const field of model.fields) {
50
+ const columnExists = (await db.query(dialect.columnExists(tableName, field.name))).length > 0;
51
+ if (columnExists)
52
+ continue;
53
+ db.log(`Adding column ${dialect.quote(field.name)} to table ${sqlTableName}...`);
54
+ await db.execute(dialect.addColumn(sqlTableName, field));
55
+ result.columnsAdded += 1;
56
+ // A new `@unique` column needs its unique too, and on SQLite that cannot
57
+ // ride along with the ADD COLUMN — SQLite refuses to add a UNIQUE column.
58
+ if (field.isUnique && !field.isId && !dialect.supportsUniqueInlineOnAddColumn) {
59
+ const indexName = `UQ_${safeName}_${field.name}`;
60
+ const quoted = dialect.quote(indexName);
61
+ if ((await db.query(dialect.uniqueConstraintExists(tableName, indexName))).length === 0) {
62
+ db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
63
+ await db.execute(dialect.createUniqueIndex(quoted, sqlTableName, quoteFields([field.name])));
64
+ result.uniqueConstraintsAdded += 1;
65
+ }
66
+ }
67
+ }
68
+ result.tablesAltered += 1;
69
+ }
70
+ // Unique constraints that are not part of the column definitions above.
71
+ for (const entry of compoundUniques) {
72
+ if (await hasUniqueConstraint(db, dialect, tableName, entry.name))
73
+ continue;
74
+ const quoted = dialect.quote(entry.name);
75
+ if (dialect.supportsNamedUniqueConstraint) {
76
+ db.log(`Adding compound unique constraint ${quoted} to table ${sqlTableName}...`);
77
+ await db.execute(dialect.addUniqueConstraint(quoted, sqlTableName, quoteFields(entry.fields)));
78
+ }
79
+ else {
80
+ db.log(`Creating unique index ${quoted} on table ${sqlTableName}...`);
81
+ await db.execute(dialect.createUniqueIndex(quoted, sqlTableName, quoteFields(entry.fields)));
82
+ result.indexesCreated += 1;
83
+ result.uniqueConstraintsAdded += 1;
84
+ }
85
+ result.uniqueConstraintsAdded += 1;
86
+ }
87
+ for (const [idx, directive] of model.indexes.entries()) {
88
+ const name = (0, push_schema_1.directiveName)(directive, model.tableName, 'index', idx);
89
+ if ((await db.query(dialect.indexExists(tableName, name))).length > 0)
90
+ continue;
91
+ const quoted = dialect.quote(name);
92
+ db.log(`Creating index ${quoted} on table ${sqlTableName}...`);
93
+ await db.execute(dialect.createIndex(quoted, sqlTableName, quoteFields(directive.fields)));
94
+ result.indexesCreated += 1;
95
+ }
96
+ }
97
+ return result;
98
+ }
99
+ /**
100
+ * Whether the unique is already there.
101
+ *
102
+ * A compound unique created inside `CREATE TABLE` has no separate object to look
103
+ * for on the providers that keep them, so it is treated as present once the table
104
+ * exists; the per-column uniques inside a column definition are handled with the
105
+ * column above.
106
+ */
107
+ async function hasUniqueConstraint(db, dialect, tableName, name) {
108
+ return (await db.query(dialect.uniqueConstraintExists(tableName, name))).length > 0;
109
+ }