@evalstudio/postgres 0.4.0 → 0.5.0

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/README.md CHANGED
@@ -17,7 +17,7 @@ npm init -y
17
17
  ### 2. Install dependencies
18
18
 
19
19
  ```bash
20
- npm install @evalstudio/cli @evalstudio/postgres
20
+ npm install @evalstudio/cli @evalstudio/postgres dotenv-cli
21
21
  ```
22
22
 
23
23
  ### 3. Initialize the project
@@ -28,7 +28,13 @@ npx evalstudio init
28
28
 
29
29
  This creates an `evalstudio.config.json` in the current directory.
30
30
 
31
- ### 4. Configure PostgreSQL storage
31
+ ### 4. Create a `.env` file
32
+
33
+ ```bash
34
+ EVALSTUDIO_DATABASE_URL=postgresql://user:pass@localhost:5432/evalstudio
35
+ ```
36
+
37
+ ### 5. Configure PostgreSQL storage
32
38
 
33
39
  Edit `evalstudio.config.json` to use Postgres:
34
40
 
@@ -52,31 +58,33 @@ Edit `evalstudio.config.json` to use Postgres:
52
58
 
53
59
  The `connectionString` supports `${VAR}` placeholders that resolve from environment variables at runtime. If `connectionString` is omitted entirely, it falls back to the `EVALSTUDIO_DATABASE_URL` environment variable.
54
60
 
55
- ### 5. Add a start script
61
+ ### 6. Add scripts
56
62
 
57
63
  Update your `package.json`:
58
64
 
59
65
  ```json
60
66
  {
61
67
  "scripts": {
62
- "start": "evalstudio serve",
63
- "db:init": "evalstudio db init"
68
+ "start": "dotenv -- evalstudio serve",
69
+ "db:init": "dotenv -- evalstudio db init"
64
70
  },
65
71
  "dependencies": {
66
72
  "@evalstudio/cli": "latest",
67
- "@evalstudio/postgres": "latest"
73
+ "@evalstudio/postgres": "latest",
74
+ "dotenv-cli": "latest"
68
75
  }
69
76
  }
70
77
  ```
71
78
 
72
- ### 6. Initialize the database
79
+ The `dotenv --` prefix loads variables from `.env` before running the command, so the `${EVALSTUDIO_DATABASE_URL}` placeholder in the config resolves correctly.
80
+
81
+ ### 7. Initialize the database
73
82
 
74
83
  ```bash
75
- export EVALSTUDIO_DATABASE_URL="postgresql://user:pass@localhost:5432/evalstudio"
76
84
  npm run db:init
77
85
  ```
78
86
 
79
- ### 7. Start the server
87
+ ### 8. Start the server
80
88
 
81
89
  ```bash
82
90
  npm start
@@ -111,7 +119,7 @@ docker run -p 3000:3000 \
111
119
  evalstudio-server
112
120
  ```
113
121
 
114
- `db:init` is idempotent — it's safe to run on every container start. The schema is created if it doesn't exist and left untouched if it does.
122
+ `db:init` is idempotent — it's safe to run on every container start. Only pending migrations are applied; already-applied ones are skipped.
115
123
 
116
124
  ## How It Works
117
125
 
@@ -119,6 +127,16 @@ When `storage.type` is set to `"postgres"` in your config, `@evalstudio/core` dy
119
127
 
120
128
  If the package is not installed, you'll get a clear error message telling you to add it.
121
129
 
130
+ ### Migrations
131
+
132
+ Schema changes are managed via version-stamped SQL migrations. A `schema_migrations` table tracks which migrations have been applied. Each migration runs in its own transaction — if one fails, previously applied migrations remain committed.
133
+
134
+ To check migration status:
135
+
136
+ ```bash
137
+ evalstudio db status
138
+ ```
139
+
122
140
  ## API
123
141
 
124
142
  ### `createPostgresStorage(connectionString: string): Promise<StorageProvider>`
@@ -127,7 +145,11 @@ Creates a PostgreSQL-backed storage provider. The database schema must already e
127
145
 
128
146
  ### `initSchema(connectionString: string): Promise<void>`
129
147
 
130
- Creates all required database tables. Used internally by the `evalstudio db init` CLI command.
148
+ Runs all pending database migrations. Used internally by the `evalstudio db init` CLI command.
149
+
150
+ ### `getMigrationStatus(connectionString: string): Promise<MigrationStatus>`
151
+
152
+ Returns applied and pending migrations. Used internally by `evalstudio db status`.
131
153
 
132
154
  ## License
133
155
 
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import type { StorageProvider } from "@evalstudio/core";
2
+ import { type MigrationStatus } from "./migrator.js";
2
3
  /**
3
4
  * Creates a PostgreSQL-backed StorageProvider.
4
5
  *
@@ -10,10 +11,19 @@ import type { StorageProvider } from "@evalstudio/core";
10
11
  */
11
12
  export declare function createPostgresStorage(connectionString: string): Promise<StorageProvider>;
12
13
  /**
13
- * Explicitly initializes the database schema.
14
+ * Runs all pending database migrations.
14
15
  * Used by the `evalstudio db init` CLI command.
16
+ * Safe to call on every startup — already-applied migrations are skipped.
15
17
  *
16
18
  * @param connectionString - PostgreSQL connection string
17
19
  */
18
20
  export declare function initSchema(connectionString: string): Promise<void>;
21
+ /**
22
+ * Returns applied and pending migration status.
23
+ * Used by the `evalstudio db status` CLI command.
24
+ *
25
+ * @param connectionString - PostgreSQL connection string
26
+ */
27
+ export declare function getMigrationStatus(connectionString: string): Promise<MigrationStatus>;
28
+ export type { MigrationStatus };
19
29
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAKxD;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAQ9F;AAED;;;;;GAKG;AACH,wBAAsB,UAAU,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxE"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAIxD,OAAO,EAAgD,KAAK,eAAe,EAAE,MAAM,eAAe,CAAC;AAEnG;;;;;;;;GAQG;AACH,wBAAsB,qBAAqB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAQ9F;AAED;;;;;;GAMG;AACH,wBAAsB,UAAU,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAOxE;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,gBAAgB,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAO3F;AAED,YAAY,EAAE,eAAe,EAAE,CAAC"}
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createPool } from "./pool.js";
2
2
  import { createPostgresStorageProvider } from "./postgres-storage.js";
3
3
  import { initSchema as initSchemaImpl } from "./schema.js";
4
+ import { getMigrationStatus as getMigrationStatusImpl } from "./migrator.js";
4
5
  /**
5
6
  * Creates a PostgreSQL-backed StorageProvider.
6
7
  *
@@ -18,8 +19,9 @@ export async function createPostgresStorage(connectionString) {
18
19
  return createPostgresStorageProvider(pool);
19
20
  }
20
21
  /**
21
- * Explicitly initializes the database schema.
22
+ * Runs all pending database migrations.
22
23
  * Used by the `evalstudio db init` CLI command.
24
+ * Safe to call on every startup — already-applied migrations are skipped.
23
25
  *
24
26
  * @param connectionString - PostgreSQL connection string
25
27
  */
@@ -32,4 +34,19 @@ export async function initSchema(connectionString) {
32
34
  await pool.end();
33
35
  }
34
36
  }
37
+ /**
38
+ * Returns applied and pending migration status.
39
+ * Used by the `evalstudio db status` CLI command.
40
+ *
41
+ * @param connectionString - PostgreSQL connection string
42
+ */
43
+ export async function getMigrationStatus(connectionString) {
44
+ const pool = createPool(connectionString);
45
+ try {
46
+ return await getMigrationStatusImpl(pool);
47
+ }
48
+ finally {
49
+ await pool.end();
50
+ }
51
+ }
35
52
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,aAAa,CAAC;AAE3D;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,gBAAwB;IAClE,MAAM,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAE1C,mEAAmE;IACnE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,MAAM,CAAC,OAAO,EAAE,CAAC;IAEjB,OAAO,6BAA6B,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,gBAAwB;IACvD,MAAM,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,WAAW,CAAC;AACvC,OAAO,EAAE,6BAA6B,EAAE,MAAM,uBAAuB,CAAC;AACtE,OAAO,EAAE,UAAU,IAAI,cAAc,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAE,kBAAkB,IAAI,sBAAsB,EAAwB,MAAM,eAAe,CAAC;AAEnG;;;;;;;;GAQG;AACH,MAAM,CAAC,KAAK,UAAU,qBAAqB,CAAC,gBAAwB;IAClE,MAAM,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAE1C,mEAAmE;IACnE,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;IACpC,MAAM,CAAC,OAAO,EAAE,CAAC;IAEjB,OAAO,6BAA6B,CAAC,IAAI,CAAC,CAAC;AAC7C,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,gBAAwB;IACvD,MAAM,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,MAAM,cAAc,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED;;;;;GAKG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,gBAAwB;IAC/D,MAAM,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,MAAM,sBAAsB,CAAC,IAAI,CAAC,CAAC;IAC5C,CAAC;YAAS,CAAC;QACT,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;IACnB,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Migration registry for EvalStudio PostgreSQL storage.
3
+ *
4
+ * Each migration is a versioned SQL string that runs in a transaction.
5
+ * Migrations are forward-only and immutable once shipped — never edit
6
+ * an existing migration. To make changes, add a new one.
7
+ *
8
+ * Naming convention: NNN_descriptive_name (e.g., 001_initial)
9
+ * The `version` integer is what the system uses; the `name` is for humans.
10
+ */
11
+ export interface Migration {
12
+ version: number;
13
+ name: string;
14
+ sql: string;
15
+ }
16
+ export declare const ALL_MIGRATIONS: Migration[];
17
+ //# sourceMappingURL=migrations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.d.ts","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,WAAW,SAAS;IACxB,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,GAAG,EAAE,MAAM,CAAC;CACb;AAED,eAAO,MAAM,cAAc,EAAE,SAAS,EA4ErC,CAAC"}
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Migration registry for EvalStudio PostgreSQL storage.
3
+ *
4
+ * Each migration is a versioned SQL string that runs in a transaction.
5
+ * Migrations are forward-only and immutable once shipped — never edit
6
+ * an existing migration. To make changes, add a new one.
7
+ *
8
+ * Naming convention: NNN_descriptive_name (e.g., 001_initial)
9
+ * The `version` integer is what the system uses; the `name` is for humans.
10
+ */
11
+ export const ALL_MIGRATIONS = [
12
+ {
13
+ version: 1,
14
+ name: "001_initial",
15
+ sql: `
16
+ -- FK strategy:
17
+ -- project_id → ON DELETE CASCADE (deleting a project removes all its data)
18
+ -- all other FKs → ON DELETE SET NULL (referenced entities can always be deleted)
19
+
20
+ CREATE TABLE IF NOT EXISTS projects (
21
+ id UUID PRIMARY KEY,
22
+ name TEXT NOT NULL,
23
+ llm_settings JSONB,
24
+ max_concurrency INTEGER,
25
+ created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
26
+ updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
27
+ );
28
+
29
+ CREATE TABLE IF NOT EXISTS personas (
30
+ id UUID PRIMARY KEY,
31
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
32
+ data JSONB NOT NULL
33
+ );
34
+ CREATE INDEX IF NOT EXISTS idx_personas_project ON personas(project_id);
35
+
36
+ CREATE TABLE IF NOT EXISTS scenarios (
37
+ id UUID PRIMARY KEY,
38
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
39
+ data JSONB NOT NULL
40
+ );
41
+ CREATE INDEX IF NOT EXISTS idx_scenarios_project ON scenarios(project_id);
42
+
43
+ CREATE TABLE IF NOT EXISTS connectors (
44
+ id UUID PRIMARY KEY,
45
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
46
+ data JSONB NOT NULL
47
+ );
48
+ CREATE INDEX IF NOT EXISTS idx_connectors_project ON connectors(project_id);
49
+
50
+ CREATE TABLE IF NOT EXISTS evals (
51
+ id UUID PRIMARY KEY,
52
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
53
+ connector_id UUID REFERENCES connectors(id) ON DELETE SET NULL,
54
+ data JSONB NOT NULL
55
+ );
56
+ CREATE INDEX IF NOT EXISTS idx_evals_project ON evals(project_id);
57
+ CREATE INDEX IF NOT EXISTS idx_evals_connector ON evals(connector_id);
58
+
59
+ CREATE TABLE IF NOT EXISTS executions (
60
+ id INTEGER NOT NULL,
61
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
62
+ eval_id UUID REFERENCES evals(id) ON DELETE SET NULL,
63
+ data JSONB NOT NULL,
64
+ PRIMARY KEY (project_id, id)
65
+ );
66
+ CREATE INDEX IF NOT EXISTS idx_executions_project ON executions(project_id);
67
+ CREATE INDEX IF NOT EXISTS idx_executions_eval ON executions(eval_id);
68
+
69
+ CREATE TABLE IF NOT EXISTS runs (
70
+ id UUID PRIMARY KEY,
71
+ project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
72
+ eval_id UUID REFERENCES evals(id) ON DELETE SET NULL,
73
+ scenario_id UUID REFERENCES scenarios(id) ON DELETE SET NULL,
74
+ persona_id UUID REFERENCES personas(id) ON DELETE SET NULL,
75
+ connector_id UUID REFERENCES connectors(id) ON DELETE SET NULL,
76
+ execution_id INTEGER,
77
+ status TEXT NOT NULL,
78
+ data JSONB NOT NULL
79
+ );
80
+ CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
81
+ CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(project_id, status);
82
+ CREATE INDEX IF NOT EXISTS idx_runs_eval ON runs(project_id, eval_id);
83
+ CREATE INDEX IF NOT EXISTS idx_runs_scenario ON runs(project_id, scenario_id);
84
+ CREATE INDEX IF NOT EXISTS idx_runs_execution ON runs(project_id, execution_id);
85
+ `,
86
+ },
87
+ ];
88
+ //# sourceMappingURL=migrations.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrations.js","sourceRoot":"","sources":["../src/migrations.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAQH,MAAM,CAAC,MAAM,cAAc,GAAgB;IACzC;QACE,OAAO,EAAE,CAAC;QACV,IAAI,EAAE,aAAa;QACnB,GAAG,EAAE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;KAsEJ;KACF;CACF,CAAC"}
@@ -0,0 +1,23 @@
1
+ import type { Pool } from "pg";
2
+ export interface MigrationStatus {
3
+ applied: Array<{
4
+ version: number;
5
+ name: string;
6
+ appliedAt: Date;
7
+ }>;
8
+ pending: Array<{
9
+ version: number;
10
+ name: string;
11
+ }>;
12
+ }
13
+ /**
14
+ * Runs all pending migrations in order.
15
+ * Each migration executes in its own transaction.
16
+ * Safe to call on every startup — already-applied migrations are skipped.
17
+ */
18
+ export declare function runMigrations(pool: Pool): Promise<void>;
19
+ /**
20
+ * Returns the current migration status: which are applied and which are pending.
21
+ */
22
+ export declare function getMigrationStatus(pool: Pool): Promise<MigrationStatus>;
23
+ //# sourceMappingURL=migrator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrator.d.ts","sourceRoot":"","sources":["../src/migrator.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAU/B,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,IAAI,CAAA;KAAE,CAAC,CAAC;IACnE,OAAO,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACnD;AAED;;;;GAIG;AACH,wBAAsB,aAAa,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAY7D;AAED;;GAEG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,eAAe,CAAC,CA4B7E"}
@@ -0,0 +1,75 @@
1
+ import { ALL_MIGRATIONS } from "./migrations.js";
2
+ const SCHEMA_MIGRATIONS_DDL = `
3
+ CREATE TABLE IF NOT EXISTS schema_migrations (
4
+ version INTEGER PRIMARY KEY,
5
+ name TEXT NOT NULL,
6
+ applied_at TIMESTAMPTZ NOT NULL DEFAULT now()
7
+ )`;
8
+ /**
9
+ * Runs all pending migrations in order.
10
+ * Each migration executes in its own transaction.
11
+ * Safe to call on every startup — already-applied migrations are skipped.
12
+ */
13
+ export async function runMigrations(pool) {
14
+ await pool.query(SCHEMA_MIGRATIONS_DDL);
15
+ const applied = await getAppliedVersions(pool);
16
+ validateMigrationOrder();
17
+ const pending = ALL_MIGRATIONS.filter((m) => !applied.has(m.version));
18
+ for (const migration of pending) {
19
+ await applyMigration(pool, migration);
20
+ }
21
+ }
22
+ /**
23
+ * Returns the current migration status: which are applied and which are pending.
24
+ */
25
+ export async function getMigrationStatus(pool) {
26
+ // Check if schema_migrations table exists
27
+ const tableExists = await pool.query(`SELECT EXISTS (
28
+ SELECT FROM information_schema.tables
29
+ WHERE table_name = 'schema_migrations'
30
+ ) AS exists`);
31
+ if (!tableExists.rows[0].exists) {
32
+ return {
33
+ applied: [],
34
+ pending: ALL_MIGRATIONS.map((m) => ({ version: m.version, name: m.name })),
35
+ };
36
+ }
37
+ const { rows } = await pool.query("SELECT version, name, applied_at FROM schema_migrations ORDER BY version");
38
+ const appliedVersions = new Set(rows.map((r) => r.version));
39
+ return {
40
+ applied: rows.map((r) => ({ version: r.version, name: r.name, appliedAt: r.applied_at })),
41
+ pending: ALL_MIGRATIONS
42
+ .filter((m) => !appliedVersions.has(m.version))
43
+ .map((m) => ({ version: m.version, name: m.name })),
44
+ };
45
+ }
46
+ async function getAppliedVersions(pool) {
47
+ const { rows } = await pool.query("SELECT version FROM schema_migrations ORDER BY version");
48
+ return new Set(rows.map((r) => r.version));
49
+ }
50
+ function validateMigrationOrder() {
51
+ for (let i = 1; i < ALL_MIGRATIONS.length; i++) {
52
+ if (ALL_MIGRATIONS[i].version <= ALL_MIGRATIONS[i - 1].version) {
53
+ throw new Error(`Migration ordering error: ${ALL_MIGRATIONS[i].name} ` +
54
+ `(version ${ALL_MIGRATIONS[i].version}) must be greater than ` +
55
+ `${ALL_MIGRATIONS[i - 1].name} (version ${ALL_MIGRATIONS[i - 1].version})`);
56
+ }
57
+ }
58
+ }
59
+ async function applyMigration(pool, migration) {
60
+ const client = await pool.connect();
61
+ try {
62
+ await client.query("BEGIN");
63
+ await client.query(migration.sql);
64
+ await client.query("INSERT INTO schema_migrations (version, name) VALUES ($1, $2)", [migration.version, migration.name]);
65
+ await client.query("COMMIT");
66
+ }
67
+ catch (err) {
68
+ await client.query("ROLLBACK");
69
+ throw new Error(`Migration ${migration.name} failed: ${err instanceof Error ? err.message : String(err)}`);
70
+ }
71
+ finally {
72
+ client.release();
73
+ }
74
+ }
75
+ //# sourceMappingURL=migrator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrator.js","sourceRoot":"","sources":["../src/migrator.ts"],"names":[],"mappings":"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"}
package/dist/schema.d.ts CHANGED
@@ -1,11 +1,12 @@
1
1
  import type { Pool } from "pg";
2
2
  /**
3
- * Initializes the database schema.
4
- * Uses IF NOT EXISTS so it's safe to call on every startup.
3
+ * Initializes the database schema by running all pending migrations.
4
+ * Safe to call on every startup already-applied migrations are skipped.
5
5
  */
6
6
  export declare function initSchema(pool: Pool): Promise<void>;
7
7
  /**
8
- * Checks if the schema has been initialized (projects table exists).
8
+ * Checks if the schema has been initialized (schema_migrations table exists
9
+ * and has at least one applied migration).
9
10
  */
10
11
  export declare function schemaExists(pool: Pool): Promise<boolean>;
11
12
  //# sourceMappingURL=schema.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AA8E/B;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAQ/D"}
1
+ {"version":3,"file":"schema.d.ts","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,IAAI,CAAC;AAG/B;;;GAGG;AACH,wBAAsB,UAAU,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAE1D;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAAC,IAAI,EAAE,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAa/D"}
package/dist/schema.js CHANGED
@@ -1,93 +1,23 @@
1
+ import { runMigrations } from "./migrator.js";
1
2
  /**
2
- * SQL schema for EvalStudio PostgreSQL storage.
3
- *
4
- * Design: reference columns (project_id, eval_id, etc.) are real columns
5
- * with REFERENCES constraints for relational integrity. The rest of the
6
- * entity payload lives in a JSONB `data` column to avoid mapping every
7
- * field upfront.
8
- */
9
- const SCHEMA_SQL = `
10
- CREATE TABLE IF NOT EXISTS projects (
11
- id UUID PRIMARY KEY,
12
- name TEXT NOT NULL,
13
- llm_settings JSONB,
14
- max_concurrency INTEGER,
15
- created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
16
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
17
- );
18
-
19
- CREATE TABLE IF NOT EXISTS personas (
20
- id UUID PRIMARY KEY,
21
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
22
- data JSONB NOT NULL
23
- );
24
- CREATE INDEX IF NOT EXISTS idx_personas_project ON personas(project_id);
25
-
26
- CREATE TABLE IF NOT EXISTS scenarios (
27
- id UUID PRIMARY KEY,
28
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
29
- data JSONB NOT NULL
30
- );
31
- CREATE INDEX IF NOT EXISTS idx_scenarios_project ON scenarios(project_id);
32
-
33
- CREATE TABLE IF NOT EXISTS connectors (
34
- id UUID PRIMARY KEY,
35
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
36
- data JSONB NOT NULL
37
- );
38
- CREATE INDEX IF NOT EXISTS idx_connectors_project ON connectors(project_id);
39
-
40
- CREATE TABLE IF NOT EXISTS evals (
41
- id UUID PRIMARY KEY,
42
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
43
- connector_id UUID NOT NULL REFERENCES connectors(id),
44
- data JSONB NOT NULL
45
- );
46
- CREATE INDEX IF NOT EXISTS idx_evals_project ON evals(project_id);
47
- CREATE INDEX IF NOT EXISTS idx_evals_connector ON evals(connector_id);
48
-
49
- CREATE TABLE IF NOT EXISTS executions (
50
- id INTEGER NOT NULL,
51
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
52
- eval_id UUID NOT NULL REFERENCES evals(id),
53
- data JSONB NOT NULL,
54
- PRIMARY KEY (project_id, id)
55
- );
56
- CREATE INDEX IF NOT EXISTS idx_executions_project ON executions(project_id);
57
- CREATE INDEX IF NOT EXISTS idx_executions_eval ON executions(eval_id);
58
-
59
- CREATE TABLE IF NOT EXISTS runs (
60
- id UUID PRIMARY KEY,
61
- project_id UUID NOT NULL REFERENCES projects(id) ON DELETE CASCADE,
62
- eval_id UUID REFERENCES evals(id),
63
- scenario_id UUID NOT NULL REFERENCES scenarios(id),
64
- persona_id UUID REFERENCES personas(id),
65
- connector_id UUID REFERENCES connectors(id),
66
- execution_id INTEGER,
67
- status TEXT NOT NULL,
68
- data JSONB NOT NULL
69
- );
70
- CREATE INDEX IF NOT EXISTS idx_runs_project ON runs(project_id);
71
- CREATE INDEX IF NOT EXISTS idx_runs_status ON runs(project_id, status);
72
- CREATE INDEX IF NOT EXISTS idx_runs_eval ON runs(project_id, eval_id);
73
- CREATE INDEX IF NOT EXISTS idx_runs_scenario ON runs(project_id, scenario_id);
74
- CREATE INDEX IF NOT EXISTS idx_runs_execution ON runs(project_id, execution_id);
75
- `;
76
- /**
77
- * Initializes the database schema.
78
- * Uses IF NOT EXISTS so it's safe to call on every startup.
3
+ * Initializes the database schema by running all pending migrations.
4
+ * Safe to call on every startup — already-applied migrations are skipped.
79
5
  */
80
6
  export async function initSchema(pool) {
81
- await pool.query(SCHEMA_SQL);
7
+ await runMigrations(pool);
82
8
  }
83
9
  /**
84
- * Checks if the schema has been initialized (projects table exists).
10
+ * Checks if the schema has been initialized (schema_migrations table exists
11
+ * and has at least one applied migration).
85
12
  */
86
13
  export async function schemaExists(pool) {
87
14
  const result = await pool.query(`SELECT EXISTS (
88
15
  SELECT FROM information_schema.tables
89
- WHERE table_name = 'projects'
16
+ WHERE table_name = 'schema_migrations'
90
17
  ) AS exists`);
91
- return result.rows[0].exists;
18
+ if (!result.rows[0].exists)
19
+ return false;
20
+ const count = await pool.query("SELECT COUNT(*) AS count FROM schema_migrations");
21
+ return parseInt(count.rows[0].count, 10) > 0;
92
22
  }
93
23
  //# sourceMappingURL=schema.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,UAAU,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkElB,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAU;IACzC,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;AAC/B,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAU;IAC3C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B;;;gBAGY,CACb,CAAC;IACF,OAAO,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC;AAC/B,CAAC"}
1
+ {"version":3,"file":"schema.js","sourceRoot":"","sources":["../src/schema.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAE9C;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,IAAU;IACzC,MAAM,aAAa,CAAC,IAAI,CAAC,CAAC;AAC5B,CAAC;AAED;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAU;IAC3C,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B;;;gBAGY,CACb,CAAC;IACF,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAEzC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAC5B,iDAAiD,CAClD,CAAC;IACF,OAAO,QAAQ,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC;AAC/C,CAAC"}
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.4.0",
6
+ "version": "0.5.0",
7
7
  "description": "PostgreSQL storage backend for EvalStudio",
8
8
  "type": "module",
9
9
  "main": "./dist/index.js",
@@ -29,7 +29,7 @@
29
29
  ],
30
30
  "dependencies": {
31
31
  "pg": "^8.13.1",
32
- "@evalstudio/core": "0.4.0"
32
+ "@evalstudio/core": "0.5.0"
33
33
  },
34
34
  "devDependencies": {
35
35
  "@types/pg": "^8.11.11",