Skip to content

Conversation

@khromov
Copy link
Owner

@khromov khromov commented Oct 21, 2023

@bluepuma77
Copy link

@khromov Does this work? Trying hard myself to get sqlite working native and on web. Just opened an issue for better docs.

@khromov
Copy link
Owner Author

khromov commented Mar 13, 2024

Yes, I've gotten it to work on web more or less but I haven't updated this PR. You have to lock jeep-sqlite at v2.5.3 and @capacitor-community/sqlite at v5.4.1. I recently tried newer versions and they don't work, but haven't figured out what broke. If you can figure it out on the latest version you are welcome to contribute. As far as I can tell everything works normally on iOS/Android, it's just web being a hassle.

Keep in mind it's not a complete example, you'll have to tweak it yourself.

Create the db.ts class below. Call it with something like:

const result = await query(`SELECT * FROM options WHERE name = ? LIMIT 1`, [optionName]);
const option = result?.values?.[0]?.value ?? null;
import { PUBLIC_DB_DEBUG } from '$env/static/public';
import { CapacitorSQLite, SQLiteConnection } from '@capacitor-community/sqlite';
import type {
	DBSQLiteValues,
	SQLiteDBConnection,
	capSQLiteChanges
} from '@capacitor-community/sqlite';
import { isNative, isWeb } from './capacitor';
//import localForage from 'localforage';

let sqlite: SQLiteConnection | undefined = undefined;
let db: SQLiteDBConnection | undefined = undefined;

// https://ionicacademy.com/sqlite-ionic-with-capacitor/
// https://github.com/capacitor-community/sqlite/blob/master/docs/Web-Usage.md

let firstCall = true;

const DB_NAME = 'mydb';

/**
 * Database migrations
 */
const version1 = [
	// options
	`CREATE TABLE "options" (
		"name"	TEXT NOT NULL UNIQUE,
		"value"	TEXT,
		PRIMARY KEY("name")
	)`,
	`CREATE INDEX "options_name_idx" ON "options" (
		"name"
	)`,
];

export async function maybeInitialize(): Promise<any> {
	if (!sqlite) {
		sqlite = new SQLiteConnection(CapacitorSQLite);
	}

	if (isWeb() && firstCall) {
		const i = await import('jeep-sqlite/dist/components/jeep-sqlite');
		customElements.define('jeep-sqlite', i.JeepSqlite);

		console.log('🌐 Initializing Web Jeep SQLite - first call');
		firstCall = false;
		const jeepEl = document.createElement('jeep-sqlite');
		document.body.appendChild(jeepEl);
		const res = await customElements.whenDefined('jeep-sqlite');

		await sqlite.initWebStore();
		console.log('✅ Web Jeep SQLite loaded');
	}

	try {
		await sqlite.checkConnectionsConsistency();
	} catch (e) {
		console.log('❌ Could not check connections consistency', e);
	}

	if (db && (await db?.isDBOpen())) {
		// console.log('⚠️ SQLite already initialized');
		return;
	}

	// Try to retreive existing connection
	try {
		console.log('⚡ Trying to retrieve connection');
		db = await sqlite?.retrieveConnection(DB_NAME, false);
		console.log('⚡ Retrieved connection', db);
	} catch (e) {
		console.log('Could not retrieve connection', e);
	}

	// If no connection exists, create a new one
	if (!db) {
		try {
			console.log('🚀 Initializing SQLite');
			await sqlite.addUpgradeStatement(DB_NAME, 1, version1);
			// await sqlite.addUpgradeStatement(DB_NAME, 2, version2);

			db = await sqlite?.createConnection(DB_NAME, false, 'no-encryption', 1, false);

			try {
				const openResult = await db?.open();

				if (isWeb()) {
					await sqlite.saveToStore(DB_NAME);
				}

				console.log('🚀 SQLite initialized');

				// await db.close();
			} catch (e) {
				console.log('Could not connect to db', e);
			}
		} catch (e) {
			console.log('Could not open db', e);
		}
	}
}

export async function close(): Promise<any> {
	await persistWeb();
	await db?.close();
}

export const persistWeb = async () => {
	if (isWeb()) {
		if (PUBLIC_DB_DEBUG === 'true') {
			console.log('💾 Persisting web DB');
		}
		await sqlite?.saveToStore(DB_NAME);
	}
};

export async function run(query: string, params: any[] = []): Promise<capSQLiteChanges> {
	return queryInternal(query, params, 'run');
}

export async function query(query: string, params: any[] = []): Promise<DBSQLiteValues> {
	return queryInternal(query, params, 'query');
}

export async function queryInternal(
	incomingQuery: string,
	params: any[] = [],
	mode: 'run' | 'query' = 'query'
): Promise<any | null> {
	const timingStart = new Date();

	await maybeInitialize();

	if (PUBLIC_DB_DEBUG === 'true') {
		console.info('----');
		console.info(`🔰 Query: ${incomingQuery}`);
		console.info('📊 Data: ', params);
	}

	if (db?.isDBOpen() && sqlite) {
		let results;
		if (mode === 'run') {
			results = await db?.run(incomingQuery, params);
		} else {
			results = await db?.query(incomingQuery, params);
		}

		// TODO: Probably not needed for selects for example
		const isSelectQuery = incomingQuery.toLowerCase().startsWith('select');

		if (!isSelectQuery) {
			//if (PUBLIC_DB_DEBUG === 'true') {
			//	console.log('📢 Non-select query ran, persisting web DB');
			//}
			await persistWeb();
		}

		if (PUBLIC_DB_DEBUG === 'true') {
			console.info('⏰ Query execution time: %dms', new Date().getTime() - timingStart.getTime());
			console.info('----');
		}

		return results;
	} else {
		return null;
	}
}

export async function deleteDb(): Promise<void> {
	await maybeInitialize();
	await db?.close();
	await CapacitorSQLite.deleteDatabase({
		database: DB_NAME
	});
	await maybeInitialize();
}

export async function exportDb(): Promise<void> {
	if (isNative()) {
		return;
	}
	const localForage = await import('localforage');

	//await db?.flush();
	await persistWeb();
	const newDbInstance = localForage.default.createInstance({
		name: 'jeepSqliteStore',
		storeName: 'databases'
	});
	const data = await newDbInstance.getItem<Uint8Array>(`${DB_NAME}SQLite.db`);
	console.log(newDbInstance);
	if (!data) return;

	const blob = new Blob([data], { type: 'application/vnd.sqlite3' });
	const a = document.createElement('a');
	a.href = window.URL.createObjectURL(blob);
	a.download = `${DB_NAME}.sqlite`;
	a.click();
}

export async function importDb(dbFile: File): Promise<void> {
	if (isNative()) {
		console.log('🚫 Importing a DB is only supported on Web.');
		return;
	}

	const localForage = await import('localforage');
	const reader = new FileReader();

	reader.onload = async (event) => {
		if (!event.target?.result) {
			console.log('❌ Could not read the file.');
			return;
		}

		// Convert result into Uint8Array format
		const data = new Uint8Array(event.target.result as ArrayBuffer);

		// Store the data in localForage
		const newDbInstance = localForage.default.createInstance({
			name: 'jeepSqliteStore',
			storeName: 'databases'
		});
		// Explicitly remove db
		await newDbInstance.removeItem(`${DB_NAME}SQLite.db`);
		await newDbInstance.setItem(`${DB_NAME}SQLite.db`, data);

		// Reinitialize your database connection to reflect the imported data
		await maybeInitialize();
	};

	reader.onerror = () => {
		console.log('❌ Error reading the file.');
	};

	reader.readAsArrayBuffer(dbFile);
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants