@mlagie/sql-connector 3.1.0 → 3.3.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.
@@ -171,37 +171,13 @@ class SchemaDiffer {
171
171
 
172
172
  /*
173
173
  * Second pass:
174
- * detect additions and modifications.
175
- */
176
- for (const [columnName, field] of Object.entries(desiredColumns)) {
177
- const currentField = getSafe(virtualCurrentColumns, columnName);
178
-
179
- if (!currentField) {
180
- operations.push({
181
- type: "addColumn",
182
- table: tableName,
183
- column: columnName,
184
- field
185
- });
186
-
187
- continue;
188
- }
189
-
190
- if (!equivalentField(field, currentField)) {
191
- operations.push({
192
- type: "alterColumn",
193
- table: tableName,
194
- column: columnName,
195
- field,
196
- previous: currentField
197
- });
198
- }
199
- }
200
-
201
- /*
202
- * Third pass:
203
174
  * detect removed columns.
204
175
  *
176
+ * Deletions are emitted BEFORE additions/alterations so that a
177
+ * dropped primary key column (e.g. `uuid`) releases its PRIMARY KEY
178
+ * before a new one (e.g. `id`) is added; otherwise PostgreSQL fails
179
+ * with "multiple primary keys for table".
180
+ *
205
181
  * Renamed columns are excluded because their old name
206
182
  * intentionally disappears from the desired schema.
207
183
  */
@@ -228,6 +204,35 @@ class SchemaDiffer {
228
204
  operations.push(operation);
229
205
  }
230
206
  }
207
+ /*
208
+ * Third pass:
209
+ * detect additions and modifications (after the deletions).
210
+ */
211
+ for (const [columnName, field] of Object.entries(desiredColumns)) {
212
+ const currentField = getSafe(virtualCurrentColumns, columnName);
213
+
214
+ if (!currentField) {
215
+ operations.push({
216
+ type: "addColumn",
217
+ table: tableName,
218
+ column: columnName,
219
+ field
220
+ });
221
+
222
+ continue;
223
+ }
224
+
225
+ if (!equivalentField(field, currentField)) {
226
+ operations.push({
227
+ type: "alterColumn",
228
+ table: tableName,
229
+ column: columnName,
230
+ field,
231
+ previous: currentField
232
+ });
233
+ }
234
+ }
235
+
231
236
  }
232
237
 
233
238
  /*
@@ -22,7 +22,7 @@ class SchemaInspector {
22
22
 
23
23
  const columnRows = await this.query(
24
24
  queries.columns,
25
- this.dialect.name === "postgres" ? [tableName] : [tableName]
25
+ [tableName]
26
26
  );
27
27
  setSafe(tables, tableName, {
28
28
  columns: Object.fromEntries(
@@ -4,7 +4,6 @@ const { MigrationStore } = require("./MigrationStore");
4
4
  const { MigrationRunner } = require("./MigrationRunner");
5
5
  const { SchemaDiffer } = require("./SchemaDiffer");
6
6
  const { SchemaInspector } = require("./SchemaInspector");
7
- const { MySQLDialect, PostgreSQLDialect } = require("./dialects");
8
7
 
9
8
  module.exports = {
10
9
  Migration,
@@ -13,6 +12,4 @@ module.exports = {
13
12
  MigrationRunner,
14
13
  SchemaDiffer,
15
14
  SchemaInspector,
16
- MySQLDialect,
17
- PostgreSQLDialect
18
15
  };
@@ -26,7 +26,7 @@ function getColumnDefinition(fieldName, field) {
26
26
  }
27
27
 
28
28
  const fieldType = getFieldType(field);
29
- const type = getSafe(sqlTypeMap, fieldType);
29
+ const type = getDialect().mapType(fieldType);
30
30
  if (!type) throw new Error(`Field ${fieldName} has unsupported type ${fieldType}.`);
31
31
 
32
32
  let colDef;
@@ -38,8 +38,8 @@ function getColumnDefinition(fieldName, field) {
38
38
  const scale = field.scale >= 0 ? field.scale : 0;
39
39
  colDef = `DECIMAL(${precision}, ${scale})`;
40
40
  } else {
41
- const hasLength = LENGTH_TYPES.has(type) || (type === "INT" && getDialect().name === "mysql");
42
- colDef = `${type}${hasLength ? `(${field.length > 0 ? field.length : 255})` : ""}`;
41
+ const hasLength = LENGTH_TYPES.has(type) || type === "INT";
42
+ colDef = getDialect().columnType(type, hasLength ? (field.length > 0 ? field.length : 255) : undefined);
43
43
  }
44
44
 
45
45
  if (field.required) colDef += ' NOT NULL';
@@ -48,7 +48,7 @@ function getColumnDefinition(fieldName, field) {
48
48
 
49
49
  if (defaultDefinition !== null) colDef += ` ${defaultDefinition}`;
50
50
  if (field.unique) colDef += ' UNIQUE';
51
- if (field.auto_increment) colDef += ' AUTO_INCREMENT';
51
+ if (field.auto_increment) colDef += ` ${getDialect().autoIncrement(fieldType)}`;
52
52
  if (field.primary_key) colDef += ' PRIMARY KEY'
53
53
  if (typeof field.customize === 'string' && field.customize.length != 0) colDef += ` ${field.customize}`;
54
54
  return `${getDialect().escape(fieldName)} ${colDef}`;
@@ -165,7 +165,7 @@ class Model {
165
165
  return `${getDialect().escape(fieldName)} ENUM(${enumValues})`;
166
166
  }
167
167
 
168
- const type = getSafe(sqlTypeMap, fieldType);
168
+ const type = getDialect().mapType(fieldType);
169
169
 
170
170
  if (!type) throw new Error(`Field ${fieldName} has unsupported type ${field}`);
171
171
  if (LENGTH_TYPES.has(type)) return `${getDialect().escape(fieldName)} ${type}(${lengthDefault})`;
@@ -183,7 +183,7 @@ class Model {
183
183
  */
184
184
  async save(data) {
185
185
  const keys = Object.keys(data);
186
- const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map(() => "?").join(", ")})`;
186
+ const sql_request = `INSERT INTO ${getDialect().escape(this.name)} (${getDialect().escapeIdentifierList(keys)}) VALUES (${keys.map((_, index) => getDialect().getPlaceholder(index)).join(", ")})`;
187
187
 
188
188
  try {
189
189
  const result = await getDialect().execute(getConnexion(), sql_request, Object.values(data));
@@ -0,0 +1,11 @@
1
+ const { getDialect } = require("../../db/dialects");
2
+
3
+ module.exports = (aggregateType, field) => {
4
+ if (typeof field === 'string') return `${aggregateType}(${getDialect().escape(field)})`;
5
+ if (field instanceof Array && field !== null) return field.map(col => `${aggregateType}(${getDialect().escape(col)})`).join(' + ');
6
+ if (field instanceof Object) {
7
+ const [key, value] = Object.entries(field)[0];
8
+ return `${aggregateType}(CASE WHEN ${getDialect().escape(key)} = ${getDialect().escapeValue(value)} THEN 1 END)`;
9
+ }
10
+ throw new Error(`Invalid field type for ${aggregateType}. Must be a string, array, or object.`);
11
+ }
@@ -1,6 +1,6 @@
1
1
  const { getDialect } = require("../../db/dialects");
2
2
  const { escapeOrderDirection } = require("../sql");
3
- const count = require("./count");
3
+ const buildAggregationField = require("./buildAggregationField");
4
4
 
5
5
  function buildGroupByItem(group) {
6
6
  let sql;
@@ -10,7 +10,7 @@ function buildGroupByItem(group) {
10
10
  if (typeof group === 'object') {
11
11
  if (group.dateFormat) {
12
12
  const [col, format] = group.dateFormat;
13
- sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
13
+ sql = getDialect().dateFormat(getDialect().escape(col), format);
14
14
  }
15
15
  if (group.col) {
16
16
  sql = getDialect().escape(group.col);
@@ -36,14 +36,16 @@ function buildField(field) {
36
36
  sql = `SUM(${getDialect().escape(field.sum)})`;
37
37
  else if (field.dateFormat) {
38
38
  const [col, format] = field.dateFormat;
39
- sql = `DATE_FORMAT(${getDialect().escape(col)}, ${getDialect().escapeValue(format)})`;
39
+ sql = getDialect().dateFormat(getDialect().escape(col), format);
40
40
  }
41
41
  else if (field.col)
42
42
  sql = getDialect().escape(field.col);
43
43
  else if (field.distinct)
44
44
  sql = `DISTINCT ${getDialect().escape(field.distinct)}`;
45
45
  else if (field.count)
46
- sql = count(field.count);
46
+ sql = buildAggregationField("COUNT", field.count);
47
+ else if (field.max)
48
+ sql = buildAggregationField("MAX", field.max);
47
49
  if (field.as)
48
50
  sql += ` AS ${getDialect().escape(field.as)}`;
49
51
  else if (field.sum)
@@ -64,8 +66,7 @@ function buildSelect(select = []) {
64
66
  const COMPARISON_OPERATORS = ["=", "!=", ">", "<", ">=", "<=", "LIKE", "NOT LIKE", "ILIKE", "NOT ILIKE", "IN", "NOT IN", "BETWEEN", "NOT BETWEEN"];
65
67
 
66
68
  function buildCaseInsensitiveLike(dialect, key, placeholder, negate) {
67
- if (dialect.name === "postgres") return null;
68
- return `${negate ? "NOT " : ""}LOWER(${dialect.escape(key)}) LIKE LOWER(${placeholder})`;
69
+ return dialect.caseInsensitiveLike(dialect.escape(key), placeholder, negate);
69
70
  }
70
71
 
71
72
  function buildWhere(where, values, offset) {
@@ -139,7 +140,7 @@ function buildWhere(where, values, offset) {
139
140
  const negate = operator === "NOT ILIKE";
140
141
  const fallback = buildCaseInsensitiveLike(dialect, key, placeholder, negate);
141
142
 
142
- conditions.push(fallback ?? `${dialect.escape(key)} ${operator} ${placeholder}`);
143
+ conditions.push(fallback);
143
144
  values.push(operatorValue);
144
145
  } else if (operatorValue === null && (operator === "=" || operator === "!=")) {
145
146
  conditions.push(`${dialect.escape(key)} IS ${operator === "!=" ? "NOT " : ""}NULL`);
@@ -1,112 +0,0 @@
1
- const { quoteIdentifier } = require("../utils");
2
-
3
- class Dialect {
4
- constructor(name, options = {}) {
5
- this.name = name;
6
- this.quote = options.quote;
7
- this.capabilities = {
8
- transactionalDDL: false,
9
- renameColumn: true,
10
- alterColumn: true,
11
- ...options.capabilities
12
- };
13
- }
14
-
15
- escape(name) {
16
- return quoteIdentifier(name, this.quote);
17
- }
18
-
19
- placeholder() {
20
- return "?";
21
- }
22
-
23
- placeholders(count) {
24
- return Array.from({ length: count }, (_, i) => this.placeholder(i)).join(", ");
25
- }
26
-
27
- execute() {
28
- throw new Error(`${this.name} dialect must implement execute().`);
29
- }
30
-
31
- async begin() {}
32
- async commit() {}
33
- async rollback() {}
34
-
35
- mapType() {
36
- throw new Error(`${this.name} dialect must implement mapType().`);
37
- }
38
-
39
- columnDefinition() {
40
- throw new Error(`${this.name} dialect must implement columnDefinition().`);
41
- }
42
-
43
- createTable() {
44
- throw new Error(`${this.name} dialect must implement createTable().`);
45
- }
46
-
47
- addColumn(table, name, field) {
48
- return `ALTER TABLE ${this.escape(table)} ADD COLUMN ${this.columnDefinition(name, field)};`;
49
- }
50
-
51
- renameColumn(table, oldName, newName) {
52
- return `ALTER TABLE ${this.escape(table)} RENAME COLUMN ${this.escape(oldName)} TO ${this.escape(newName)};`;
53
- }
54
-
55
- alterColumn() {
56
- throw new Error(`${this.name} does not implement alterColumn().`);
57
- }
58
-
59
- dropColumn(table, name) {
60
- return `ALTER TABLE ${this.escape(table)} DROP COLUMN ${this.escape(name)};`;
61
- }
62
-
63
- dropTable(table) {
64
- return `DROP TABLE ${this.escape(table)};`;
65
- }
66
-
67
- introspectionQueries() {
68
- throw new Error(`${this.name} dialect must implement introspectionQueries().`);
69
- }
70
-
71
- normalizeDatabaseColumn(row) {
72
- return row;
73
- }
74
-
75
- normalizeDatabaseTable(row) {
76
- return row;
77
- }
78
-
79
- createMigrationTable() {
80
- throw new Error(`${this.name} dialect must implement createMigrationTable().`);
81
- }
82
-
83
- insertMigration() {
84
- throw new Error(`${this.name} dialect must implement insertMigration().`);
85
- }
86
-
87
- selectMigrations() {
88
- throw new Error(`${this.name} dialect must implement selectMigrations().`);
89
- }
90
-
91
- deleteMigration() {
92
- throw new Error(`${this.name} dialect must implement deleteMigration().`);
93
- }
94
-
95
- async transaction(connection, callback) {
96
- if (!this.capabilities.transactionalDDL) return callback();
97
-
98
- await this.begin(connection);
99
- try {
100
- const result = await callback();
101
- await this.commit(connection);
102
- return result;
103
- } catch (error) {
104
- try {
105
- await this.rollback(connection);
106
- } catch {}
107
- throw error;
108
- }
109
- }
110
- }
111
-
112
- module.exports = { Dialect };
@@ -1,205 +0,0 @@
1
- const { Dialect } = require("./Dialect");
2
- const { normalizeType } = require("../utils");
3
-
4
- class MySQLDialect extends Dialect {
5
- constructor() {
6
- super("mysql", {
7
- quote: "`",
8
- capabilities: {
9
- transactionalDDL: false,
10
- renameColumn: true,
11
- alterColumn: true
12
- }
13
- });
14
- }
15
-
16
- placeholder() {
17
- return "?";
18
- }
19
-
20
- execute(connection, sql, values = []) {
21
- return connection.promise ? connection.promise().query(sql, values).then(([rows]) => rows)
22
- : new Promise((resolve, reject) => {
23
- connection.query(sql, values, (error, rows) => error ? reject(error) : resolve(rows));
24
- });
25
- }
26
-
27
- mapType(field) {
28
- const rawType = normalizeType(field);
29
- const type = String(rawType || "").toUpperCase();
30
- const length = field.length || 255;
31
-
32
- switch (type) {
33
- case "STRING":
34
- case "VARCHAR":
35
- return `VARCHAR(${length})`;
36
- case "CHAR":
37
- return `CHAR(${length})`;
38
- case "TEXT":
39
- return "TEXT";
40
- case "NUMBER":
41
- case "INT":
42
- case "INTEGER":
43
- return "INT";
44
- case "SMALLINT":
45
- return "SMALLINT";
46
- case "MEDIUMINT":
47
- return "MEDIUMINT";
48
- case "BIGINT":
49
- return "BIGINT";
50
- case "DECIMAL":
51
- case "NUMERIC":
52
- return "DECIMAL";
53
- case "FLOAT":
54
- return "DOUBLE";
55
- case "DOUBLE":
56
- return "DOUBLE";
57
- case "BOOLEAN":
58
- case "BOOL":
59
- case "TINYINT":
60
- return "BOOLEAN";
61
- case "DATE":
62
- return "DATE";
63
- case "DATETIME":
64
- case "TIMESTAMP":
65
- case "NOW":
66
- case "CURRENTTIMESTAMP":
67
- return "DATETIME";
68
- case "OBJECT":
69
- case "JSON":
70
- return "JSON";
71
- case "BLOB":
72
- return "BLOB";
73
- case "VARBINARY":
74
- case "BINARY":
75
- return "VARBINARY";
76
- default:
77
- throw new Error(`Unsupported MySQL type: ${rawType}`);
78
- }
79
- }
80
-
81
- defaultSql(value) {
82
- if (value === undefined) return "";
83
- if (value === null) return "DEFAULT NULL";
84
- if (typeof value === "number") return `DEFAULT ${value}`;
85
- if (typeof value === "boolean") return `DEFAULT ${value ? 1 : 0}`;
86
- if (typeof value === "string") {
87
- if (/^(CURRENT_TIMESTAMP|CURRENT_DATE|CURRENT_TIME)\b/i.test(value)) {
88
- return `DEFAULT ${value}`;
89
- }
90
- return `DEFAULT '${value.replace(/'/g, "''")}'`;
91
- }
92
- return `DEFAULT '${String(value).replace(/'/g, "''")}'`;
93
- }
94
-
95
- columnDefinition(name, field) {
96
- if (field.primary_key && field.unique) {
97
- throw new Error(`Field '${name}' cannot be both PRIMARY KEY and UNIQUE.`);
98
- }
99
-
100
- let sql = `${this.escape(name)} ${this.mapType(field)}`;
101
-
102
- if (field.required || field.primary_key) sql += " NOT NULL";
103
- if (field.default !== undefined) sql += ` ${this.defaultSql(field.default, field)}`;
104
- if (field.unique) sql += " UNIQUE";
105
- if (field.auto_increment) sql += " AUTO_INCREMENT";
106
- if (field.primary_key) sql += " PRIMARY KEY";
107
- if (typeof field.customize === "string" && field.customize.trim()) {
108
- sql += ` ${field.customize.trim()}`;
109
- }
110
-
111
- return sql;
112
- }
113
-
114
- createTable(table, schema) {
115
- const columns = [];
116
- const foreignKeys = [];
117
-
118
- for (const [name, field] of Object.entries(schema)) {
119
- columns.push(this.columnDefinition(name, field));
120
-
121
- if (field.foreignKey) {
122
- const match = /^([A-Za-z_][A-Za-z0-9_$]*)(?:\s*\(\s*([A-Za-z_][A-Za-z0-9_$]*)\s*\)|\s*\.\s*([A-Za-z_][A-Za-z0-9_$]*)\s*)$/.exec(field.foreignKey);
123
- if (!match) throw new Error(`Invalid foreign key definition for ${name}.`);
124
- const refColumn = match[2] || match[3];
125
- foreignKeys.push(
126
- `FOREIGN KEY (${this.escape(name)}) REFERENCES ${this.escape(match[1])} (${this.escape(refColumn)})`
127
- );
128
- }
129
- }
130
-
131
- return `CREATE TABLE IF NOT EXISTS ${this.escape(table)} (${columns.concat(foreignKeys).join(", ")});`;
132
- }
133
-
134
- alterColumn(table, name, field) {
135
- return `ALTER TABLE ${this.escape(table)} MODIFY COLUMN ${this.columnDefinition(name, field)};`;
136
- }
137
-
138
- introspectionQueries() {
139
- return {
140
- tables: `
141
- SELECT TABLE_NAME AS table_name
142
- FROM information_schema.tables
143
- WHERE table_schema = DATABASE()
144
- AND table_type = 'BASE TABLE'
145
- `,
146
- columns: `
147
- SELECT
148
- COLUMN_NAME AS column_name,
149
- DATA_TYPE AS data_type,
150
- COLUMN_TYPE AS column_type,
151
- IS_NULLABLE AS is_nullable,
152
- COLUMN_DEFAULT AS column_default,
153
- COLUMN_KEY AS column_key,
154
- EXTRA AS extra,
155
- CHARACTER_MAXIMUM_LENGTH AS character_maximum_length
156
- FROM information_schema.columns
157
- WHERE table_schema = DATABASE()
158
- AND table_name = ?
159
- ORDER BY ORDINAL_POSITION
160
- `
161
- };
162
- }
163
-
164
- normalizeDatabaseColumn(row) {
165
- const type = String(row.data_type || "").toLowerCase();
166
- return {
167
- type,
168
- length: row.character_maximum_length ?? undefined,
169
- required: row.is_nullable === "NO",
170
- primary_key: row.column_key === "PRI",
171
- unique: row.column_key === "UNI",
172
- auto_increment: String(row.extra || "").toLowerCase().includes("auto_increment"),
173
- default: row.column_default
174
- };
175
- }
176
-
177
- createMigrationTable() {
178
- return `
179
- CREATE TABLE IF NOT EXISTS ${this.escape("sql_connector_migrations")} (
180
- ${this.escape("id")} VARCHAR(191) NOT NULL PRIMARY KEY,
181
- ${this.escape("checksum")} VARCHAR(64) NOT NULL,
182
- ${this.escape("applied_at")} DATETIME NOT NULL,
183
- ${this.escape("created_at")} DATETIME NOT NULL
184
- );
185
- `;
186
- }
187
-
188
- insertMigration() {
189
- return `
190
- INSERT INTO ${this.escape("sql_connector_migrations")}
191
- (${this.escape("id")}, ${this.escape("checksum")}, ${this.escape("applied_at")}, ${this.escape("created_at")})
192
- VALUES (?, ?, ?, ?)
193
- `;
194
- }
195
-
196
- selectMigrations() {
197
- return `SELECT ${this.escape("id")} AS id, ${this.escape("checksum")} AS checksum, ${this.escape("applied_at")} AS applied_at FROM ${this.escape("sql_connector_migrations")} ORDER BY ${this.escape("id")}`;
198
- }
199
-
200
- deleteMigration() {
201
- return `DELETE FROM ${this.escape("sql_connector_migrations")} WHERE ${this.escape("id")} = ?`;
202
- }
203
- }
204
-
205
- module.exports = { MySQLDialect };