@query-doctor/core 0.7.2 → 0.8.0-rc.10

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/dist/index.cjs CHANGED
@@ -1817,14 +1817,18 @@ const ExportedStatsStatistics = zod.z.object({
1817
1817
  });
1818
1818
  const ExportedStatsColumns = zod.z.object({
1819
1819
  columnName: zod.z.string(),
1820
+ attlen: zod.z.number().nullable(),
1820
1821
  stats: ExportedStatsStatistics.nullable()
1821
1822
  });
1822
1823
  const ExportedStatsIndex = zod.z.object({
1823
1824
  indexName: zod.z.string(),
1825
+ amname: zod.z.string().default("btree"),
1824
1826
  relpages: zod.z.number(),
1825
1827
  reltuples: zod.z.number(),
1826
1828
  relallvisible: zod.z.number(),
1827
- relallfrozen: zod.z.number().optional()
1829
+ relallfrozen: zod.z.number().optional(),
1830
+ fillfactor: zod.z.number().default(90),
1831
+ columns: zod.z.array(zod.z.object({ attlen: zod.z.number().nullable() })).default([])
1828
1832
  });
1829
1833
  const ExportedStatsV1 = zod.z.object({
1830
1834
  tableName: zod.z.string(),
@@ -1833,14 +1837,13 @@ const ExportedStatsV1 = zod.z.object({
1833
1837
  reltuples: zod.z.number(),
1834
1838
  relallvisible: zod.z.number(),
1835
1839
  relallfrozen: zod.z.number().optional(),
1836
- columns: zod.z.array(ExportedStatsColumns).nullable(),
1840
+ columns: zod.z.array(ExportedStatsColumns).default([]),
1837
1841
  indexes: zod.z.array(ExportedStatsIndex)
1838
1842
  });
1839
1843
  const ExportedStats = zod.z.union([ExportedStatsV1]);
1840
1844
  const StatisticsMode = zod.z.discriminatedUnion("kind", [zod.z.object({
1841
1845
  kind: zod.z.literal("fromAssumption"),
1842
- reltuples: zod.z.number().min(0),
1843
- relpages: zod.z.number().min(0)
1846
+ reltuples: zod.z.number().min(0)
1844
1847
  }), zod.z.object({
1845
1848
  kind: zod.z.literal("fromStatisticsExport"),
1846
1849
  stats: zod.z.array(ExportedStats),
@@ -1848,6 +1851,19 @@ const StatisticsMode = zod.z.discriminatedUnion("kind", [zod.z.object({
1848
1851
  })]);
1849
1852
  const DEFAULT_RELTUPLES = 1e4;
1850
1853
  const DEFAULT_RELPAGES = 1;
1854
+ const DEFAULT_PAGE_SIZE = 2 ** 13;
1855
+ function estimateStawidth(col) {
1856
+ return col.attlen ?? 32;
1857
+ }
1858
+ function estimateRelpages(reltuples, columns) {
1859
+ const rowWidth = columns.reduce((sum, col) => sum + estimateStawidth(col), 0) + 27;
1860
+ return Math.ceil(reltuples * rowWidth / DEFAULT_PAGE_SIZE);
1861
+ }
1862
+ function estimateIndexRelpages(reltuples, columns, fillfactor, amname, tableRelpages) {
1863
+ if (amname === "gin") return Math.ceil(tableRelpages * .3);
1864
+ const keyWidth = columns.reduce((sum, col) => sum + estimateStawidth(col) + 16, 0);
1865
+ return Math.ceil(reltuples * keyWidth / DEFAULT_PAGE_SIZE / fillfactor);
1866
+ }
1851
1867
  var Statistics = class Statistics {
1852
1868
  constructor(db, postgresVersion, ownMetadata, statsMode) {
1853
1869
  this.db = db;
@@ -1860,11 +1876,10 @@ var Statistics = class Statistics {
1860
1876
  if (statsMode.kind === "fromStatisticsExport") this.exportedMetadata = statsMode.stats;
1861
1877
  } else this.mode = Statistics.defaultStatsMode;
1862
1878
  }
1863
- static statsModeFromAssumption({ reltuples, relpages }) {
1879
+ static statsModeFromAssumption({ reltuples }) {
1864
1880
  return {
1865
1881
  kind: "fromAssumption",
1866
- reltuples,
1867
- relpages
1882
+ reltuples
1868
1883
  };
1869
1884
  }
1870
1885
  /**
@@ -1928,17 +1943,17 @@ var Statistics = class Statistics {
1928
1943
  const columnStatsValues = [];
1929
1944
  for (const table of this.ownMetadata) {
1930
1945
  const target = (this.exportedMetadata?.find((m) => m.tableName === table.tableName && m.schemaName === table.schemaName))?.columns ?? table.columns;
1931
- if (!target) continue;
1932
1946
  for (const column of target) {
1933
1947
  const { stats } = column;
1934
1948
  if (!stats || this.mode.kind === "fromAssumption") {
1949
+ const stawidth = stats?.stawidth || estimateStawidth(column);
1935
1950
  columnStatsValues.push({
1936
1951
  schema_name: table.schemaName,
1937
1952
  table_name: table.tableName,
1938
1953
  column_name: column.columnName,
1939
1954
  stainherit: false,
1940
1955
  stanullfrac: .04,
1941
- stawidth: stats?.stawidth ?? 32,
1956
+ stawidth,
1942
1957
  stadistinct: -.9,
1943
1958
  stakind1: 0,
1944
1959
  stakind2: 0,
@@ -2239,7 +2254,7 @@ var Statistics = class Statistics {
2239
2254
  let relallfrozen;
2240
2255
  if (this.mode.kind === "fromAssumption") {
2241
2256
  reltuples = this.mode.reltuples;
2242
- relpages = this.mode.relpages;
2257
+ relpages = estimateRelpages(reltuples, table.columns);
2243
2258
  } else if (targetTable) {
2244
2259
  reltuples = targetTable.reltuples;
2245
2260
  relpages = targetTable.relpages;
@@ -2258,15 +2273,18 @@ var Statistics = class Statistics {
2258
2273
  relallfrozen,
2259
2274
  relallvisible
2260
2275
  });
2261
- if (this.mode.kind === "fromAssumption") for (const index of table.indexes) reltuplesValues.push({
2262
- relname: index.indexName,
2263
- schema_name: table.schemaName,
2264
- reltuples: this.mode.reltuples,
2265
- relpages: Math.ceil(this.mode.relpages * .2),
2266
- relallfrozen: 0,
2267
- relallvisible: this.mode.relpages
2268
- });
2269
- if (targetTable) for (const index of targetTable.indexes) reltuplesValues.push({
2276
+ if (this.mode.kind === "fromAssumption") for (const index of table.indexes) {
2277
+ const indexRelpages = estimateIndexRelpages(this.mode.reltuples, index.columns, index.fillfactor / 100, index.amname, relpages);
2278
+ reltuplesValues.push({
2279
+ relname: index.indexName,
2280
+ schema_name: table.schemaName,
2281
+ reltuples: this.mode.reltuples,
2282
+ relpages: indexRelpages,
2283
+ relallfrozen: 0,
2284
+ relallvisible: indexRelpages
2285
+ });
2286
+ }
2287
+ else if (targetTable) for (const index of targetTable.indexes) reltuplesValues.push({
2270
2288
  relname: index.indexName,
2271
2289
  schema_name: targetTable.schemaName,
2272
2290
  reltuples: index.reltuples,
@@ -2309,19 +2327,19 @@ var Statistics = class Statistics {
2309
2327
  static async dumpStats(db, postgresVersion, kind) {
2310
2328
  const fullDump = kind === "full";
2311
2329
  console.log(`dumping stats for postgres ${(0, colorette.gray)(postgresVersion)}`);
2312
- return (await db.exec(`
2330
+ const stats = await db.exec(`
2313
2331
  WITH table_columns AS (
2314
2332
  SELECT
2315
- c.table_name,
2316
- c.table_schema,
2333
+ cl.relname,
2334
+ n.nspname,
2317
2335
  cl.reltuples,
2318
2336
  cl.relpages,
2319
2337
  cl.relallvisible,
2320
2338
  -- cl.relallfrozen,
2321
- n.nspname AS schema_name,
2322
2339
  json_agg(
2323
2340
  json_build_object(
2324
- 'columnName', c.column_name,
2341
+ 'columnName', a.attname,
2342
+ 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END,
2325
2343
  'stats', (
2326
2344
  SELECT json_build_object(
2327
2345
  'starelid', s.starelid,
@@ -2344,21 +2362,15 @@ var Statistics = class Statistics {
2344
2362
  WHERE s.starelid = a.attrelid AND s.staattnum = a.attnum
2345
2363
  )
2346
2364
  )
2347
- ORDER BY c.ordinal_position
2365
+ ORDER BY a.attnum
2348
2366
  ) AS columns
2349
- FROM information_schema.columns c
2350
- JOIN pg_attribute a
2351
- ON a.attrelid = (quote_ident(c.table_schema) || '.' || quote_ident(c.table_name))::regclass
2352
- AND a.attname = c.column_name
2353
- JOIN pg_class cl
2354
- ON cl.oid = a.attrelid
2355
- JOIN pg_namespace n
2356
- ON n.oid = cl.relnamespace
2357
- WHERE c.table_name NOT LIKE 'pg_%'
2358
- AND n.nspname <> 'information_schema'
2359
- AND n.nspname NOT IN ('tiger', 'tiger_data', 'topology')
2360
- AND c.table_name NOT IN ('pg_stat_statements', 'pg_stat_statements_info')
2361
- GROUP BY c.table_name, c.table_schema, cl.reltuples, cl.relpages, cl.relallvisible, n.nspname
2367
+ FROM pg_class cl
2368
+ JOIN pg_namespace n ON n.oid = cl.relnamespace
2369
+ JOIN pg_attribute a ON a.attrelid = cl.oid AND a.attnum > 0 AND NOT a.attisdropped
2370
+ WHERE cl.relkind = 'r'
2371
+ AND n.nspname NOT IN ('pg_catalog', 'information_schema', 'tiger', 'tiger_data', 'topology')
2372
+ AND cl.relname NOT IN ('pg_stat_statements', 'pg_stat_statements_info')
2373
+ GROUP BY cl.relname, n.nspname, cl.reltuples, cl.relpages, cl.relallvisible
2362
2374
  ),
2363
2375
  table_indexes AS (
2364
2376
  SELECT
@@ -2366,15 +2378,37 @@ var Statistics = class Statistics {
2366
2378
  json_agg(
2367
2379
  json_build_object(
2368
2380
  'indexName', i.relname,
2381
+ 'amname', am.amname,
2369
2382
  'reltuples', i.reltuples,
2370
2383
  'relpages', i.relpages,
2371
- 'relallvisible', i.relallvisible
2372
- -- 'relallfrozen', i.relallfrozen
2384
+ 'relallvisible', i.relallvisible,
2385
+ -- 'relallfrozen', i.relallfrozen,
2386
+ 'fillfactor', COALESCE(
2387
+ (
2388
+ SELECT (regexp_match(opt, 'fillfactor=(\\d+)'))[1]::integer
2389
+ FROM unnest(i.reloptions) AS opt
2390
+ WHERE opt LIKE 'fillfactor=%'
2391
+ LIMIT 1
2392
+ ),
2393
+ 90
2394
+ ),
2395
+ 'columns', COALESCE(
2396
+ (
2397
+ SELECT json_agg(json_build_object(
2398
+ 'attlen', CASE WHEN a.attlen > 0 THEN a.attlen ELSE NULL END
2399
+ ) ORDER BY col_pos.ord)
2400
+ FROM unnest(ix.indkey) WITH ORDINALITY AS col_pos(attnum, ord)
2401
+ JOIN pg_attribute a ON a.attrelid = t.oid AND a.attnum = col_pos.attnum
2402
+ WHERE col_pos.attnum > 0
2403
+ ),
2404
+ '[]'::json
2405
+ )
2373
2406
  )
2374
2407
  ) AS indexes
2375
2408
  FROM pg_class t
2376
2409
  JOIN pg_index ix ON ix.indrelid = t.oid
2377
2410
  JOIN pg_class i ON i.oid = ix.indexrelid
2411
+ JOIN pg_am am ON am.oid = i.relam
2378
2412
  JOIN pg_namespace n ON n.oid = t.relnamespace
2379
2413
  WHERE t.relname NOT LIKE 'pg_%'
2380
2414
  AND n.nspname <> 'information_schema'
@@ -2383,20 +2417,21 @@ var Statistics = class Statistics {
2383
2417
  )
2384
2418
  SELECT json_agg(
2385
2419
  json_build_object(
2386
- 'tableName', tc.table_name,
2387
- 'schemaName', tc.table_schema,
2420
+ 'tableName', tc.relname,
2421
+ 'schemaName', tc.nspname,
2388
2422
  'reltuples', tc.reltuples,
2389
2423
  'relpages', tc.relpages,
2390
2424
  'relallvisible', tc.relallvisible,
2391
2425
  -- 'relallfrozen', tc.relallfrozen,
2392
- 'columns', tc.columns,
2426
+ 'columns', COALESCE(tc.columns, '[]'::json),
2393
2427
  'indexes', COALESCE(ti.indexes, '[]'::json)
2394
2428
  )
2395
2429
  )
2396
2430
  FROM table_columns tc
2397
2431
  LEFT JOIN table_indexes ti
2398
- ON ti.table_name = tc.table_name;
2399
- `, [fullDump]))[0].json_agg;
2432
+ ON ti.table_name = tc.relname;
2433
+ `, [fullDump]);
2434
+ return zod.z.array(ExportedStats).parse(stats[0].json_agg);
2400
2435
  }
2401
2436
  /**
2402
2437
  * Returns all indexes in the database.
@@ -2491,6 +2526,15 @@ var PssRewriter = class {
2491
2526
  }
2492
2527
  };
2493
2528
  //#endregion
2529
+ //#region src/sentry.ts
2530
+ function deriveSentryEnvironment(url) {
2531
+ if (!url) return "local";
2532
+ if (url.includes("next")) return "next";
2533
+ if (url.includes("staging")) return "staging";
2534
+ if (url.includes("querydoctor.com")) return "production";
2535
+ return "local";
2536
+ }
2537
+ //#endregion
2494
2538
  exports.Analyzer = Analyzer;
2495
2539
  exports.ExportedStats = ExportedStats;
2496
2540
  exports.ExportedStatsColumns = ExportedStatsColumns;
@@ -2508,6 +2552,7 @@ exports.SKIP = SKIP;
2508
2552
  exports.Statistics = Statistics;
2509
2553
  exports.StatisticsMode = StatisticsMode;
2510
2554
  exports.StatisticsSource = StatisticsSource;
2555
+ exports.deriveSentryEnvironment = deriveSentryEnvironment;
2511
2556
  exports.dropIndex = dropIndex;
2512
2557
  exports.ignoredIdentifier = ignoredIdentifier;
2513
2558
  exports.isIndexProbablyDroppable = isIndexProbablyDroppable;