mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-07 04:12:04 +00:00
scripts/bench/seed-large-account.ts (moved from scripts/, extended) writes one synthetic account of ten years: 1,265,561 measurement rows across the main types and five sources (Apple Health dense spot rows, Withings blood pressure and scale, a weekly manual weight, three years of Oura nightly values, 90 days of Nightscout CGM), `stats:` and per-sample cumulative rows, nightly sleep stages, 3,020 workouts, 1,095 mood entries and three medications with 3,285 intake events. Everything goes in through set-based INSERT … SELECT generate_series; values and timestamps are functions of the row index and `--anchor`, so a run is reproducible. The rollup tier is folded with recomputeUserRollups, as the boot backfill does, and the tables are analysed. About 80 s on the Mac mini. scripts/bench/run.ts boots the production build once with every role (so the queues exist), then measures against a web-only process under the production heap ceiling: p50, p95 and max latency and SQL statements per request (pg_stat_statements, read once nothing is in flight and the count holds still) for /api/auth/me, the dashboard summary and snapshot, the pulse series over 30, 365 and 3650 days, the day view, insights health status and correlations, each cold (caches evicted by a one-row manual-sync ingest before every request) and warm, plus a 5,000-row batch ingest as ten requests of 500. It writes JSON and a markdown table. docs/contributing/benchmarks.md describes the setup against a throwaway Postgres in Docker with the production pg_stat_statements settings.
660 lines
18 KiB
TypeScript
660 lines
18 KiB
TypeScript
/**
|
|
* Seed one synthetic account the size of a heavy real one, for the benchmark
|
|
* harness (`scripts/bench/run.ts`) and for reproducing the maintenance jobs:
|
|
* about 1.26 million measurement rows over ten years, plus workouts, mood
|
|
* entries and medications with their intake history.
|
|
*
|
|
* DATABASE_URL=postgresql://… pnpm dlx tsx scripts/bench/seed-large-account.ts \
|
|
* [--user bench-large] [--years 10] [--anchor 2026-10-22] [--reset]
|
|
* [--no-rollups]
|
|
*
|
|
* Never point this at a database that holds real data: it inserts rows under
|
|
* the given user id and, with `--reset`, deletes that user (and, through the
|
|
* cascades, everything it owns) first.
|
|
*
|
|
* Deterministic: every value and timestamp is a function of the row index and
|
|
* the anchor day (`--anchor`, UTC midnight; default today), and ids are
|
|
* derived from the user id, the series and the index. Two runs with the same
|
|
* arguments write the same rows.
|
|
*
|
|
* The shape follows what the integrations write, with overlapping sources so
|
|
* the canonical-source pick has work to do:
|
|
*
|
|
* - Apple Health dense spot rows (heart rate about every ten minutes, HRV,
|
|
* SpO2, respiratory rate, gait metrics, audio exposure) keyed by a hashed
|
|
* sample id;
|
|
* - Withings morning and evening blood pressure with its pulse, and daily
|
|
* weight, body fat and muscle mass; a weekly manual weight beside it;
|
|
* - Oura nightly resting heart rate and RMSSD for the last three years,
|
|
* beside Apple Health's resting heart rate;
|
|
* - Nightscout CGM glucose every five minutes for the last 90 days;
|
|
* - one `stats:` daily row per cumulative type and day (the export import)
|
|
* and per-sample cumulative rows with opaque ids for the last 90 days
|
|
* (the iOS sync);
|
|
* - sleep-stage segments every night;
|
|
* - about 3,000 workouts, a mood entry a day for three years, and three
|
|
* daily medications with their intake events for three years.
|
|
*
|
|
* Everything goes in through set-based `INSERT … SELECT … generate_series`,
|
|
* so a million rows take seconds rather than a million round trips. The
|
|
* rollup tier is then folded with `recomputeUserRollups`, the same path the
|
|
* boot backfill takes for an imported account, so reads see the state a real
|
|
* account is in. `ANALYZE` runs at the end: a planner reading pre-seed
|
|
* statistics picks plans no production table of this size would get.
|
|
*/
|
|
import { prisma } from "@/lib/db";
|
|
import {
|
|
ALL_GRANULARITIES,
|
|
recomputeUserRollups,
|
|
} from "@/lib/rollups/measurement-rollups";
|
|
|
|
function arg(name: string, fallback: string): string {
|
|
const i = process.argv.indexOf(`--${name}`);
|
|
return i >= 0 && process.argv[i + 1] ? process.argv[i + 1]! : fallback;
|
|
}
|
|
|
|
const USER_ID = arg("user", "bench-large");
|
|
const YEARS = Number(arg("years", "10"));
|
|
const ANCHOR = arg("anchor", new Date().toISOString().slice(0, 10));
|
|
const RESET = process.argv.includes("--reset");
|
|
const ROLLUPS = !process.argv.includes("--no-rollups");
|
|
const ZONE = "Europe/Berlin";
|
|
|
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(ANCHOR)) {
|
|
throw new Error(`--anchor must be YYYY-MM-DD, got ${ANCHOR}`);
|
|
}
|
|
|
|
/**
|
|
* A spot series: rows evenly spaced over the last `days` days before the
|
|
* anchor. `perDay` above one spaces rows through the day; `everyDays` above
|
|
* one writes one row every that many days at `offsetSec` past midnight UTC.
|
|
*/
|
|
interface SpotSeries {
|
|
type: string;
|
|
unit: string;
|
|
source: string;
|
|
base: number;
|
|
spread: number;
|
|
days: number;
|
|
perDay?: number;
|
|
everyDays?: number;
|
|
offsetSec?: number;
|
|
/** externalId prefix; `stats:` rows are generated separately. */
|
|
idPrefix: string;
|
|
}
|
|
|
|
async function insertSpot(s: SpotSeries): Promise<number> {
|
|
const step = s.everyDays
|
|
? s.everyDays * 86_400
|
|
: Math.floor(86_400 / (s.perDay ?? 1));
|
|
const count = Math.floor((s.days * 86_400) / step);
|
|
// Jitter stays below the step so every measured_at is distinct per type
|
|
// and source (the (user, type, measured_at, source, sleep_stage) unique
|
|
// index). Series that share a step and offset (BP systolic, diastolic and
|
|
// the Withings pulse) share their timestamps, as a cuff reading does.
|
|
const jitter = Math.max(1, Math.min(59, step - 1));
|
|
return prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO measurements
|
|
(id, user_id, type, value, unit, source, measured_at, external_id, created_at, updated_at)
|
|
SELECT
|
|
'seed-' || $1 || '-' || $2 || '-' || $10 || '-' || g,
|
|
$1,
|
|
$2::measurement_type,
|
|
round(($3 + $4 * sin(g / 37.0) + ($4 / 2) * ((g * 7919) % 100) / 100.0)::numeric, 2),
|
|
$5,
|
|
$10::measurement_source,
|
|
$11::date::timestamp - make_interval(secs => $6 * $7::bigint)
|
|
+ make_interval(secs => g * $6 + $12 + (g * 31) % $8),
|
|
$9 || md5($2 || $10 || g::text),
|
|
now(), now()
|
|
FROM generate_series(0, $7::bigint - 1) AS g
|
|
`,
|
|
USER_ID,
|
|
s.type,
|
|
s.base,
|
|
s.spread,
|
|
s.unit,
|
|
step,
|
|
count,
|
|
jitter,
|
|
s.idPrefix,
|
|
s.source,
|
|
ANCHOR,
|
|
s.offsetSec ?? 0,
|
|
);
|
|
}
|
|
|
|
function log(label: string, n: number): void {
|
|
console.log(`${label.padEnd(40)} ${String(n).padStart(9)}`);
|
|
}
|
|
|
|
async function seedMeasurements(days: number): Promise<number> {
|
|
const recent = Math.min(days, 90);
|
|
const threeYears = Math.min(days, 3 * 365);
|
|
const spot: SpotSeries[] = [
|
|
// Apple Health, the dense tier.
|
|
{
|
|
type: "PULSE",
|
|
unit: "bpm",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 147,
|
|
base: 62,
|
|
spread: 25,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "HEART_RATE_VARIABILITY",
|
|
unit: "ms",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 12,
|
|
base: 45,
|
|
spread: 20,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "OXYGEN_SATURATION",
|
|
unit: "%",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 24,
|
|
base: 96,
|
|
spread: 2,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "RESPIRATORY_RATE",
|
|
unit: "breaths/min",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 24,
|
|
base: 14,
|
|
spread: 3,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "WALKING_SPEED",
|
|
unit: "m/s",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 12,
|
|
base: 1.3,
|
|
spread: 0.2,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "WALKING_STEP_LENGTH",
|
|
unit: "m",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 12,
|
|
base: 0.72,
|
|
spread: 0.06,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "WALKING_ASYMMETRY",
|
|
unit: "%",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 12,
|
|
base: 3,
|
|
spread: 2,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "WALKING_DOUBLE_SUPPORT",
|
|
unit: "%",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 12,
|
|
base: 28,
|
|
spread: 3,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "AUDIO_EXPOSURE_ENV",
|
|
unit: "dBA",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 24,
|
|
base: 62,
|
|
spread: 12,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "RESTING_HEART_RATE",
|
|
unit: "bpm",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 1,
|
|
offsetSec: 6 * 3600,
|
|
base: 56,
|
|
spread: 4,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
{
|
|
type: "TIME_IN_DAYLIGHT",
|
|
unit: "minutes",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 1,
|
|
offsetSec: 18 * 3600,
|
|
base: 60,
|
|
spread: 40,
|
|
days,
|
|
idPrefix: "hk-",
|
|
},
|
|
// The iOS sync's per-sample cumulative rows: dense, recent, opaque ids.
|
|
{
|
|
type: "ACTIVITY_STEPS",
|
|
unit: "steps",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 100,
|
|
base: 90,
|
|
spread: 60,
|
|
days: recent,
|
|
idPrefix: "uuid-",
|
|
},
|
|
{
|
|
type: "ACTIVE_ENERGY_BURNED",
|
|
unit: "kcal",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 400,
|
|
base: 1.2,
|
|
spread: 0.9,
|
|
days: recent,
|
|
idPrefix: "uuid-",
|
|
},
|
|
{
|
|
type: "WALKING_RUNNING_DISTANCE",
|
|
unit: "m",
|
|
source: "APPLE_HEALTH",
|
|
perDay: 100,
|
|
base: 70,
|
|
spread: 50,
|
|
days: recent,
|
|
idPrefix: "uuid-",
|
|
},
|
|
// Withings: a cuff reading morning and evening, and the scale.
|
|
{
|
|
type: "BLOOD_PRESSURE_SYS",
|
|
unit: "mmHg",
|
|
source: "WITHINGS",
|
|
perDay: 2,
|
|
offsetSec: 6 * 3600,
|
|
base: 126,
|
|
spread: 10,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
{
|
|
type: "BLOOD_PRESSURE_DIA",
|
|
unit: "mmHg",
|
|
source: "WITHINGS",
|
|
perDay: 2,
|
|
offsetSec: 6 * 3600,
|
|
base: 81,
|
|
spread: 7,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
{
|
|
type: "PULSE",
|
|
unit: "bpm",
|
|
source: "WITHINGS",
|
|
perDay: 2,
|
|
offsetSec: 6 * 3600,
|
|
base: 66,
|
|
spread: 8,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
{
|
|
type: "WEIGHT",
|
|
unit: "kg",
|
|
source: "WITHINGS",
|
|
perDay: 1,
|
|
offsetSec: 5 * 3600,
|
|
base: 82,
|
|
spread: 1.5,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
{
|
|
type: "BODY_FAT",
|
|
unit: "%",
|
|
source: "WITHINGS",
|
|
perDay: 1,
|
|
offsetSec: 5 * 3600,
|
|
base: 22,
|
|
spread: 2,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
{
|
|
type: "MUSCLE_MASS",
|
|
unit: "kg",
|
|
source: "WITHINGS",
|
|
perDay: 1,
|
|
offsetSec: 5 * 3600,
|
|
base: 61,
|
|
spread: 1,
|
|
days,
|
|
idPrefix: "wi-",
|
|
},
|
|
// A weekly manual weight beside the scale.
|
|
{
|
|
type: "WEIGHT",
|
|
unit: "kg",
|
|
source: "MANUAL",
|
|
everyDays: 7,
|
|
offsetSec: 19 * 3600,
|
|
base: 82.4,
|
|
spread: 1.5,
|
|
days,
|
|
idPrefix: "man-",
|
|
},
|
|
// Oura, nightly, the last three years.
|
|
{
|
|
type: "RESTING_HEART_RATE",
|
|
unit: "bpm",
|
|
source: "OURA",
|
|
perDay: 1,
|
|
offsetSec: 4 * 3600,
|
|
base: 54,
|
|
spread: 4,
|
|
days: threeYears,
|
|
idPrefix: "oura-",
|
|
},
|
|
{
|
|
type: "HRV_RMSSD",
|
|
unit: "ms",
|
|
source: "OURA",
|
|
perDay: 1,
|
|
offsetSec: 4 * 3600,
|
|
base: 48,
|
|
spread: 15,
|
|
days: threeYears,
|
|
idPrefix: "oura-",
|
|
},
|
|
// Nightscout CGM, every five minutes, the last 90 days.
|
|
{
|
|
type: "BLOOD_GLUCOSE",
|
|
unit: "mg/dL",
|
|
source: "NIGHTSCOUT",
|
|
perDay: 288,
|
|
base: 105,
|
|
spread: 30,
|
|
days: recent,
|
|
idPrefix: "ns-",
|
|
},
|
|
];
|
|
|
|
let total = 0;
|
|
for (const s of spot) {
|
|
const n = await insertSpot(s);
|
|
total += n;
|
|
log(`${s.type} (${s.source})`, n);
|
|
}
|
|
|
|
// One export-estimate `stats:` row per cumulative type and local day, at
|
|
// local noon — what `flushCumulativeBuckets` in the export import writes —
|
|
// for every day older than the per-sample window.
|
|
for (const [type, hk, unit, base] of [
|
|
["ACTIVITY_STEPS", "HKQuantityTypeIdentifierStepCount", "steps", 8000],
|
|
[
|
|
"ACTIVE_ENERGY_BURNED",
|
|
"HKQuantityTypeIdentifierActiveEnergyBurned",
|
|
"kcal",
|
|
550,
|
|
],
|
|
[
|
|
"WALKING_RUNNING_DISTANCE",
|
|
"HKQuantityTypeIdentifierDistanceWalkingRunning",
|
|
"m",
|
|
6000,
|
|
],
|
|
["FLIGHTS_CLIMBED", "HKQuantityTypeIdentifierFlightsClimbed", "flights", 9],
|
|
] as const) {
|
|
const n = await prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO measurements
|
|
(id, user_id, type, value, unit, source, measured_at, external_id,
|
|
aggregation_provenance, created_at, updated_at)
|
|
SELECT
|
|
'seed-' || $1 || '-' || $2 || '-stats-' || d,
|
|
$1, $2::measurement_type, $5 + (d * 7919) % ($5 / 2), $4, 'APPLE_HEALTH',
|
|
(($7::date - d)::timestamp + interval '12 hours')
|
|
AT TIME ZONE $8 AT TIME ZONE 'UTC',
|
|
'stats:' || $3 || ':' || to_char($7::date - d, 'YYYY-MM-DD'),
|
|
'EXPORT_XML_SOURCE_MAX', now(), now()
|
|
FROM generate_series(91, $6::int) AS d
|
|
`,
|
|
USER_ID,
|
|
type,
|
|
hk,
|
|
unit,
|
|
base,
|
|
days,
|
|
ANCHOR,
|
|
ZONE,
|
|
);
|
|
total += n;
|
|
log(`${type} (stats:)`, n);
|
|
}
|
|
|
|
// Sleep: 30 segments a night from 23:00 local, 16 minutes apart, rotating
|
|
// through the stages. Distinct measured_at per segment.
|
|
const sleep = await prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO measurements
|
|
(id, user_id, type, value, unit, source, measured_at, external_id,
|
|
sleep_stage, created_at, updated_at)
|
|
SELECT
|
|
'seed-' || $1 || '-sleep-' || d || '-' || s,
|
|
$1, 'SLEEP_DURATION', 16, 'minutes', 'APPLE_HEALTH',
|
|
(($3::date - d)::timestamp - interval '1 hour' + s * interval '16 minutes')
|
|
AT TIME ZONE $4 AT TIME ZONE 'UTC',
|
|
'hk-' || md5('sleep' || d || '-' || s),
|
|
(ARRAY['CORE','DEEP','CORE','REM','AWAKE','CORE']::sleep_stage[])[1 + s % 6],
|
|
now(), now()
|
|
FROM generate_series(1, $2::int) AS d, generate_series(0, 29) AS s
|
|
`,
|
|
USER_ID,
|
|
days,
|
|
ANCHOR,
|
|
ZONE,
|
|
);
|
|
total += sleep;
|
|
log("SLEEP_DURATION (APPLE_HEALTH)", sleep);
|
|
return total;
|
|
}
|
|
|
|
async function seedWorkouts(days: number): Promise<number> {
|
|
// About one workout every 29 hours, rotating through five sports.
|
|
const count = Math.floor((days * 24) / 29);
|
|
return prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO workouts
|
|
(id, user_id, sport_type, started_at, ended_at, duration_sec,
|
|
total_energy_kcal, total_distance_m, avg_heart_rate, max_heart_rate,
|
|
source, external_id, created_at, updated_at)
|
|
SELECT
|
|
'seed-' || $1 || '-workout-' || g,
|
|
$1,
|
|
(ARRAY['walking','running','cycling','hiking','swimming'])[1 + g % 5],
|
|
$3::date::timestamp - make_interval(hours => (g * 29)::int) + interval '17 hours',
|
|
$3::date::timestamp - make_interval(hours => (g * 29)::int) + interval '17 hours'
|
|
+ make_interval(secs => (1800 + (g * 97) % 3600)::int),
|
|
(1800 + (g * 97) % 3600)::int,
|
|
round((250 + (g * 13) % 400)::numeric, 1),
|
|
round((3000 + (g * 211) % 9000)::numeric, 1),
|
|
(115 + (g * 7) % 40)::int,
|
|
(150 + (g * 5) % 35)::int,
|
|
'APPLE_HEALTH',
|
|
'hk-workout-' || md5($1 || g::text),
|
|
now(), now()
|
|
FROM generate_series(0, $2::int - 1) AS g
|
|
`,
|
|
USER_ID,
|
|
count,
|
|
ANCHOR,
|
|
);
|
|
}
|
|
|
|
async function seedMood(days: number): Promise<number> {
|
|
const span = Math.min(days, 3 * 365);
|
|
return prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO mood_entries
|
|
(id, user_id, date, mood, score, mood_a1, stress_a2, energy_a3,
|
|
source, mood_logged_at, tz, synced_at, created_at, updated_at)
|
|
SELECT
|
|
'seed-' || $1 || '-mood-' || d,
|
|
$1,
|
|
to_char($3::date - d, 'YYYY-MM-DD'),
|
|
(ARRAY['LAUSIG','SCHLECHT','OKAY','GUT','SUPER_GUT'])[1 + (d * 7) % 5],
|
|
1 + (d * 7) % 5,
|
|
2 * ((d * 7) % 5) + 1,
|
|
(d * 3) % 11,
|
|
(d * 5) % 11,
|
|
'MANUAL',
|
|
(($3::date - d)::timestamp + interval '20 hours') AT TIME ZONE $4 AT TIME ZONE 'UTC',
|
|
$4,
|
|
now(), now(), now()
|
|
FROM generate_series(1, $2::int) AS d
|
|
`,
|
|
USER_ID,
|
|
span,
|
|
ANCHOR,
|
|
ZONE,
|
|
);
|
|
}
|
|
|
|
async function seedMedications(days: number): Promise<number> {
|
|
const span = Math.min(days, 3 * 365);
|
|
const meds = [
|
|
{ key: "a", name: "Synthetic A", dose: "5 mg", window: ["08:00", "09:00"] },
|
|
{
|
|
key: "b",
|
|
name: "Synthetic B",
|
|
dose: "100 mg",
|
|
window: ["08:00", "09:00"],
|
|
},
|
|
{
|
|
key: "c",
|
|
name: "Synthetic C",
|
|
dose: "20 mg",
|
|
window: ["20:00", "21:00"],
|
|
},
|
|
];
|
|
let events = 0;
|
|
for (const m of meds) {
|
|
const medId = `seed-${USER_ID}-med-${m.key}`;
|
|
await prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO medications
|
|
(id, user_id, name, dose, starts_on, created_at, updated_at)
|
|
VALUES ($1, $2, $3, $4, ($5::date - $6::int)::timestamp, now(), now())
|
|
`,
|
|
medId,
|
|
USER_ID,
|
|
m.name,
|
|
m.dose,
|
|
ANCHOR,
|
|
span,
|
|
);
|
|
await prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO medication_schedules
|
|
(id, medication_id, window_start, window_end, schedule_type)
|
|
VALUES ($1, $2, $3, $4, 'SCHEDULED')
|
|
`,
|
|
`${medId}-schedule`,
|
|
medId,
|
|
m.window[0],
|
|
m.window[1],
|
|
);
|
|
// One intake event per day; about one in twelve skipped, the rest taken
|
|
// a few minutes into the window.
|
|
events += await prisma.$executeRawUnsafe(
|
|
`
|
|
INSERT INTO medication_intake_events
|
|
(id, user_id, medication_id, scheduled_for, taken_at, skipped,
|
|
attribution_source, source, created_at, updated_at)
|
|
SELECT
|
|
$1 || '-intake-' || d,
|
|
$2,
|
|
$1,
|
|
(($3::date - d)::timestamp + $5::time) AT TIME ZONE $6 AT TIME ZONE 'UTC',
|
|
CASE WHEN d % 12 = 0 THEN NULL
|
|
ELSE (($3::date - d)::timestamp + $5::time
|
|
+ make_interval(mins => (d * 7) % 40)) AT TIME ZONE $6 AT TIME ZONE 'UTC'
|
|
END,
|
|
d % 12 = 0,
|
|
'AUTO',
|
|
'WEB',
|
|
now(), now()
|
|
FROM generate_series(1, $4::int) AS d
|
|
`,
|
|
medId,
|
|
USER_ID,
|
|
ANCHOR,
|
|
span,
|
|
m.window[0],
|
|
ZONE,
|
|
);
|
|
}
|
|
return events;
|
|
}
|
|
|
|
async function main(): Promise<void> {
|
|
const days = Math.round(YEARS * 365);
|
|
const started = Date.now();
|
|
|
|
if (RESET) {
|
|
await prisma.$executeRawUnsafe(`DELETE FROM users WHERE id = $1`, USER_ID);
|
|
}
|
|
await prisma.$executeRawUnsafe(
|
|
`INSERT INTO users (id, username, timezone, onboarding_completed_at, updated_at)
|
|
VALUES ($1, $1, $2, now(), now())
|
|
ON CONFLICT (id) DO NOTHING`,
|
|
USER_ID,
|
|
ZONE,
|
|
);
|
|
|
|
const measurements = await seedMeasurements(days);
|
|
log("measurements total", measurements);
|
|
log("workouts", await seedWorkouts(days));
|
|
log("mood entries", await seedMood(days));
|
|
log("medication intake events", await seedMedications(days));
|
|
|
|
if (ROLLUPS) {
|
|
const folded = await recomputeUserRollups(USER_ID, {
|
|
granularities: ALL_GRANULARITIES,
|
|
statementTimeoutMs: 600_000,
|
|
});
|
|
log("rollup rows", folded.rowsUpserted);
|
|
}
|
|
|
|
for (const table of [
|
|
"measurements",
|
|
"measurement_rollups",
|
|
"workouts",
|
|
"mood_entries",
|
|
"medication_intake_events",
|
|
]) {
|
|
await prisma.$executeRawUnsafe(`ANALYZE ${table}`);
|
|
}
|
|
console.log(`done in ${((Date.now() - started) / 1000).toFixed(1)} s`);
|
|
}
|
|
|
|
main()
|
|
.catch((err) => {
|
|
console.error(err);
|
|
process.exitCode = 1;
|
|
})
|
|
.finally(() => prisma.$disconnect());
|