bun-query-builder 0.2.35 → 0.2.36

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
@@ -30092,10 +30092,139 @@ class Seeder {
30092
30092
  function defineSeeder(seederClass) {
30093
30093
  return seederClass;
30094
30094
  }
30095
+ // src/actions/schema-drift.ts
30096
+ function familyOfDeclared(type) {
30097
+ switch (type) {
30098
+ case "text":
30099
+ return "text";
30100
+ case "string":
30101
+ case "enum":
30102
+ return "varchar";
30103
+ case "integer":
30104
+ case "bigint":
30105
+ return "integer";
30106
+ case "float":
30107
+ case "double":
30108
+ case "decimal":
30109
+ return "fractional";
30110
+ case "boolean":
30111
+ return "boolean";
30112
+ case "date":
30113
+ case "datetime":
30114
+ case "timestamptz":
30115
+ return "date";
30116
+ case "json":
30117
+ return "json";
30118
+ default:
30119
+ return "other";
30120
+ }
30121
+ }
30122
+ function familyOfSqlType(sqlType) {
30123
+ const type = String(sqlType ?? "").toLowerCase().trim();
30124
+ if (/^(?:bool|boolean|bit)\b/.test(type) || type === "tinyint(1)")
30125
+ return "boolean";
30126
+ if (/^(?:json|jsonb)\b/.test(type))
30127
+ return "json";
30128
+ if (/^(?:bytea|blob|binary|varbinary)\b/.test(type))
30129
+ return "binary";
30130
+ if (/^(?:date|time|timestamp|datetime)\b/.test(type))
30131
+ return "date";
30132
+ if (/(?:^|\b)(?:varchar|character varying|nvarchar|char)\b/.test(type))
30133
+ return "varchar";
30134
+ if (/^(?:text|longtext|mediumtext|tinytext|clob|citext)\b/.test(type))
30135
+ return "text";
30136
+ if (/^(?:decimal|numeric|real|double|money)\b/.test(type) || /^float\d*\b/.test(type))
30137
+ return "fractional";
30138
+ if (/^(?:integer|bigint|smallint|tinyint|serial|bigserial|smallserial|mediumint)\b/.test(type) || /^int\d*\b/.test(type))
30139
+ return "integer";
30140
+ return "other";
30141
+ }
30142
+ function satisfies(declared, actual) {
30143
+ if (declared === actual)
30144
+ return true;
30145
+ if (declared === "varchar" && actual === "text")
30146
+ return true;
30147
+ if (declared === "integer" && actual === "fractional")
30148
+ return true;
30149
+ if (declared === "other" || actual === "other")
30150
+ return true;
30151
+ return false;
30152
+ }
30153
+ async function auditSchemaDrift(plan, options = {}) {
30154
+ const dialect = options.dialect ?? config5.dialect;
30155
+ const declaredTables = plan.tables.map((table) => table.table);
30156
+ const live = await introspectDatabase({ tables: declaredTables });
30157
+ const byTable = new Map(live.map((model) => [model.table.toLowerCase(), model]));
30158
+ const missingTables = [];
30159
+ const missingColumns = [];
30160
+ const typeMismatches = [];
30161
+ for (const table of plan.tables) {
30162
+ const liveTable = byTable.get(table.table.toLowerCase());
30163
+ if (!liveTable) {
30164
+ missingTables.push(table.table);
30165
+ continue;
30166
+ }
30167
+ const liveColumns = new Map(liveTable.columns.map((column) => [column.name.toLowerCase(), column]));
30168
+ for (const column of table.columns) {
30169
+ const liveColumn = liveColumns.get(column.name.toLowerCase());
30170
+ const declared = familyOfDeclared(column.type);
30171
+ if (!liveColumn) {
30172
+ missingColumns.push({ table: table.table, column: column.name, expected: declared });
30173
+ continue;
30174
+ }
30175
+ const actual = familyOfSqlType(liveColumn.sqlType);
30176
+ if (!satisfies(declared, actual)) {
30177
+ typeMismatches.push({
30178
+ table: table.table,
30179
+ column: column.name,
30180
+ expected: declared,
30181
+ actual,
30182
+ actualSqlType: liveColumn.sqlType
30183
+ });
30184
+ }
30185
+ }
30186
+ }
30187
+ return {
30188
+ missingTables,
30189
+ missingColumns,
30190
+ typeMismatches,
30191
+ clean: missingTables.length === 0 && missingColumns.length === 0 && typeMismatches.length === 0
30192
+ };
30193
+ }
30194
+ function formatSchemaDrift(drift) {
30195
+ if (drift.clean)
30196
+ return "";
30197
+ const lines = [];
30198
+ if (drift.missingTables.length > 0)
30199
+ lines.push(` Missing tables: ${drift.missingTables.join(", ")}`);
30200
+ for (const column of drift.missingColumns.slice(0, 10))
30201
+ lines.push(` \u2022 ${column.table}.${column.column} is missing (models declare ${column.expected})`);
30202
+ for (const mismatch of drift.typeMismatches.slice(0, 10))
30203
+ lines.push(` \u2022 ${mismatch.table}.${mismatch.column} is ${mismatch.actualSqlType}, models declare ${mismatch.expected}`);
30204
+ const shown = Math.min(drift.missingColumns.length, 10) + Math.min(drift.typeMismatches.length, 10);
30205
+ const total = drift.missingColumns.length + drift.typeMismatches.length;
30206
+ if (total > shown)
30207
+ lines.push(` + ${total - shown} more`);
30208
+ return [
30209
+ "The live schema does not match the models.",
30210
+ ...lines,
30211
+ "",
30212
+ "Migrations will NOT fix this on their own: the runner only applies files that have",
30213
+ "not run yet, and a corrected `CREATE TABLE IF NOT EXISTS` is a no-op against a table",
30214
+ "that already exists. Regenerate and apply the difference, or rebuild the schema."
30215
+ ].join(`
30216
+ `);
30217
+ }
30218
+ var init_schema_drift = __esm(() => {
30219
+ init_config();
30220
+ init_introspect_db();
30221
+ });
30222
+
30095
30223
  // src/index.ts
30096
30224
  var init_src2 = __esm(() => {
30097
30225
  init_model2();
30098
30226
  init_model2();
30227
+ init_schema_drift();
30099
30228
  init_actions();
30100
30229
  init_browser();
30101
30230
  init_client();
@@ -32189,7 +32318,7 @@ function getPrefix() {
32189
32318
  }
32190
32319
  var prefix = getPrefix();
32191
32320
  // package.json
32192
- var version2 = "0.2.35";
32321
+ var version2 = "0.2.36";
32193
32322
 
32194
32323
  // bin/cli.ts
32195
32324
  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
@@ -30091,10 +30091,139 @@ class Seeder {
30091
30091
  function defineSeeder(seederClass) {
30092
30092
  return seederClass;
30093
30093
  }
30094
+ // src/actions/schema-drift.ts
30095
+ function familyOfDeclared(type) {
30096
+ switch (type) {
30097
+ case "text":
30098
+ return "text";
30099
+ case "string":
30100
+ case "enum":
30101
+ return "varchar";
30102
+ case "integer":
30103
+ case "bigint":
30104
+ return "integer";
30105
+ case "float":
30106
+ case "double":
30107
+ case "decimal":
30108
+ return "fractional";
30109
+ case "boolean":
30110
+ return "boolean";
30111
+ case "date":
30112
+ case "datetime":
30113
+ case "timestamptz":
30114
+ return "date";
30115
+ case "json":
30116
+ return "json";
30117
+ default:
30118
+ return "other";
30119
+ }
30120
+ }
30121
+ function familyOfSqlType(sqlType) {
30122
+ const type = String(sqlType ?? "").toLowerCase().trim();
30123
+ if (/^(?:bool|boolean|bit)\b/.test(type) || type === "tinyint(1)")
30124
+ return "boolean";
30125
+ if (/^(?:json|jsonb)\b/.test(type))
30126
+ return "json";
30127
+ if (/^(?:bytea|blob|binary|varbinary)\b/.test(type))
30128
+ return "binary";
30129
+ if (/^(?:date|time|timestamp|datetime)\b/.test(type))
30130
+ return "date";
30131
+ if (/(?:^|\b)(?:varchar|character varying|nvarchar|char)\b/.test(type))
30132
+ return "varchar";
30133
+ if (/^(?:text|longtext|mediumtext|tinytext|clob|citext)\b/.test(type))
30134
+ return "text";
30135
+ if (/^(?:decimal|numeric|real|double|money)\b/.test(type) || /^float\d*\b/.test(type))
30136
+ return "fractional";
30137
+ if (/^(?:integer|bigint|smallint|tinyint|serial|bigserial|smallserial|mediumint)\b/.test(type) || /^int\d*\b/.test(type))
30138
+ return "integer";
30139
+ return "other";
30140
+ }
30141
+ function satisfies(declared, actual) {
30142
+ if (declared === actual)
30143
+ return true;
30144
+ if (declared === "varchar" && actual === "text")
30145
+ return true;
30146
+ if (declared === "integer" && actual === "fractional")
30147
+ return true;
30148
+ if (declared === "other" || actual === "other")
30149
+ return true;
30150
+ return false;
30151
+ }
30152
+ async function auditSchemaDrift(plan, options = {}) {
30153
+ const dialect = options.dialect ?? config5.dialect;
30154
+ const declaredTables = plan.tables.map((table) => table.table);
30155
+ const live = await introspectDatabase({ tables: declaredTables });
30156
+ const byTable = new Map(live.map((model) => [model.table.toLowerCase(), model]));
30157
+ const missingTables = [];
30158
+ const missingColumns = [];
30159
+ const typeMismatches = [];
30160
+ for (const table of plan.tables) {
30161
+ const liveTable = byTable.get(table.table.toLowerCase());
30162
+ if (!liveTable) {
30163
+ missingTables.push(table.table);
30164
+ continue;
30165
+ }
30166
+ const liveColumns = new Map(liveTable.columns.map((column) => [column.name.toLowerCase(), column]));
30167
+ for (const column of table.columns) {
30168
+ const liveColumn = liveColumns.get(column.name.toLowerCase());
30169
+ const declared = familyOfDeclared(column.type);
30170
+ if (!liveColumn) {
30171
+ missingColumns.push({ table: table.table, column: column.name, expected: declared });
30172
+ continue;
30173
+ }
30174
+ const actual = familyOfSqlType(liveColumn.sqlType);
30175
+ if (!satisfies(declared, actual)) {
30176
+ typeMismatches.push({
30177
+ table: table.table,
30178
+ column: column.name,
30179
+ expected: declared,
30180
+ actual,
30181
+ actualSqlType: liveColumn.sqlType
30182
+ });
30183
+ }
30184
+ }
30185
+ }
30186
+ return {
30187
+ missingTables,
30188
+ missingColumns,
30189
+ typeMismatches,
30190
+ clean: missingTables.length === 0 && missingColumns.length === 0 && typeMismatches.length === 0
30191
+ };
30192
+ }
30193
+ function formatSchemaDrift(drift) {
30194
+ if (drift.clean)
30195
+ return "";
30196
+ const lines = [];
30197
+ if (drift.missingTables.length > 0)
30198
+ lines.push(` Missing tables: ${drift.missingTables.join(", ")}`);
30199
+ for (const column of drift.missingColumns.slice(0, 10))
30200
+ lines.push(` \u2022 ${column.table}.${column.column} is missing (models declare ${column.expected})`);
30201
+ for (const mismatch of drift.typeMismatches.slice(0, 10))
30202
+ lines.push(` \u2022 ${mismatch.table}.${mismatch.column} is ${mismatch.actualSqlType}, models declare ${mismatch.expected}`);
30203
+ const shown = Math.min(drift.missingColumns.length, 10) + Math.min(drift.typeMismatches.length, 10);
30204
+ const total = drift.missingColumns.length + drift.typeMismatches.length;
30205
+ if (total > shown)
30206
+ lines.push(` + ${total - shown} more`);
30207
+ return [
30208
+ "The live schema does not match the models.",
30209
+ ...lines,
30210
+ "",
30211
+ "Migrations will NOT fix this on their own: the runner only applies files that have",
30212
+ "not run yet, and a corrected `CREATE TABLE IF NOT EXISTS` is a no-op against a table",
30213
+ "that already exists. Regenerate and apply the difference, or rebuild the schema."
30214
+ ].join(`
30215
+ `);
30216
+ }
30217
+ var init_schema_drift = __esm(() => {
30218
+ init_config();
30219
+ init_introspect_db();
30220
+ });
30221
+
30094
30222
  // src/index.ts
30095
30223
  var init_src2 = __esm(() => {
30096
30224
  init_model2();
30097
30225
  init_model2();
30226
+ init_schema_drift();
30098
30227
  init_actions();
30099
30228
  init_browser();
30100
30229
  init_client();
@@ -30139,6 +30268,7 @@ export {
30139
30268
  setConfig,
30140
30269
  seedModel,
30141
30270
  saveMigrationSnapshot,
30271
+ satisfies,
30142
30272
  runSeeders,
30143
30273
  runSeeder,
30144
30274
  runBenchmark,
@@ -30202,7 +30332,10 @@ export {
30202
30332
  relationDiagram as generateDiagram,
30203
30333
  generateAccessPatterns,
30204
30334
  freshDatabase,
30335
+ formatSchemaDrift,
30205
30336
  file,
30337
+ familyOfSqlType,
30338
+ familyOfDeclared,
30206
30339
  extractTableDefinition,
30207
30340
  extractModelSchema,
30208
30341
  extractInsertId,
@@ -30257,6 +30390,7 @@ export {
30257
30390
  buildDatabaseSchema,
30258
30391
  browserQuery,
30259
30392
  browserAuth,
30393
+ auditSchemaDrift,
30260
30394
  applySqliteBootstrapPragmas,
30261
30395
  VitessDriver,
30262
30396
  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.36",
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",