forked from jsnbuchanan/crowd-funder-for-time-pwa
fix: Identify and fix migration tracking issue with proper parameter binding
- Root cause: Migration names were not being properly inserted into migrations table - Fixed parameter binding in Capacitor platform service migration functions - Added detailed debugging to track SQL execution and parameter passing - Reverted migrations back to proper form (without IF NOT EXISTS workarounds) - Enhanced extractMigrationNames to handle Capacitor SQLite result format The migration system should now properly track applied migrations and avoid re-running them on subsequent app starts.
This commit is contained in:
@@ -40,12 +40,13 @@ const randomBytes = crypto.getRandomValues(new Uint8Array(32));
|
|||||||
const secretBase64 = arrayBufferToBase64(randomBytes);
|
const secretBase64 = arrayBufferToBase64(randomBytes);
|
||||||
|
|
||||||
// Each migration can include multiple SQL statements (with semicolons)
|
// Each migration can include multiple SQL statements (with semicolons)
|
||||||
|
// NOTE: These should run only once per migration. The migration system tracks
|
||||||
|
// which migrations have been applied in the 'migrations' table.
|
||||||
const MIGRATIONS = [
|
const MIGRATIONS = [
|
||||||
{
|
{
|
||||||
name: "001_initial",
|
name: "001_initial",
|
||||||
sql: `
|
sql: `
|
||||||
-- Create accounts table only if it doesn't exist
|
CREATE TABLE accounts (
|
||||||
CREATE TABLE IF NOT EXISTS accounts (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
dateCreated TEXT NOT NULL,
|
dateCreated TEXT NOT NULL,
|
||||||
derivationPath TEXT,
|
derivationPath TEXT,
|
||||||
@@ -56,19 +57,16 @@ const MIGRATIONS = [
|
|||||||
publicKeyHex TEXT NOT NULL
|
publicKeyHex TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_accounts_did ON accounts(did);
|
CREATE INDEX idx_accounts_did ON accounts(did);
|
||||||
|
|
||||||
-- Create secret table only if it doesn't exist
|
CREATE TABLE secret (
|
||||||
CREATE TABLE IF NOT EXISTS secret (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
secretBase64 TEXT NOT NULL
|
secretBase64 TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Insert secret only if table is empty
|
INSERT INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
|
||||||
INSERT OR IGNORE INTO secret (id, secretBase64) VALUES (1, '${secretBase64}');
|
|
||||||
|
|
||||||
-- Create settings table only if it doesn't exist
|
CREATE TABLE settings (
|
||||||
CREATE TABLE IF NOT EXISTS settings (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
accountDid TEXT,
|
accountDid TEXT,
|
||||||
activeDid TEXT,
|
activeDid TEXT,
|
||||||
@@ -100,13 +98,11 @@ const MIGRATIONS = [
|
|||||||
webPushServer TEXT
|
webPushServer TEXT
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_settings_accountDid ON settings(accountDid);
|
CREATE INDEX idx_settings_accountDid ON settings(accountDid);
|
||||||
|
|
||||||
-- Insert default settings only if table is empty
|
INSERT INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
||||||
INSERT OR IGNORE INTO settings (id, apiServer) VALUES (1, '${DEFAULT_ENDORSER_API_SERVER}');
|
|
||||||
|
|
||||||
-- Create contacts table only if it doesn't exist
|
CREATE TABLE contacts (
|
||||||
CREATE TABLE IF NOT EXISTS contacts (
|
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
did TEXT NOT NULL,
|
did TEXT NOT NULL,
|
||||||
name TEXT,
|
name TEXT,
|
||||||
@@ -119,17 +115,15 @@ const MIGRATIONS = [
|
|||||||
registered BOOLEAN
|
registered BOOLEAN
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_contacts_did ON contacts(did);
|
CREATE INDEX idx_contacts_did ON contacts(did);
|
||||||
CREATE INDEX IF NOT EXISTS idx_contacts_name ON contacts(name);
|
CREATE INDEX idx_contacts_name ON contacts(name);
|
||||||
|
|
||||||
-- Create logs table only if it doesn't exist
|
CREATE TABLE logs (
|
||||||
CREATE TABLE IF NOT EXISTS logs (
|
|
||||||
date TEXT NOT NULL,
|
date TEXT NOT NULL,
|
||||||
message TEXT NOT NULL
|
message TEXT NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
-- Create temp table only if it doesn't exist
|
CREATE TABLE temp (
|
||||||
CREATE TABLE IF NOT EXISTS temp (
|
|
||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
blobB64 TEXT
|
blobB64 TEXT
|
||||||
);
|
);
|
||||||
@@ -139,7 +133,6 @@ const MIGRATIONS = [
|
|||||||
name: "002_add_iViewContent_to_contacts",
|
name: "002_add_iViewContent_to_contacts",
|
||||||
sql: `
|
sql: `
|
||||||
-- Add iViewContent column to contacts table
|
-- Add iViewContent column to contacts table
|
||||||
-- The migration service will check if the column already exists
|
|
||||||
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
ALTER TABLE contacts ADD COLUMN iViewContent BOOLEAN DEFAULT TRUE;
|
||||||
`,
|
`,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,18 +87,24 @@ export async function runMigrations<T>(
|
|||||||
console.log("📋 [Migration] Checking migration status...");
|
console.log("📋 [Migration] Checking migration status...");
|
||||||
|
|
||||||
// Create migrations table if it doesn't exist
|
// Create migrations table if it doesn't exist
|
||||||
|
console.log("🔧 [Migration] Creating migrations table if it doesn't exist...");
|
||||||
await sqlExec(`
|
await sqlExec(`
|
||||||
CREATE TABLE IF NOT EXISTS migrations (
|
CREATE TABLE IF NOT EXISTS migrations (
|
||||||
name TEXT PRIMARY KEY,
|
name TEXT PRIMARY KEY,
|
||||||
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
applied_at TEXT DEFAULT CURRENT_TIMESTAMP
|
||||||
);
|
);
|
||||||
`);
|
`);
|
||||||
|
console.log("✅ [Migration] Migrations table ready");
|
||||||
|
|
||||||
// Get list of already applied migrations
|
// Get list of already applied migrations
|
||||||
|
console.log("🔍 [Migration] Querying existing migrations...");
|
||||||
const appliedMigrationsResult = await sqlQuery(
|
const appliedMigrationsResult = await sqlQuery(
|
||||||
"SELECT name FROM migrations",
|
"SELECT name FROM migrations",
|
||||||
);
|
);
|
||||||
|
console.log("📊 [Migration] Raw query result:", appliedMigrationsResult);
|
||||||
|
|
||||||
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
const appliedMigrations = extractMigrationNames(appliedMigrationsResult);
|
||||||
|
console.log("📋 [Migration] Extracted applied migrations:", Array.from(appliedMigrations));
|
||||||
|
|
||||||
// Get all registered migrations
|
// Get all registered migrations
|
||||||
const migrations = migrationRegistry.getMigrations();
|
const migrations = migrationRegistry.getMigrations();
|
||||||
@@ -110,6 +116,7 @@ export async function runMigrations<T>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`);
|
console.log(`📊 [Migration] Found ${migrations.length} total migrations, ${appliedMigrations.size} already applied`);
|
||||||
|
console.log(`📝 [Migration] Registered migrations: ${migrations.map(m => m.name).join(', ')}`);
|
||||||
|
|
||||||
let appliedCount = 0;
|
let appliedCount = 0;
|
||||||
let skippedCount = 0;
|
let skippedCount = 0;
|
||||||
@@ -125,18 +132,17 @@ export async function runMigrations<T>(
|
|||||||
console.log(`🔄 [Migration] Applying migration: ${migration.name}`);
|
console.log(`🔄 [Migration] Applying migration: ${migration.name}`);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Special handling for column addition migrations
|
// Execute the migration SQL
|
||||||
if (migration.sql.includes("ALTER TABLE") && migration.sql.includes("ADD COLUMN")) {
|
console.log(`🔧 [Migration] Executing SQL for ${migration.name}...`);
|
||||||
await handleColumnAddition(migration, sqlExec, sqlQuery);
|
await sqlExec(migration.sql);
|
||||||
} else {
|
console.log(`✅ [Migration] SQL executed successfully for ${migration.name}`);
|
||||||
// Execute the migration SQL
|
|
||||||
await sqlExec(migration.sql);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Record that the migration was applied
|
// Record that the migration was applied
|
||||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
console.log(`📝 [Migration] Recording migration ${migration.name} as applied...`);
|
||||||
|
const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||||
migration.name,
|
migration.name,
|
||||||
]);
|
]);
|
||||||
|
console.log(`✅ [Migration] Migration record inserted:`, insertResult);
|
||||||
|
|
||||||
console.log(`✅ [Migration] Successfully applied: ${migration.name}`);
|
console.log(`✅ [Migration] Successfully applied: ${migration.name}`);
|
||||||
logger.info(
|
logger.info(
|
||||||
@@ -144,14 +150,17 @@ export async function runMigrations<T>(
|
|||||||
);
|
);
|
||||||
appliedCount++;
|
appliedCount++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error(`❌ [Migration] Error applying ${migration.name}:`, error);
|
||||||
|
|
||||||
// Handle specific cases where the migration might be partially applied
|
// Handle specific cases where the migration might be partially applied
|
||||||
const errorMessage = String(error).toLowerCase();
|
const errorMessage = String(error).toLowerCase();
|
||||||
|
|
||||||
// Check if it's a duplicate column error - this means the column already exists
|
// Check if it's a duplicate table/column error - this means the schema already exists
|
||||||
if (
|
if (
|
||||||
errorMessage.includes("duplicate column") ||
|
errorMessage.includes("duplicate column") ||
|
||||||
errorMessage.includes("column already exists") ||
|
errorMessage.includes("column already exists") ||
|
||||||
errorMessage.includes("already exists")
|
errorMessage.includes("already exists") ||
|
||||||
|
errorMessage.includes("table") && errorMessage.includes("already exists")
|
||||||
) {
|
) {
|
||||||
console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Marking as complete.`);
|
console.log(`⚠️ [Migration] ${migration.name} appears already applied (${errorMessage}). Marking as complete.`);
|
||||||
logger.warn(
|
logger.warn(
|
||||||
@@ -160,9 +169,11 @@ export async function runMigrations<T>(
|
|||||||
|
|
||||||
// Mark the migration as applied since the schema change already exists
|
// Mark the migration as applied since the schema change already exists
|
||||||
try {
|
try {
|
||||||
await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
console.log(`📝 [Migration] Attempting to record ${migration.name} as applied despite error...`);
|
||||||
|
const insertResult = await sqlExec("INSERT INTO migrations (name) VALUES (?)", [
|
||||||
migration.name,
|
migration.name,
|
||||||
]);
|
]);
|
||||||
|
console.log(`✅ [Migration] Migration record inserted after error:`, insertResult);
|
||||||
console.log(`✅ [Migration] Marked as applied: ${migration.name}`);
|
console.log(`✅ [Migration] Marked as applied: ${migration.name}`);
|
||||||
logger.info(
|
logger.info(
|
||||||
`[MigrationService] Successfully marked migration as applied: ${migration.name}`,
|
`[MigrationService] Successfully marked migration as applied: ${migration.name}`,
|
||||||
@@ -196,53 +207,4 @@ export async function runMigrations<T>(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle column addition migrations with proper existence checking
|
|
||||||
*
|
|
||||||
* @param migration - The migration containing column addition
|
|
||||||
* @param sqlExec - Function to execute SQL statements
|
|
||||||
* @param sqlQuery - Function to query SQL data
|
|
||||||
*/
|
|
||||||
async function handleColumnAddition<T>(
|
|
||||||
migration: Migration,
|
|
||||||
sqlExec: (sql: string, params?: unknown[]) => Promise<unknown>,
|
|
||||||
sqlQuery: (sql: string, params?: unknown[]) => Promise<T>,
|
|
||||||
): Promise<void> {
|
|
||||||
// Extract table name and column name from ALTER TABLE statement
|
|
||||||
const alterMatch = migration.sql.match(/ALTER TABLE\s+(\w+)\s+ADD COLUMN\s+(\w+)/i);
|
|
||||||
|
|
||||||
if (!alterMatch) {
|
|
||||||
// If we can't parse it, just try to execute normally
|
|
||||||
await sqlExec(migration.sql);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [, tableName, columnName] = alterMatch;
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Check if column already exists
|
|
||||||
const columnCheckResult = await sqlQuery(
|
|
||||||
`SELECT COUNT(*) as count FROM pragma_table_info('${tableName}') WHERE name = ?`,
|
|
||||||
[columnName]
|
|
||||||
) as any;
|
|
||||||
|
|
||||||
const columnExists = columnCheckResult?.values?.[0]?.count > 0 ||
|
|
||||||
columnCheckResult?.count > 0 ||
|
|
||||||
(Array.isArray(columnCheckResult) && columnCheckResult.length > 0);
|
|
||||||
|
|
||||||
if (columnExists) {
|
|
||||||
console.log(`⏭️ [Migration] Column ${columnName} already exists in table ${tableName}, skipping`);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Column doesn't exist, so add it
|
|
||||||
console.log(`🔄 [Migration] Adding column ${columnName} to table ${tableName}`);
|
|
||||||
await sqlExec(migration.sql);
|
|
||||||
console.log(`✅ [Migration] Successfully added column ${columnName} to table ${tableName}`);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
// If the column check fails, try the original migration and let error handling catch duplicates
|
|
||||||
console.log(`⚠️ [Migration] Column check failed, attempting migration anyway: ${error}`);
|
|
||||||
await sqlExec(migration.sql);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -242,17 +242,50 @@ export class CapacitorPlatformService implements PlatformService {
|
|||||||
throw new Error("Database not initialized");
|
throw new Error("Database not initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
const sqlExec: (sql: string) => Promise<capSQLiteChanges> =
|
const sqlExec = async (sql: string, params?: unknown[]): Promise<capSQLiteChanges> => {
|
||||||
this.db.execute.bind(this.db);
|
console.log(`🔧 [CapacitorMigration] Executing SQL:`, sql);
|
||||||
const sqlQuery: (sql: string) => Promise<DBSQLiteValues> =
|
console.log(`📋 [CapacitorMigration] With params:`, params);
|
||||||
this.db.query.bind(this.db);
|
|
||||||
const extractMigrationNames: (result: DBSQLiteValues) => Set<string> = (
|
if (params && params.length > 0) {
|
||||||
result,
|
// Use run method for parameterized queries
|
||||||
) => {
|
const result = await this.db!.run(sql, params);
|
||||||
const names =
|
console.log(`✅ [CapacitorMigration] Run result:`, result);
|
||||||
result.values?.map((row: { name: string }) => row.name) || [];
|
return result;
|
||||||
|
} else {
|
||||||
|
// Use execute method for non-parameterized queries
|
||||||
|
const result = await this.db!.execute(sql);
|
||||||
|
console.log(`✅ [CapacitorMigration] Execute result:`, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const sqlQuery = async (sql: string, params?: unknown[]): Promise<DBSQLiteValues> => {
|
||||||
|
console.log(`🔍 [CapacitorMigration] Querying SQL:`, sql);
|
||||||
|
console.log(`📋 [CapacitorMigration] With params:`, params);
|
||||||
|
|
||||||
|
const result = await this.db!.query(sql, params);
|
||||||
|
console.log(`📊 [CapacitorMigration] Query result:`, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractMigrationNames = (result: DBSQLiteValues): Set<string> => {
|
||||||
|
console.log(`🔍 [CapacitorMigration] Extracting migration names from:`, result);
|
||||||
|
|
||||||
|
// Handle the Capacitor SQLite result format
|
||||||
|
const names = result.values?.map((row: any) => {
|
||||||
|
// The row could be an object with 'name' property or an array where name is first element
|
||||||
|
if (typeof row === 'object' && row.name !== undefined) {
|
||||||
|
return row.name;
|
||||||
|
} else if (Array.isArray(row) && row.length > 0) {
|
||||||
|
return row[0];
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}).filter(name => name !== null) || [];
|
||||||
|
|
||||||
|
console.log(`📋 [CapacitorMigration] Extracted names:`, names);
|
||||||
return new Set(names);
|
return new Set(names);
|
||||||
};
|
};
|
||||||
|
|
||||||
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
await runMigrations(sqlExec, sqlQuery, extractMigrationNames);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user