All files / api/src/connectors/libsql migrate.ts

100% Statements 35/35
100% Branches 6/6
100% Functions 2/2
100% Lines 35/35

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67  1x 1x 1x             1x           1x 60x 60x 60x                             1x 60x 60x 60x 60x 60x   60x 60x   60x 180x 3x 3x   177x 177x 177x 177x 177x 177x 177x 177x 177x 177x   177x 177x 177x   60x 60x  
import type { Client } from '@libsql/client';
import { info } from '@shared/console-logging';
import { ensureForeignKeys } from './client';
import { type LibsqlMigration, libsqlMigrations } from './schema';
 
/**
 * Mirrors the Postgres tracking table that
 * `docker/postgres/migrations/run-migrations.sh` maintains, so both backends
 * answer "which migrations have been applied" the same way.
 */
const SCHEMA_MIGRATIONS = `
CREATE TABLE IF NOT EXISTS schema_migrations (
  version TEXT PRIMARY KEY,
  applied_at TEXT NOT NULL DEFAULT (strftime('%Y-%m-%dT%H:%M:%fZ', 'now'))
)`;
 
const appliedVersions = async (client: Client): Promise<Set<string>> => {
  const result = await client.execute('SELECT version FROM schema_migrations');
  return new Set(result.rows.map((row) => String(row.version)));
};
 
/**
 * Apply any migrations this database has not seen.
 *
 * Each migration goes through `batch(..., 'write')`, which wraps its statements
 * in a single transaction: a migration either lands completely or not at all,
 * and its `schema_migrations` row is written inside the same transaction so the
 * two can never disagree.
 *
 * Unlike the Postgres runner there is no advisory lock. A local database has a
 * single writer by definition, and for a remote one the `CREATE TABLE IF NOT
 * EXISTS` / `INSERT OR IGNORE` shape means a concurrent second runner converges
 * on the same result rather than corrupting anything.
 */
export const migrateLibsql = async (
  client: Client,
  migrations: LibsqlMigration[] = libsqlMigrations,
): Promise<string[]> => {
  await ensureForeignKeys(client);
  await client.execute(SCHEMA_MIGRATIONS);
 
  const applied = await appliedVersions(client);
  const ran: string[] = [];
 
  for (const migration of migrations) {
    if (applied.has(migration.version)) {
      continue;
    }
 
    await client.batch(
      [
        ...migration.statements,
        {
          sql: 'INSERT OR IGNORE INTO schema_migrations (version) VALUES (?)',
          args: [migration.version],
        },
      ],
      'write',
    );
 
    ran.push(migration.version);
    info(`[libsql] applied migration ${migration.version}`);
  }
 
  return ran;
};