Fix Android database initialization issue with platform-specific approach

This commit is contained in:
DocNR 2025-04-03 22:18:02 -04:00
parent ff8851bd04
commit 3e4f304f56
4 changed files with 320 additions and 9 deletions

View File

@ -5,6 +5,20 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Documentation
- Added comprehensive React Query integration plan to address authentication state transitions and hook ordering issues
- Created detailed technical documentation for integrating React Query with SQLite, NDK, and Amber signer
- Added detailed conflict resolution strategies for local-first Nostr app
- Implemented enhanced error handling patterns for React Query
- Developed executive summary for stakeholder review
### Fixed
- Android database initialization error (NullPointerException) by:
- Creating a platform-specific database initialization path for Android
- Implementing resilient error handling with step-by-step table creation
- Simplifying SQL statements for better Android compatibility
- Replacing dynamic imports with static imports
### Added ### Added
- Centralized Authentication System with Advanced Security - Centralized Authentication System with Advanced Security
- Implemented new AuthService for unified authentication management - Implemented new AuthService for unified authentication management

View File

@ -1,6 +1,8 @@
// lib/db/schema.ts // lib/db/schema.ts
import { SQLiteDatabase } from 'expo-sqlite'; import { SQLiteDatabase } from 'expo-sqlite';
import { Platform } from 'react-native'; import { Platform } from 'react-native';
// Import the migration functions directly to avoid dynamic imports that can fail on Android
import { addNostrFieldsToWorkouts, createNostrWorkoutsTable } from './migrations/add-nostr-fields-to-workouts';
export const SCHEMA_VERSION = 12; export const SCHEMA_VERSION = 12;
@ -132,10 +134,7 @@ class Schema {
try { try {
console.log('[Schema] Running migration v11 - Adding Nostr fields to workouts'); console.log('[Schema] Running migration v11 - Adding Nostr fields to workouts');
// Import the migration functions // Run the migrations using the directly imported functions
const { addNostrFieldsToWorkouts, createNostrWorkoutsTable } = await import('./migrations/add-nostr-fields-to-workouts');
// Run the migrations
await addNostrFieldsToWorkouts(db); await addNostrFieldsToWorkouts(db);
await createNostrWorkoutsTable(db); await createNostrWorkoutsTable(db);
@ -194,10 +193,306 @@ class Schema {
} }
} }
// Method specifically for Android database initialization
async createTablesAndroid(db: SQLiteDatabase): Promise<void> {
try {
console.log('[Schema] Using Android-specific database initialization');
// Create schema_version table separately without transaction
try {
await db.execAsync(`
CREATE TABLE IF NOT EXISTS schema_version (
version INTEGER PRIMARY KEY,
updated_at INTEGER NOT NULL
);
`);
console.log('[Schema] Schema_version table created successfully on Android');
} catch (error) {
console.error('[Schema] Error creating schema_version table on Android:', error);
}
// Get current version
const currentVersion = await this.getCurrentVersion(db);
console.log(`[Schema] Android - Current version: ${currentVersion}, Target version: ${SCHEMA_VERSION}`);
// If already at current version, just check for missing tables
if (currentVersion === SCHEMA_VERSION) {
console.log(`[Schema] Android - Database already at version ${SCHEMA_VERSION}, checking for missing tables`);
await this.ensureCriticalTablesExist(db);
return;
}
// For Android, we'll create tables one by one without a transaction
// 1. Drop all tables except schema_version (if needed)
await this.dropAllTables(db);
// 2. Create tables one by one - simplifying CHECK constraints for Android
try {
// Create exercises table - removed CHECK constraints for Android
console.log('[Schema] Android - Creating exercises table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS exercises (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
category TEXT NOT NULL,
equipment TEXT,
description TEXT,
format_json TEXT,
format_units_json TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
source TEXT NOT NULL DEFAULT 'local',
nostr_event_id TEXT
);
`);
console.log('[Schema] Android - Exercises table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating exercises table:', error);
}
try {
// Create exercise_tags table
console.log('[Schema] Android - Creating exercise_tags table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS exercise_tags (
exercise_id TEXT NOT NULL,
tag TEXT NOT NULL,
FOREIGN KEY(exercise_id) REFERENCES exercises(id) ON DELETE CASCADE,
UNIQUE(exercise_id, tag)
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_exercise_tags ON exercise_tags(tag);`);
console.log('[Schema] Android - Exercise_tags table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating exercise_tags table:', error);
}
try {
// Create nostr_events table
console.log('[Schema] Android - Creating nostr_events table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS nostr_events (
id TEXT PRIMARY KEY,
pubkey TEXT NOT NULL,
kind INTEGER NOT NULL,
created_at INTEGER NOT NULL,
content TEXT NOT NULL,
sig TEXT,
raw_event TEXT NOT NULL,
received_at INTEGER NOT NULL
);
`);
console.log('[Schema] Android - Nostr_events table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating nostr_events table:', error);
}
try {
// Create event_tags table
console.log('[Schema] Android - Creating event_tags table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS event_tags (
event_id TEXT NOT NULL,
name TEXT NOT NULL,
value TEXT NOT NULL,
index_num INTEGER NOT NULL,
FOREIGN KEY(event_id) REFERENCES nostr_events(id) ON DELETE CASCADE
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_event_tags ON event_tags(name, value);`);
console.log('[Schema] Android - Event_tags table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating event_tags table:', error);
}
try {
// Create templates table
console.log('[Schema] Android - Creating templates table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS templates (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
type TEXT NOT NULL,
description TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
nostr_event_id TEXT,
source TEXT NOT NULL DEFAULT 'local',
parent_id TEXT,
is_archived BOOLEAN NOT NULL DEFAULT 0,
author_pubkey TEXT
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_templates_updated_at ON templates(updated_at);`);
console.log('[Schema] Android - Templates table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating templates table:', error);
}
try {
// Create template_exercises table
console.log('[Schema] Android - Creating template_exercises table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS template_exercises (
id TEXT PRIMARY KEY,
template_id TEXT NOT NULL,
exercise_id TEXT NOT NULL,
display_order INTEGER NOT NULL,
target_sets INTEGER,
target_reps INTEGER,
target_weight REAL,
notes TEXT,
created_at INTEGER NOT NULL,
updated_at INTEGER NOT NULL,
FOREIGN KEY(template_id) REFERENCES templates(id) ON DELETE CASCADE
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_template_exercises_template_id ON template_exercises(template_id);`);
console.log('[Schema] Android - Template_exercises table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating template_exercises table:', error);
}
try {
// Create powr_packs table
console.log('[Schema] Android - Creating powr_packs table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS powr_packs (
id TEXT PRIMARY KEY,
title TEXT NOT NULL,
description TEXT,
author_pubkey TEXT,
nostr_event_id TEXT,
import_date INTEGER NOT NULL,
updated_at INTEGER NOT NULL
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_powr_packs_import_date ON powr_packs(import_date DESC);`);
console.log('[Schema] Android - Powr_packs table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating powr_packs table:', error);
}
try {
// Create powr_pack_items table - removed CHECK constraint for Android
console.log('[Schema] Android - Creating powr_pack_items table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS powr_pack_items (
pack_id TEXT NOT NULL,
item_id TEXT NOT NULL,
item_type TEXT NOT NULL,
item_order INTEGER,
is_imported BOOLEAN NOT NULL DEFAULT 0,
nostr_event_id TEXT,
PRIMARY KEY (pack_id, item_id),
FOREIGN KEY (pack_id) REFERENCES powr_packs(id) ON DELETE CASCADE
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_powr_pack_items_type ON powr_pack_items(item_type);`);
console.log('[Schema] Android - Powr_pack_items table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating powr_pack_items table:', error);
}
try {
// Create favorites table
console.log('[Schema] Android - Creating favorites table...');
await db.execAsync(`
CREATE TABLE IF NOT EXISTS favorites (
id TEXT PRIMARY KEY,
content_type TEXT NOT NULL,
content_id TEXT NOT NULL,
content TEXT NOT NULL,
pubkey TEXT,
created_at INTEGER NOT NULL,
UNIQUE(content_type, content_id)
);
`);
await db.execAsync(`CREATE INDEX IF NOT EXISTS idx_favorites_content ON favorites(content_type, content_id);`);
console.log('[Schema] Android - Favorites table created successfully');
} catch (error) {
console.error('[Schema] Android - Error creating favorites table:', error);
}
// Run migrations one by one
if (currentVersion < 8) {
try {
await this.migrate_v8(db);
} catch (error) {
console.error('[Schema] Android - Error in migrate_v8:', error);
}
}
if (currentVersion < 9) {
try {
await this.migrate_v9(db);
} catch (error) {
console.error('[Schema] Android - Error in migrate_v9:', error);
}
}
if (currentVersion < 10) {
try {
await this.migrate_v10(db);
} catch (error) {
console.error('[Schema] Android - Error in migrate_v10:', error);
}
}
if (currentVersion < 11) {
try {
await this.migrate_v11(db);
} catch (error) {
console.error('[Schema] Android - Error in migrate_v11:', error);
}
}
if (currentVersion < 12) {
try {
await this.migrate_v12(db);
} catch (error) {
console.error('[Schema] Android - Error in migrate_v12:', error);
}
}
// Update schema version
try {
await this.updateSchemaVersion(db);
} catch (error) {
console.error('[Schema] Android - Error updating schema version:', error);
}
// Finally, make sure critical tables exist
await this.ensureCriticalTablesExist(db);
console.log('[Schema] Android - Database initialization completed');
} catch (error) {
console.error('[Schema] Android - Error initializing database:', error);
// Don't throw - try to continue with partial initialization
}
}
async createTables(db: SQLiteDatabase): Promise<void> { async createTables(db: SQLiteDatabase): Promise<void> {
try { try {
console.log(`[Schema] Initializing database on ${Platform.OS}`); console.log(`[Schema] Initializing database on ${Platform.OS}`);
// For Android, use a specialized initialization method
if (Platform.OS === 'android') {
await this.createTablesAndroid(db);
return;
}
// iOS and other platforms - use the original implementation
// First, ensure schema_version table exists since we need it for version checking // First, ensure schema_version table exists since we need it for version checking
await db.execAsync(` await db.execAsync(`
CREATE TABLE IF NOT EXISTS schema_version ( CREATE TABLE IF NOT EXISTS schema_version (

9
package-lock.json generated
View File

@ -10,6 +10,7 @@
"hasInstallScript": true, "hasInstallScript": true,
"dependencies": { "dependencies": {
"@expo/cli": "^0.22.16", "@expo/cli": "^0.22.16",
"@expo/config-plugins": "^9.0.17",
"@noble/hashes": "^1.7.1", "@noble/hashes": "^1.7.1",
"@noble/secp256k1": "^2.2.3", "@noble/secp256k1": "^2.2.3",
"@nostr-dev-kit/ndk": "^2.12.0", "@nostr-dev-kit/ndk": "^2.12.0",
@ -60,7 +61,7 @@
"expo-router": "~4.0.19", "expo-router": "~4.0.19",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.22", "expo-splash-screen": "~0.29.22",
"expo-sqlite": "~15.1.3", "expo-sqlite": "~15.1.4",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.9", "expo-system-ui": "~4.0.9",
"jest": "~29.7.0", "jest": "~29.7.0",
@ -12388,9 +12389,9 @@
} }
}, },
"node_modules/expo-sqlite": { "node_modules/expo-sqlite": {
"version": "15.1.3", "version": "15.1.4",
"resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-15.1.3.tgz", "resolved": "https://registry.npmjs.org/expo-sqlite/-/expo-sqlite-15.1.4.tgz",
"integrity": "sha512-YyxU4rBfSo+aLKBbRjlw4SoAkLLbUPpB2XLq+JMwIZrTdVFwr+CvtyNLsC9omevsLXBODXhVkX0Rk3gASag2eg==", "integrity": "sha512-1SG5Qi6/L2SK/o5EKtvEmlMVGdra/wYYh/intI94/ovsUfZGFrDG31YGtTt4rLpE95M6FwHUVpALO8g9/G9B2Q==",
"license": "MIT", "license": "MIT",
"peerDependencies": { "peerDependencies": {
"expo": "*", "expo": "*",

View File

@ -24,6 +24,7 @@
}, },
"dependencies": { "dependencies": {
"@expo/cli": "^0.22.16", "@expo/cli": "^0.22.16",
"@expo/config-plugins": "^9.0.17",
"@noble/hashes": "^1.7.1", "@noble/hashes": "^1.7.1",
"@noble/secp256k1": "^2.2.3", "@noble/secp256k1": "^2.2.3",
"@nostr-dev-kit/ndk": "^2.12.0", "@nostr-dev-kit/ndk": "^2.12.0",
@ -74,7 +75,7 @@
"expo-router": "~4.0.19", "expo-router": "~4.0.19",
"expo-secure-store": "~14.0.1", "expo-secure-store": "~14.0.1",
"expo-splash-screen": "~0.29.22", "expo-splash-screen": "~0.29.22",
"expo-sqlite": "~15.1.3", "expo-sqlite": "~15.1.4",
"expo-status-bar": "~2.0.1", "expo-status-bar": "~2.0.1",
"expo-system-ui": "~4.0.9", "expo-system-ui": "~4.0.9",
"jest": "~29.7.0", "jest": "~29.7.0",