tempest-db-js 0.2.0 → 0.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.
package/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  📖 **Documentation:** [Português (BR)](https://mauriciobenjamin700.github.io/tempest-db-js/) · [English (US)](https://mauriciobenjamin700.github.io/tempest-db-js/en/)
7
7
 
8
- > ✅ **Status: alpha (v0.2.0), published on [npm](https://www.npmjs.com/package/tempest-db-js).** The full path works end-to-end — declarative models, typed query builder (aggregations, `DISTINCT`, upsert), **real SQLite execution** (tested against `node:sqlite`), joins, relations, Alembic-style migrations + a `tempest-db` CLI, a typed `BaseRepository`, and an opt-in active-record layer. The public API may still shift before v1.0.
8
+ > ✅ **Status: alpha (v0.3.0), published on [npm](https://www.npmjs.com/package/tempest-db-js).** The full path works end-to-end — declarative models, typed query builder (aggregations, `DISTINCT`, upsert), **real SQLite + PostgreSQL execution**, a **MySQL** dialect, joins, relations, Alembic-style migrations (sync + **async** runner) with a `tempest-db` CLI, a typed `BaseRepository`, and an opt-in active-record layer. The public API may still shift before v1.0.
9
9
 
10
10
  ## Why tempest-db-js
11
11
 
@@ -105,7 +105,7 @@ HTTP integration recipes (Hono, Express, Fastify) live in the [docs](https://mau
105
105
 
106
106
  ## Roadmap
107
107
 
108
- See [ROADMAP.md](./ROADMAP.md). Shipped: SQLite + PostgreSQL execution, joins, relations, migrations, repository. Next: `tempest-ts-sdk` integration and PostgreSQL CI against a live database.
108
+ See [ROADMAP.md](./ROADMAP.md). Shipped (v0.3.0): SQLite + PostgreSQL execution (both tested in CI, Postgres against a live database), a MySQL dialect, joins, relations, sync + async migration runners with a `tempest-db` CLI, repository, aggregations/upsert, opt-in active-record. Next: MySQL execution in CI + `RETURNING` round-trip, async CLI wiring, then `tempest-ts-sdk`.
109
109
 
110
110
  ## Development
111
111
 
package/dist/bin.cjs CHANGED
@@ -97,8 +97,8 @@ function makeRevisionId(label, parents) {
97
97
  }
98
98
 
99
99
  // src/migrations/ddl.ts
100
- function quoteId(name) {
101
- return `"${name.replace(/"/g, '""')}"`;
100
+ function quoteId(name, dialect) {
101
+ return dialect === "mysql" ? `\`${name.replace(/`/g, "``")}\`` : `"${name.replace(/"/g, '""')}"`;
102
102
  }
103
103
  function quoteLiteral(value) {
104
104
  return `'${value.replace(/'/g, "''")}'`;
@@ -123,6 +123,45 @@ function renderColumnType(type, dialect) {
123
123
  return "TEXT";
124
124
  }
125
125
  }
126
+ if (dialect === "mysql") {
127
+ switch (kind) {
128
+ case "smallint":
129
+ return "SMALLINT";
130
+ case "integer":
131
+ return "INT";
132
+ case "bigint":
133
+ return "BIGINT";
134
+ case "numeric":
135
+ return meta.precision !== void 0 ? `DECIMAL(${meta.precision}${meta.scale !== void 0 ? `, ${meta.scale}` : ""})` : "DECIMAL";
136
+ case "real":
137
+ return "FLOAT";
138
+ case "double":
139
+ return "DOUBLE";
140
+ case "varchar":
141
+ return `VARCHAR(${meta.length ?? 255})`;
142
+ case "char":
143
+ return `CHAR(${meta.length ?? 255})`;
144
+ case "text":
145
+ return "TEXT";
146
+ case "boolean":
147
+ return "TINYINT(1)";
148
+ case "date":
149
+ return "DATE";
150
+ case "time":
151
+ return "TIME";
152
+ case "datetime":
153
+ case "timestamp":
154
+ return "DATETIME";
155
+ case "blob":
156
+ return "BLOB";
157
+ case "json":
158
+ return "JSON";
159
+ case "uuid":
160
+ return "CHAR(36)";
161
+ case "enum":
162
+ return `ENUM(${(meta.values ?? []).map(quoteLiteral).join(", ")})`;
163
+ }
164
+ }
126
165
  switch (kind) {
127
166
  case "smallint":
128
167
  return "SMALLINT";
@@ -167,19 +206,21 @@ function renderDefault(def, dialect) {
167
206
  if (typeof expr === "object") return expr.raw;
168
207
  switch (expr) {
169
208
  case "now":
170
- return dialect === "sqlite" ? "CURRENT_TIMESTAMP" : "now()";
209
+ return dialect === "postgresql" ? "now()" : "CURRENT_TIMESTAMP";
171
210
  case "current_date":
172
211
  return "CURRENT_DATE";
173
212
  case "current_time":
174
213
  return "CURRENT_TIME";
175
214
  case "uuidv4":
176
- return dialect === "sqlite" ? "(lower(hex(randomblob(16))))" : "gen_random_uuid()";
215
+ if (dialect === "postgresql") return "gen_random_uuid()";
216
+ if (dialect === "mysql") return "(UUID())";
217
+ return "(lower(hex(randomblob(16))))";
177
218
  }
178
219
  }
179
220
  const value = def.value;
180
221
  if (value === null) return "NULL";
181
222
  if (typeof value === "boolean") {
182
- return dialect === "sqlite" ? value ? "1" : "0" : value ? "TRUE" : "FALSE";
223
+ return dialect === "postgresql" ? value ? "TRUE" : "FALSE" : value ? "1" : "0";
183
224
  }
184
225
  if (typeof value === "number" || typeof value === "bigint") return String(value);
185
226
  if (value instanceof Date) return quoteLiteral(value.toISOString());
@@ -187,7 +228,7 @@ function renderDefault(def, dialect) {
187
228
  return quoteLiteral(String(value));
188
229
  }
189
230
  function renderColumnDef(col, dialect) {
190
- let sql = `${quoteId(col.name)} ${renderColumnType(col.type, dialect)}`;
231
+ let sql = `${quoteId(col.name, dialect)} ${renderColumnType(col.type, dialect)}`;
191
232
  if (col.notNull) sql += " NOT NULL";
192
233
  if (col.default !== null) sql += ` DEFAULT ${renderDefault(col.default, dialect)}`;
193
234
  return sql;
@@ -209,23 +250,28 @@ function renderCreateTable(table, dialect) {
209
250
  if (dialect === "postgresql" && c.type.kind === "enum") {
210
251
  const typeName = enumTypeName(table.name, c.name);
211
252
  const values = (c.type.meta.values ?? []).map(quoteLiteral).join(", ");
212
- typeStmts.push(`CREATE TYPE ${quoteId(typeName)} AS ENUM (${values})`);
213
- let def = `${quoteId(c.name)} ${quoteId(typeName)}`;
253
+ typeStmts.push(`CREATE TYPE ${quoteId(typeName, dialect)} AS ENUM (${values})`);
254
+ let def = `${quoteId(c.name, dialect)} ${quoteId(typeName, dialect)}`;
214
255
  if (c.notNull) def += " NOT NULL";
215
256
  if (c.default !== null) def += ` DEFAULT ${renderDefault(c.default, dialect)}`;
216
257
  return def;
217
258
  }
218
259
  if (dialect === "postgresql" && isAutoIncrementPk(table, c)) {
219
- return `${quoteId(c.name)} ${postgresSerialType(c.type.kind)}`;
260
+ return `${quoteId(c.name, dialect)} ${postgresSerialType(c.type.kind)}`;
261
+ }
262
+ if (dialect === "mysql" && isAutoIncrementPk(table, c)) {
263
+ return `${quoteId(c.name, dialect)} ${renderColumnType(c.type, dialect)} NOT NULL AUTO_INCREMENT`;
220
264
  }
221
265
  return renderColumnDef(c, dialect);
222
266
  });
223
267
  if (table.primaryKey.length > 0) {
224
- cols.push(`PRIMARY KEY (${table.primaryKey.map(quoteId).join(", ")})`);
268
+ cols.push(
269
+ `PRIMARY KEY (${table.primaryKey.map((c) => quoteId(c, dialect)).join(", ")})`
270
+ );
225
271
  }
226
272
  return [
227
273
  ...typeStmts,
228
- `CREATE TABLE ${quoteId(table.name)} (
274
+ `CREATE TABLE ${quoteId(table.name, dialect)} (
229
275
  ${cols.join(",\n ")}
230
276
  )`
231
277
  ];
@@ -235,23 +281,27 @@ function renderOperation(op, dialect) {
235
281
  case "create_table":
236
282
  return renderCreateTable(op.table, dialect);
237
283
  case "drop_table":
238
- return [`DROP TABLE ${quoteId(op.table.name)}`];
284
+ return [`DROP TABLE ${quoteId(op.table.name, dialect)}`];
239
285
  case "rename_table":
240
- return [`ALTER TABLE ${quoteId(op.from)} RENAME TO ${quoteId(op.to)}`];
286
+ return dialect === "mysql" ? [`RENAME TABLE ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`] : [
287
+ `ALTER TABLE ${quoteId(op.from, dialect)} RENAME TO ${quoteId(op.to, dialect)}`
288
+ ];
241
289
  case "add_column":
242
290
  return [
243
- `ALTER TABLE ${quoteId(op.table)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
291
+ `ALTER TABLE ${quoteId(op.table, dialect)} ADD COLUMN ${renderColumnDef(op.column, dialect)}`
244
292
  ];
245
293
  case "drop_column":
246
- return [`ALTER TABLE ${quoteId(op.table)} DROP COLUMN ${quoteId(op.column.name)}`];
294
+ return [
295
+ `ALTER TABLE ${quoteId(op.table, dialect)} DROP COLUMN ${quoteId(op.column.name, dialect)}`
296
+ ];
247
297
  case "rename_column":
248
298
  return [
249
- `ALTER TABLE ${quoteId(op.table)} RENAME COLUMN ${quoteId(op.from)} TO ${quoteId(op.to)}`
299
+ `ALTER TABLE ${quoteId(op.table, dialect)} RENAME COLUMN ${quoteId(op.from, dialect)} TO ${quoteId(op.to, dialect)}`
250
300
  ];
251
301
  case "alter_column":
252
302
  return renderAlterColumn(op.table, op.to, dialect);
253
303
  case "recreate_table":
254
- return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderPostgresTableDiff(op.from, op.to);
304
+ return dialect === "sqlite" ? renderSqliteRebuild(op.from, op.to) : renderTableDiff(op.from, op.to, dialect);
255
305
  case "execute":
256
306
  return [op.up];
257
307
  }
@@ -261,34 +311,38 @@ function renderSqliteRebuild(from, to) {
261
311
  const common = Object.keys(to.columns).filter((c) => c in from.columns);
262
312
  const cols = Object.values(to.columns).map((c) => renderColumnDef(c, "sqlite"));
263
313
  if (to.primaryKey.length > 0) {
264
- cols.push(`PRIMARY KEY (${to.primaryKey.map(quoteId).join(", ")})`);
314
+ cols.push(
315
+ `PRIMARY KEY (${to.primaryKey.map((c) => quoteId(c, "sqlite")).join(", ")})`
316
+ );
265
317
  }
266
- const commonSql = common.map(quoteId).join(", ");
318
+ const commonSql = common.map((c) => quoteId(c, "sqlite")).join(", ");
267
319
  return [
268
320
  "PRAGMA foreign_keys=off",
269
- `CREATE TABLE ${quoteId(tmp)} (
321
+ `CREATE TABLE ${quoteId(tmp, "sqlite")} (
270
322
  ${cols.join(",\n ")}
271
323
  )`,
272
- common.length > 0 ? `INSERT INTO ${quoteId(tmp)} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name)}` : `-- no common columns to copy from ${from.name}`,
273
- `DROP TABLE ${quoteId(from.name)}`,
274
- `ALTER TABLE ${quoteId(tmp)} RENAME TO ${quoteId(to.name)}`,
324
+ common.length > 0 ? `INSERT INTO ${quoteId(tmp, "sqlite")} (${commonSql}) SELECT ${commonSql} FROM ${quoteId(from.name, "sqlite")}` : `-- no common columns to copy from ${from.name}`,
325
+ `DROP TABLE ${quoteId(from.name, "sqlite")}`,
326
+ `ALTER TABLE ${quoteId(tmp, "sqlite")} RENAME TO ${quoteId(to.name, "sqlite")}`,
275
327
  "PRAGMA foreign_keys=on"
276
328
  ];
277
329
  }
278
- function renderPostgresTableDiff(from, to) {
330
+ function renderTableDiff(from, to, dialect) {
279
331
  const stmts = [];
280
332
  for (const [name, col] of Object.entries(to.columns)) {
281
333
  if (!(name in from.columns)) {
282
334
  stmts.push(
283
- `ALTER TABLE ${quoteId(to.name)} ADD COLUMN ${renderColumnDef(col, "postgresql")}`
335
+ `ALTER TABLE ${quoteId(to.name, dialect)} ADD COLUMN ${renderColumnDef(col, dialect)}`
284
336
  );
285
337
  } else {
286
- stmts.push(...renderAlterColumn(to.name, col, "postgresql"));
338
+ stmts.push(...renderAlterColumn(to.name, col, dialect));
287
339
  }
288
340
  }
289
341
  for (const name of Object.keys(from.columns)) {
290
342
  if (!(name in to.columns)) {
291
- stmts.push(`ALTER TABLE ${quoteId(to.name)} DROP COLUMN ${quoteId(name)}`);
343
+ stmts.push(
344
+ `ALTER TABLE ${quoteId(to.name, dialect)} DROP COLUMN ${quoteId(name, dialect)}`
345
+ );
292
346
  }
293
347
  }
294
348
  return stmts;
@@ -296,11 +350,16 @@ function renderPostgresTableDiff(from, to) {
296
350
  function renderAlterColumn(table, to, dialect) {
297
351
  if (dialect === "sqlite") {
298
352
  throw new Error(
299
- `alter_column on SQLite needs batch/table-rebuild (Phase 6e); column ${table}.${to.name}`
353
+ `alter_column on SQLite needs a table-rebuild (recreate_table); column ${table}.${to.name}`
300
354
  );
301
355
  }
302
- const t = quoteId(table);
303
- const c = quoteId(to.name);
356
+ if (dialect === "mysql") {
357
+ return [
358
+ `ALTER TABLE ${quoteId(table, dialect)} MODIFY COLUMN ${renderColumnDef(to, dialect)}`
359
+ ];
360
+ }
361
+ const t = quoteId(table, dialect);
362
+ const c = quoteId(to.name, dialect);
304
363
  const stmts = [
305
364
  `ALTER TABLE ${t} ALTER COLUMN ${c} TYPE ${renderColumnType(to.type, dialect)}`
306
365
  ];