thedotmack/claude-mem · critical

schema v46: failed to copy

Error message

schema v46: failed to copy ${target.table}.${target.column} row ${mismatch.row_id} exactly

What it means

After copying revision values into the temporary TEXT column, the migration verifies at least one row matches exactly (temporary <> CAST(column AS TEXT) finds no row). A mismatch means the copy did not preserve the value byte-for-byte, so it refuses to drop/rename columns, preventing silent data loss.

Solutions

  1. Ensure no other process is writing to the database and retry startup
  2. Restore a pre-migration backup
  3. Compare the flagged row's two values to diagnose the affinity behavior
  4. Report as a bug if it reproduces with exclusive access

Example fix

-- identify the mismatching row
SELECT CAST(id AS TEXT) FROM sdk_sessions WHERE __tmp_sync_rev <> CAST(sync_rev AS TEXT) LIMIT 1;
Defensive patterns

Strategy: retry

Validate before calling

// ensure exclusive access before startup
const lock = db.query('PRAGMA database_list').all();
// also verify no other worker holds a write lock:
const busy = db.query('PRAGMA busy_timeout').get();

Try / catch

try { store = new SessionStore(dbPath); } catch (e) {
  if (String(e.message).includes('failed to copy') && String(e.message).startsWith('schema v46')) {
    await retryWithExclusiveLock(() => new SessionStore(dbPath));
  } else throw e;
}

Prevention

When it happens

Trigger: The post-copy verification SELECT in ensureSyncRevisionTextAffinity returns a row where the temporary column differs from CAST(original AS TEXT).

Common situations: Concurrent writers modifying rows mid-migration; SQLite collation/type affinity quirks altering copied values; an external process touching the DB during startup.

Understand the failure class

Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/869e6edd4ae55fab. Report an issue: GitHub.

Appendix: source

Thrown at src/services/sqlite/SessionStore.ts:754

        if (isText(source)) continue;
        if (columns.some(info => info.name === target.temporary)) {
          throw new Error(`schema v46: unexpected temporary column ${target.table}.${target.temporary}`);
        }

        this.db.run(
          `ALTER TABLE ${target.table} ADD COLUMN ${target.temporary} TEXT NOT NULL DEFAULT '1'`
        );
        this.db.run(
          `UPDATE ${target.table} SET ${target.temporary} = CAST(${target.column} AS TEXT)`
        );
        const mismatch = this.db.prepare(`
          SELECT CAST(id AS TEXT) AS row_id
          FROM ${target.table}
          WHERE ${target.temporary} <> CAST(${target.column} AS TEXT)
          LIMIT 1
        `).get() as { row_id: string } | undefined;
        if (mismatch) {
          throw new Error(
            `schema v46: failed to copy ${target.table}.${target.column} row ${mismatch.row_id} exactly`
          );
        }
        this.db.run(`ALTER TABLE ${target.table} DROP COLUMN ${target.column}`);
        this.db.run(
          `ALTER TABLE ${target.table} RENAME COLUMN ${target.temporary} TO ${target.column}`
        );
      }

      this.db.prepare('INSERT OR IGNORE INTO schema_versions (version, applied_at) VALUES (?, ?)')
        .run(46, new Date().toISOString());
    });
    tx();
  }

  /**
   * Canonical-v2 entity heads and durable tombstone queue. Revisions remain
   * decimal TEXT so a remote value is never rounded through a JS number.

View on GitHub (pinned to d8bc9755e7)