bun-query-builder 0.2.35 → 0.2.37

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.
@@ -0,0 +1,74 @@
1
+ import type { MigrationPlan, NormalizedColumnType } from '../migrations';
2
+ import type { SupportedDialect } from '../types';
3
+ /** The family a model's declared type belongs to. */
4
+ export declare function familyOfDeclared(type: NormalizedColumnType): TypeFamily;
5
+ /** The family a raw SQL type reported by a database belongs to. */
6
+ export declare function familyOfSqlType(sqlType: string): TypeFamily;
7
+ /**
8
+ * Whether a live column is an acceptable home for what the model declares.
9
+ *
10
+ * Same family always passes. Beyond that, only widenings are tolerated: a
11
+ * model asking for `varchar` is content in a `text` column, because nothing is
12
+ * lost. The reverse is exactly the bug this exists to find.
13
+ */
14
+ export declare function satisfies(declared: TypeFamily, actual: TypeFamily): boolean;
15
+ /**
16
+ * Audit the live database against a migration plan.
17
+ *
18
+ * Pass the plan built from the models (`buildMigrationPlan`). Tables the models
19
+ * do not declare are ignored: a database is allowed to hold more than one
20
+ * application's models, and framework-owned tables are not this plan's business.
21
+ */
22
+ export declare function auditSchemaDrift(plan: MigrationPlan, options?: { dialect?: SupportedDialect }): Promise<SchemaDrift>;
23
+ /**
24
+ * A human-readable report, or an empty string when there is nothing to say.
25
+ *
26
+ * Says what to do as well as what is wrong. A drift warning that only names the
27
+ * problem leaves the reader to guess whether it matters, and the honest answer
28
+ * here is that it always does: the schema cannot repair itself, because the
29
+ * migration runner has already decided there is nothing to run.
30
+ */
31
+ export declare function formatSchemaDrift(drift: SchemaDrift): string;
32
+ /**
33
+ * Compare a live database against what the models declare.
34
+ *
35
+ * This exists because the failure it catches is silent by construction. The
36
+ * migration runner only applies migration FILES: it asks "which of these have
37
+ * not run yet", not "does the database actually look like the models". So a
38
+ * database created from an older, wrong migration set reports **"nothing to
39
+ * migrate — your database is already up to date"** forever, while a column that
40
+ * should be `text` sits at `varchar(255)` and one that should hold 99.5 is an
41
+ * integer that stores 100.
42
+ *
43
+ * Every layer says it is fine. The write succeeds, the read comes back rounded
44
+ * or the insert is refused for length, and the only symptom reaches a customer.
45
+ * `CREATE TABLE IF NOT EXISTS` cannot repair it either, so a corrected migration
46
+ * set deploys as a no-op and the wrong schema survives every deploy after it.
47
+ *
48
+ * The comparison is by type FAMILY rather than by exact SQL string. Dialects
49
+ * spell the same type several ways — `character varying`, `varchar`, `int4`,
50
+ * `integer`, `real`, `float4` — and an audit that reported those as drift would
51
+ * be noise, which is the fastest way to teach people to ignore it.
52
+ */
53
+ export declare interface ColumnDrift {
54
+ table: string
55
+ column: string
56
+ expected: string
57
+ actual: string
58
+ actualSqlType: string
59
+ }
60
+ export declare interface SchemaDrift {
61
+ missingTables: string[]
62
+ missingColumns: Array<{ table: string, column: string, expected: string }>
63
+ typeMismatches: ColumnDrift[]
64
+ clean: boolean
65
+ }
66
+ /**
67
+ * The coarse family a column type belongs to.
68
+ *
69
+ * Deliberately coarse. `bigint` and `integer` are both `integer` here: widening
70
+ * an id is not the class of mistake this looks for, and flagging it would bury
71
+ * the ones that are. What it does separate is the pairs that silently lose
72
+ * data — text from bounded varchar, and fractional from whole.
73
+ */
74
+ export type TypeFamily = 'text' | 'varchar' | 'integer' | 'fractional' | 'boolean' | 'date' | 'json' | 'binary' | 'other';
package/dist/bin/cli.js CHANGED
@@ -19489,13 +19489,27 @@ async function resetDatabase(dir, opts = {}) {
19489
19489
  const dialect = opts.dialect || config5.dialect || "postgres";
19490
19490
  const driver = getDialectDriver(dialect);
19491
19491
  const workspaceRoot = getWorkspaceRoot();
19492
+ const dropTableForReset = async (tableName) => {
19493
+ const dropSql = driver.dropTable(tableName);
19494
+ await withFreshConnection(async (bunSql2) => {
19495
+ if (dialect === "sqlite")
19496
+ await bunSql2.unsafe("PRAGMA foreign_keys = OFF").execute();
19497
+ if (dialect === "mysql" || dialect === "vitess")
19498
+ await bunSql2.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute();
19499
+ try {
19500
+ await bunSql2.unsafe(dropSql).execute();
19501
+ } finally {
19502
+ if (dialect === "sqlite")
19503
+ await bunSql2.unsafe("PRAGMA foreign_keys = ON").execute();
19504
+ if (dialect === "mysql" || dialect === "vitess")
19505
+ await bunSql2.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute();
19506
+ }
19507
+ });
19508
+ };
19492
19509
  try {
19493
- const dropMigrationsSql = driver.dropTable("migrations");
19494
19510
  try {
19495
- await withFreshConnection(async (bunSql2) => {
19496
- await bunSql2.unsafe(dropMigrationsSql).execute();
19497
- info("-- Dropped migrations table");
19498
- });
19511
+ await dropTableForReset("migrations");
19512
+ info("-- Dropped migrations table");
19499
19513
  } catch (err) {
19500
19514
  console.error(err);
19501
19515
  }
@@ -19526,11 +19540,8 @@ async function resetDatabase(dir, opts = {}) {
19526
19540
  info(`-- Dropping ${tableNames.length} tables: ${tableNames.join(", ")}`);
19527
19541
  for (const tableName of tableNames.reverse()) {
19528
19542
  try {
19529
- const dropSql = driver.dropTable(tableName);
19530
- await withFreshConnection(async (bunSql2) => {
19531
- await bunSql2.unsafe(dropSql).execute();
19532
- info(`-- Dropped table: ${tableName}`);
19533
- });
19543
+ await dropTableForReset(tableName);
19544
+ info(`-- Dropped table: ${tableName}`);
19534
19545
  } catch (err) {
19535
19546
  console.error(err);
19536
19547
  info(`-- Table ${tableName} may not exist, skipping drop`);
@@ -30092,10 +30103,139 @@ class Seeder {
30092
30103
  function defineSeeder(seederClass) {
30093
30104
  return seederClass;
30094
30105
  }
30106
+ // src/actions/schema-drift.ts
30107
+ function familyOfDeclared(type) {
30108
+ switch (type) {
30109
+ case "text":
30110
+ return "text";
30111
+ case "string":
30112
+ case "enum":
30113
+ return "varchar";
30114
+ case "integer":
30115
+ case "bigint":
30116
+ return "integer";
30117
+ case "float":
30118
+ case "double":
30119
+ case "decimal":
30120
+ return "fractional";
30121
+ case "boolean":
30122
+ return "boolean";
30123
+ case "date":
30124
+ case "datetime":
30125
+ case "timestamptz":
30126
+ return "date";
30127
+ case "json":
30128
+ return "json";
30129
+ default:
30130
+ return "other";
30131
+ }
30132
+ }
30133
+ function familyOfSqlType(sqlType) {
30134
+ const type = String(sqlType ?? "").toLowerCase().trim();
30135
+ if (/^(?:bool|boolean|bit)\b/.test(type) || type === "tinyint(1)")
30136
+ return "boolean";
30137
+ if (/^(?:json|jsonb)\b/.test(type))
30138
+ return "json";
30139
+ if (/^(?:bytea|blob|binary|varbinary)\b/.test(type))
30140
+ return "binary";
30141
+ if (/^(?:date|time|timestamp|datetime)\b/.test(type))
30142
+ return "date";
30143
+ if (/(?:^|\b)(?:varchar|character varying|nvarchar|char)\b/.test(type))
30144
+ return "varchar";
30145
+ if (/^(?:text|longtext|mediumtext|tinytext|clob|citext)\b/.test(type))
30146
+ return "text";
30147
+ if (/^(?:decimal|numeric|real|double|money)\b/.test(type) || /^float\d*\b/.test(type))
30148
+ return "fractional";
30149
+ if (/^(?:integer|bigint|smallint|tinyint|serial|bigserial|smallserial|mediumint)\b/.test(type) || /^int\d*\b/.test(type))
30150
+ return "integer";
30151
+ return "other";
30152
+ }
30153
+ function satisfies(declared, actual) {
30154
+ if (declared === actual)
30155
+ return true;
30156
+ if (declared === "varchar" && actual === "text")
30157
+ return true;
30158
+ if (declared === "integer" && actual === "fractional")
30159
+ return true;
30160
+ if (declared === "other" || actual === "other")
30161
+ return true;
30162
+ return false;
30163
+ }
30164
+ async function auditSchemaDrift(plan, options = {}) {
30165
+ const dialect = options.dialect ?? config5.dialect;
30166
+ const declaredTables = plan.tables.map((table) => table.table);
30167
+ const live = await introspectDatabase({ tables: declaredTables });
30168
+ const byTable = new Map(live.map((model) => [model.table.toLowerCase(), model]));
30169
+ const missingTables = [];
30170
+ const missingColumns = [];
30171
+ const typeMismatches = [];
30172
+ for (const table of plan.tables) {
30173
+ const liveTable = byTable.get(table.table.toLowerCase());
30174
+ if (!liveTable) {
30175
+ missingTables.push(table.table);
30176
+ continue;
30177
+ }
30178
+ const liveColumns = new Map(liveTable.columns.map((column) => [column.name.toLowerCase(), column]));
30179
+ for (const column of table.columns) {
30180
+ const liveColumn = liveColumns.get(column.name.toLowerCase());
30181
+ const declared = familyOfDeclared(column.type);
30182
+ if (!liveColumn) {
30183
+ missingColumns.push({ table: table.table, column: column.name, expected: declared });
30184
+ continue;
30185
+ }
30186
+ const actual = familyOfSqlType(liveColumn.sqlType);
30187
+ if (!satisfies(declared, actual)) {
30188
+ typeMismatches.push({
30189
+ table: table.table,
30190
+ column: column.name,
30191
+ expected: declared,
30192
+ actual,
30193
+ actualSqlType: liveColumn.sqlType
30194
+ });
30195
+ }
30196
+ }
30197
+ }
30198
+ return {
30199
+ missingTables,
30200
+ missingColumns,
30201
+ typeMismatches,
30202
+ clean: missingTables.length === 0 && missingColumns.length === 0 && typeMismatches.length === 0
30203
+ };
30204
+ }
30205
+ function formatSchemaDrift(drift) {
30206
+ if (drift.clean)
30207
+ return "";
30208
+ const lines = [];
30209
+ if (drift.missingTables.length > 0)
30210
+ lines.push(` Missing tables: ${drift.missingTables.join(", ")}`);
30211
+ for (const column of drift.missingColumns.slice(0, 10))
30212
+ lines.push(` \u2022 ${column.table}.${column.column} is missing (models declare ${column.expected})`);
30213
+ for (const mismatch of drift.typeMismatches.slice(0, 10))
30214
+ lines.push(` \u2022 ${mismatch.table}.${mismatch.column} is ${mismatch.actualSqlType}, models declare ${mismatch.expected}`);
30215
+ const shown = Math.min(drift.missingColumns.length, 10) + Math.min(drift.typeMismatches.length, 10);
30216
+ const total = drift.missingColumns.length + drift.typeMismatches.length;
30217
+ if (total > shown)
30218
+ lines.push(` + ${total - shown} more`);
30219
+ return [
30220
+ "The live schema does not match the models.",
30221
+ ...lines,
30222
+ "",
30223
+ "Migrations will NOT fix this on their own: the runner only applies files that have",
30224
+ "not run yet, and a corrected `CREATE TABLE IF NOT EXISTS` is a no-op against a table",
30225
+ "that already exists. Regenerate and apply the difference, or rebuild the schema."
30226
+ ].join(`
30227
+ `);
30228
+ }
30229
+ var init_schema_drift = __esm(() => {
30230
+ init_config();
30231
+ init_introspect_db();
30232
+ });
30233
+
30095
30234
  // src/index.ts
30096
30235
  var init_src2 = __esm(() => {
30097
30236
  init_model2();
30098
30237
  init_model2();
30238
+ init_schema_drift();
30099
30239
  init_actions();
30100
30240
  init_browser();
30101
30241
  init_client();
@@ -32189,7 +32329,7 @@ function getPrefix() {
32189
32329
  }
32190
32330
  var prefix = getPrefix();
32191
32331
  // package.json
32192
- var version2 = "0.2.35";
32332
+ var version2 = "0.2.37";
32193
32333
 
32194
32334
  // bin/cli.ts
32195
32335
  init_actions();
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ export type {
24
24
  ModelCreateDataLoose,
25
25
  RelationCardinality,
26
26
  } from './type-inference';
27
+ export type { ColumnDrift, SchemaDrift, TypeFamily } from './actions/schema-drift';
27
28
  export * from './actions/index';
28
29
  export * from './browser';
29
30
  export * from './client';
@@ -51,3 +52,4 @@ export { type ModelDefinition, defineModel } from './model';
51
52
  // Explicit re-exports for model registry functions
52
53
  export { getModel, getAllModels, getModelRegistry, hasModel, clearModelRegistry, registerModel } from './model';
53
54
  export { toDialectPlaceholders } from './sql-fragments';
55
+ export { auditSchemaDrift, familyOfDeclared, familyOfSqlType, formatSchemaDrift, satisfies } from './actions/schema-drift';
package/dist/src/index.js CHANGED
@@ -19488,13 +19488,27 @@ async function resetDatabase(dir, opts = {}) {
19488
19488
  const dialect = opts.dialect || config5.dialect || "postgres";
19489
19489
  const driver = getDialectDriver(dialect);
19490
19490
  const workspaceRoot = getWorkspaceRoot();
19491
+ const dropTableForReset = async (tableName) => {
19492
+ const dropSql = driver.dropTable(tableName);
19493
+ await withFreshConnection(async (bunSql2) => {
19494
+ if (dialect === "sqlite")
19495
+ await bunSql2.unsafe("PRAGMA foreign_keys = OFF").execute();
19496
+ if (dialect === "mysql" || dialect === "vitess")
19497
+ await bunSql2.unsafe("SET FOREIGN_KEY_CHECKS = 0").execute();
19498
+ try {
19499
+ await bunSql2.unsafe(dropSql).execute();
19500
+ } finally {
19501
+ if (dialect === "sqlite")
19502
+ await bunSql2.unsafe("PRAGMA foreign_keys = ON").execute();
19503
+ if (dialect === "mysql" || dialect === "vitess")
19504
+ await bunSql2.unsafe("SET FOREIGN_KEY_CHECKS = 1").execute();
19505
+ }
19506
+ });
19507
+ };
19491
19508
  try {
19492
- const dropMigrationsSql = driver.dropTable("migrations");
19493
19509
  try {
19494
- await withFreshConnection(async (bunSql2) => {
19495
- await bunSql2.unsafe(dropMigrationsSql).execute();
19496
- info("-- Dropped migrations table");
19497
- });
19510
+ await dropTableForReset("migrations");
19511
+ info("-- Dropped migrations table");
19498
19512
  } catch (err) {
19499
19513
  console.error(err);
19500
19514
  }
@@ -19525,11 +19539,8 @@ async function resetDatabase(dir, opts = {}) {
19525
19539
  info(`-- Dropping ${tableNames.length} tables: ${tableNames.join(", ")}`);
19526
19540
  for (const tableName of tableNames.reverse()) {
19527
19541
  try {
19528
- const dropSql = driver.dropTable(tableName);
19529
- await withFreshConnection(async (bunSql2) => {
19530
- await bunSql2.unsafe(dropSql).execute();
19531
- info(`-- Dropped table: ${tableName}`);
19532
- });
19542
+ await dropTableForReset(tableName);
19543
+ info(`-- Dropped table: ${tableName}`);
19533
19544
  } catch (err) {
19534
19545
  console.error(err);
19535
19546
  info(`-- Table ${tableName} may not exist, skipping drop`);
@@ -30091,10 +30102,139 @@ class Seeder {
30091
30102
  function defineSeeder(seederClass) {
30092
30103
  return seederClass;
30093
30104
  }
30105
+ // src/actions/schema-drift.ts
30106
+ function familyOfDeclared(type) {
30107
+ switch (type) {
30108
+ case "text":
30109
+ return "text";
30110
+ case "string":
30111
+ case "enum":
30112
+ return "varchar";
30113
+ case "integer":
30114
+ case "bigint":
30115
+ return "integer";
30116
+ case "float":
30117
+ case "double":
30118
+ case "decimal":
30119
+ return "fractional";
30120
+ case "boolean":
30121
+ return "boolean";
30122
+ case "date":
30123
+ case "datetime":
30124
+ case "timestamptz":
30125
+ return "date";
30126
+ case "json":
30127
+ return "json";
30128
+ default:
30129
+ return "other";
30130
+ }
30131
+ }
30132
+ function familyOfSqlType(sqlType) {
30133
+ const type = String(sqlType ?? "").toLowerCase().trim();
30134
+ if (/^(?:bool|boolean|bit)\b/.test(type) || type === "tinyint(1)")
30135
+ return "boolean";
30136
+ if (/^(?:json|jsonb)\b/.test(type))
30137
+ return "json";
30138
+ if (/^(?:bytea|blob|binary|varbinary)\b/.test(type))
30139
+ return "binary";
30140
+ if (/^(?:date|time|timestamp|datetime)\b/.test(type))
30141
+ return "date";
30142
+ if (/(?:^|\b)(?:varchar|character varying|nvarchar|char)\b/.test(type))
30143
+ return "varchar";
30144
+ if (/^(?:text|longtext|mediumtext|tinytext|clob|citext)\b/.test(type))
30145
+ return "text";
30146
+ if (/^(?:decimal|numeric|real|double|money)\b/.test(type) || /^float\d*\b/.test(type))
30147
+ return "fractional";
30148
+ if (/^(?:integer|bigint|smallint|tinyint|serial|bigserial|smallserial|mediumint)\b/.test(type) || /^int\d*\b/.test(type))
30149
+ return "integer";
30150
+ return "other";
30151
+ }
30152
+ function satisfies(declared, actual) {
30153
+ if (declared === actual)
30154
+ return true;
30155
+ if (declared === "varchar" && actual === "text")
30156
+ return true;
30157
+ if (declared === "integer" && actual === "fractional")
30158
+ return true;
30159
+ if (declared === "other" || actual === "other")
30160
+ return true;
30161
+ return false;
30162
+ }
30163
+ async function auditSchemaDrift(plan, options = {}) {
30164
+ const dialect = options.dialect ?? config5.dialect;
30165
+ const declaredTables = plan.tables.map((table) => table.table);
30166
+ const live = await introspectDatabase({ tables: declaredTables });
30167
+ const byTable = new Map(live.map((model) => [model.table.toLowerCase(), model]));
30168
+ const missingTables = [];
30169
+ const missingColumns = [];
30170
+ const typeMismatches = [];
30171
+ for (const table of plan.tables) {
30172
+ const liveTable = byTable.get(table.table.toLowerCase());
30173
+ if (!liveTable) {
30174
+ missingTables.push(table.table);
30175
+ continue;
30176
+ }
30177
+ const liveColumns = new Map(liveTable.columns.map((column) => [column.name.toLowerCase(), column]));
30178
+ for (const column of table.columns) {
30179
+ const liveColumn = liveColumns.get(column.name.toLowerCase());
30180
+ const declared = familyOfDeclared(column.type);
30181
+ if (!liveColumn) {
30182
+ missingColumns.push({ table: table.table, column: column.name, expected: declared });
30183
+ continue;
30184
+ }
30185
+ const actual = familyOfSqlType(liveColumn.sqlType);
30186
+ if (!satisfies(declared, actual)) {
30187
+ typeMismatches.push({
30188
+ table: table.table,
30189
+ column: column.name,
30190
+ expected: declared,
30191
+ actual,
30192
+ actualSqlType: liveColumn.sqlType
30193
+ });
30194
+ }
30195
+ }
30196
+ }
30197
+ return {
30198
+ missingTables,
30199
+ missingColumns,
30200
+ typeMismatches,
30201
+ clean: missingTables.length === 0 && missingColumns.length === 0 && typeMismatches.length === 0
30202
+ };
30203
+ }
30204
+ function formatSchemaDrift(drift) {
30205
+ if (drift.clean)
30206
+ return "";
30207
+ const lines = [];
30208
+ if (drift.missingTables.length > 0)
30209
+ lines.push(` Missing tables: ${drift.missingTables.join(", ")}`);
30210
+ for (const column of drift.missingColumns.slice(0, 10))
30211
+ lines.push(` \u2022 ${column.table}.${column.column} is missing (models declare ${column.expected})`);
30212
+ for (const mismatch of drift.typeMismatches.slice(0, 10))
30213
+ lines.push(` \u2022 ${mismatch.table}.${mismatch.column} is ${mismatch.actualSqlType}, models declare ${mismatch.expected}`);
30214
+ const shown = Math.min(drift.missingColumns.length, 10) + Math.min(drift.typeMismatches.length, 10);
30215
+ const total = drift.missingColumns.length + drift.typeMismatches.length;
30216
+ if (total > shown)
30217
+ lines.push(` + ${total - shown} more`);
30218
+ return [
30219
+ "The live schema does not match the models.",
30220
+ ...lines,
30221
+ "",
30222
+ "Migrations will NOT fix this on their own: the runner only applies files that have",
30223
+ "not run yet, and a corrected `CREATE TABLE IF NOT EXISTS` is a no-op against a table",
30224
+ "that already exists. Regenerate and apply the difference, or rebuild the schema."
30225
+ ].join(`
30226
+ `);
30227
+ }
30228
+ var init_schema_drift = __esm(() => {
30229
+ init_config();
30230
+ init_introspect_db();
30231
+ });
30232
+
30094
30233
  // src/index.ts
30095
30234
  var init_src2 = __esm(() => {
30096
30235
  init_model2();
30097
30236
  init_model2();
30237
+ init_schema_drift();
30098
30238
  init_actions();
30099
30239
  init_browser();
30100
30240
  init_client();
@@ -30139,6 +30279,7 @@ export {
30139
30279
  setConfig,
30140
30280
  seedModel,
30141
30281
  saveMigrationSnapshot,
30282
+ satisfies,
30142
30283
  runSeeders,
30143
30284
  runSeeder,
30144
30285
  runBenchmark,
@@ -30202,7 +30343,10 @@ export {
30202
30343
  relationDiagram as generateDiagram,
30203
30344
  generateAccessPatterns,
30204
30345
  freshDatabase,
30346
+ formatSchemaDrift,
30205
30347
  file,
30348
+ familyOfSqlType,
30349
+ familyOfDeclared,
30206
30350
  extractTableDefinition,
30207
30351
  extractModelSchema,
30208
30352
  extractInsertId,
@@ -30257,6 +30401,7 @@ export {
30257
30401
  buildDatabaseSchema,
30258
30402
  browserQuery,
30259
30403
  browserAuth,
30404
+ auditSchemaDrift,
30260
30405
  applySqliteBootstrapPragmas,
30261
30406
  VitessDriver,
30262
30407
  SingleTableRepository,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "bun-query-builder",
3
3
  "type": "module",
4
- "version": "0.2.35",
4
+ "version": "0.2.37",
5
5
  "description": "A simple yet performant query builder for TypeScript. Built with Bun.",
6
6
  "author": "Chris Breuer <chris@stacksjs.org>",
7
7
  "license": "MIT",