@bakery-framework/orm 2.0.0-alpha.6 → 2.0.0-alpha.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bakery-framework/orm",
3
- "version": "2.0.0-alpha.6",
3
+ "version": "2.0.0-alpha.8",
4
4
  "description": "Bakery database layer: adapters, query builder, schema sync and backup.",
5
5
  "keywords": [
6
6
  "bakery",
@@ -54,6 +54,6 @@
54
54
  "bun": ">=1.3.14"
55
55
  },
56
56
  "dependencies": {
57
- "@bakery-framework/core": "^2.0.0-alpha.6"
57
+ "@bakery-framework/core": "^2.0.0-alpha.8"
58
58
  }
59
59
  }
@@ -253,59 +253,62 @@ export class PGAdapter extends SQLAdapter {
253
253
  " WHERE table_schema NOT IN ('pg_catalog', 'information_schema')" +
254
254
  ' ORDER BY table_name',
255
255
  ).all()) as any[]
256
- const tablesWithDetails: SQLAdapter.TableDetails[] = []
257
-
258
- for (const t of res) {
259
- const qName = this.quote(t.name)
260
- const [countRes, cols, pkCols, idxs] = (await Promise.all([
261
- this.query(`SELECT COUNT(*)::int as count FROM ${qName}`).get(),
262
- this.query(
263
- 'SELECT column_name AS name, data_type AS type,' +
264
- ' is_nullable AS is_nullable' +
265
- ' FROM information_schema.columns' +
266
- ' WHERE table_name = ?' +
267
- " AND table_schema NOT IN ('pg_catalog', 'information_schema')" +
268
- ' ORDER BY ordinal_position',
269
- ).all(t.name),
270
- // `::regclass` casts a **string**, so the table name binds as a
271
- // parameter. It used to interpolate `qName` — a double-quoted
272
- // *identifier* — which Postgres reads as a column reference, so this
273
- // threw `column "<table>" does not exist` for every table and took the
274
- // whole of `getSchema()` down with it on this dialect.
275
- //
276
- // The quiet case was worse than the loud one: a table with a column of
277
- // the same name resolved, casting that column's *value* to a regclass
278
- // and reporting some other table's primary key as this one's.
279
- this.query(
280
- 'SELECT a.attname AS name' +
281
- ' FROM pg_index i' +
282
- ' JOIN pg_attribute a ON a.attrelid = i.indrelid' +
283
- ' AND a.attnum = ANY(i.indkey)' +
284
- ' WHERE i.indisprimary AND i.indrelid = ?::regclass',
285
- ).all(t.name),
286
- this.query(
287
- 'SELECT indexname AS name, indexdef AS def' +
288
- ' FROM pg_indexes' +
289
- " WHERE schemaname NOT IN ('pg_catalog', 'information_schema')" +
290
- ' AND tablename = ?',
291
- ).all(t.name),
292
- ])) as [SQLAdapter.CountRow, any[], any[], any[]]
293
- tablesWithDetails.push({
294
- name: t.name,
295
- rowCount: countRes?.count || 0,
296
- columns: cols.map(c => ({
297
- name: c.name,
298
- type: c.type,
299
- notnull: c.is_nullable === 'NO',
300
- pk: pkCols.some(pk => pk.name === c.name),
301
- })),
302
- indexes: idxs.map(i => ({
303
- name: i.name,
304
- unique: /UNIQUE/i.test(i.def),
305
- })),
306
- })
307
- }
308
- return tablesWithDetails
256
+ // One wave rather than N. The four queries per table were already
257
+ // concurrent; the loop around them awaited each table in turn, so a
258
+ // 20-table schema cost 20 sequential round-trip waves. See the note in
259
+ // sqlite.ts about the COUNT(*), which is a full scan here too.
260
+ return await Promise.all(
261
+ res.map(async t => {
262
+ const qName = this.quote(t.name)
263
+ const [countRes, cols, pkCols, idxs] = (await Promise.all([
264
+ this.query(`SELECT COUNT(*)::int as count FROM ${qName}`).get(),
265
+ this.query(
266
+ 'SELECT column_name AS name, data_type AS type,' +
267
+ ' is_nullable AS is_nullable' +
268
+ ' FROM information_schema.columns' +
269
+ ' WHERE table_name = ?' +
270
+ " AND table_schema NOT IN ('pg_catalog', 'information_schema')" +
271
+ ' ORDER BY ordinal_position',
272
+ ).all(t.name),
273
+ // `::regclass` casts a **string**, so the table name binds as a
274
+ // parameter. It used to interpolate `qName` a double-quoted
275
+ // *identifier* — which Postgres reads as a column reference, so this
276
+ // threw `column "<table>" does not exist` for every table and took the
277
+ // whole of `getSchema()` down with it on this dialect.
278
+ //
279
+ // The quiet case was worse than the loud one: a table with a column of
280
+ // the same name resolved, casting that column's *value* to a regclass
281
+ // and reporting some other table's primary key as this one's.
282
+ this.query(
283
+ 'SELECT a.attname AS name' +
284
+ ' FROM pg_index i' +
285
+ ' JOIN pg_attribute a ON a.attrelid = i.indrelid' +
286
+ ' AND a.attnum = ANY(i.indkey)' +
287
+ ' WHERE i.indisprimary AND i.indrelid = ?::regclass',
288
+ ).all(t.name),
289
+ this.query(
290
+ 'SELECT indexname AS name, indexdef AS def' +
291
+ ' FROM pg_indexes' +
292
+ " WHERE schemaname NOT IN ('pg_catalog', 'information_schema')" +
293
+ ' AND tablename = ?',
294
+ ).all(t.name),
295
+ ])) as [SQLAdapter.CountRow, any[], any[], any[]]
296
+ return {
297
+ name: t.name,
298
+ rowCount: countRes?.count || 0,
299
+ columns: cols.map(c => ({
300
+ name: c.name,
301
+ type: c.type,
302
+ notnull: c.is_nullable === 'NO',
303
+ pk: pkCols.some(pk => pk.name === c.name),
304
+ })),
305
+ indexes: idxs.map(i => ({
306
+ name: i.name,
307
+ unique: /UNIQUE/i.test(i.def),
308
+ })),
309
+ }
310
+ }),
311
+ )
309
312
  }
310
313
 
311
314
  async getData(
@@ -287,40 +287,50 @@ export class SQLiteAdapter extends SQLAdapter {
287
287
  'SELECT name FROM sqlite_master' +
288
288
  " WHERE type='table' AND name NOT LIKE 'sqlite_%'",
289
289
  ).all()) as SQLAdapter.NameRow[]
290
- const tablesWithDetails: SQLAdapter.TableDetails[] = []
291
- for (const t of res) {
292
- const tableName = this.quote(t.name)
293
-
294
- const [countRes, cols, idxs] = (await Promise.all([
295
- this.query(`SELECT COUNT(*) as count FROM ${tableName}`).get(),
296
- this.query(`PRAGMA table_info(${tableName})`).all(),
297
- this.query(`PRAGMA index_list(${tableName})`).all(),
298
- ])) as [SQLAdapter.CountRow, any[], any[]]
299
-
300
- tablesWithDetails.push({
301
- name: t.name,
302
- rowCount: countRes?.count || 0,
303
- columns: cols.map(c => ({
304
- name: c.name,
305
- type: c.type,
306
- notnull: c.notnull === 1,
307
- // `pk` is the column's **1-based position within the primary key**,
308
- // not a boolean — `PRAGMA table_info` reports 0 for "not part of the
309
- // key", 1 for the first key column, 2 for the second. So `=== 1`
310
- // reported a composite `PRIMARY KEY (a, b)` as a single-column key on
311
- // `a`, silently, and only on SQLite: MySQL reads `column_key = 'PRI'`
312
- // and Postgres reads `pg_index.indisprimary`, both of which are set on
313
- // every member.
314
- //
315
- // `parseConstraints` a few hundred lines down already had this right
316
- // (`col.pk > 0`), which is why `getConstraints()` disagreed with
317
- // `getSchema()` about the same table.
318
- pk: c.pk > 0,
319
- })),
320
- indexes: idxs.map(i => ({ name: i.name, unique: i.unique === 1 })),
321
- })
322
- }
323
- return tablesWithDetails
290
+ // Per table rather than per wave: the three queries below were already
291
+ // concurrent, but the loop around them awaited each table in turn, so a
292
+ // 20-table schema cost 20 sequential round-trip waves. They share nothing,
293
+ // so the whole fan-out is one wave now.
294
+ //
295
+ // Worth knowing before this grows: the `COUNT(*)` is a full scan on this
296
+ // dialect and on Postgres, and it is paid on every caller of `getSchema()`
297
+ // even though only a schema *listing* displays the number. Making it
298
+ // optional means `rowCount` has to admit it can be absent, which is a type
299
+ // change across every consumer — recorded rather than done here.
300
+ return await Promise.all(
301
+ res.map(async t => {
302
+ const tableName = this.quote(t.name)
303
+
304
+ const [countRes, cols, idxs] = (await Promise.all([
305
+ this.query(`SELECT COUNT(*) as count FROM ${tableName}`).get(),
306
+ this.query(`PRAGMA table_info(${tableName})`).all(),
307
+ this.query(`PRAGMA index_list(${tableName})`).all(),
308
+ ])) as [SQLAdapter.CountRow, any[], any[]]
309
+
310
+ return {
311
+ name: t.name,
312
+ rowCount: countRes?.count || 0,
313
+ columns: cols.map(c => ({
314
+ name: c.name,
315
+ type: c.type,
316
+ notnull: c.notnull === 1,
317
+ // `pk` is the column's **1-based position within the primary key**,
318
+ // not a boolean — `PRAGMA table_info` reports 0 for "not part of the
319
+ // key", 1 for the first key column, 2 for the second. So `=== 1`
320
+ // reported a composite `PRIMARY KEY (a, b)` as a single-column key on
321
+ // `a`, silently, and only on SQLite: MySQL reads `column_key = 'PRI'`
322
+ // and Postgres reads `pg_index.indisprimary`, both of which are set on
323
+ // every member.
324
+ //
325
+ // `parseConstraints` a few hundred lines down already had this right
326
+ // (`col.pk > 0`), which is why `getConstraints()` disagreed with
327
+ // `getSchema()` about the same table.
328
+ pk: c.pk > 0,
329
+ })),
330
+ indexes: idxs.map(i => ({ name: i.name, unique: i.unique === 1 })),
331
+ }
332
+ }),
333
+ )
324
334
  }
325
335
 
326
336
  async getData(