Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/poor-foxes-fall.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sqlseal": patch
---

fixing issue with config table not found on first run
2 changes: 0 additions & 2 deletions src/modules/database/factory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { App } from "obsidian";
import { DatabaseProvider } from "./sqlocal";

export const databaseFactory = async (app: App, provider: DatabaseProvider) => {
console.log('#### DATABASE CREATION')

const db = await provider.get(null)
await db.connect()

Expand Down
220 changes: 142 additions & 78 deletions src/modules/database/sqlocal/sqlocalWorkerDatabase.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,55 @@ import { sanitise } from "../../../utils/sanitiseColumn";
// @ts-ignore
import wasmUrl from 'virtual:wa-sqlite-wasm-url';

/**
* Retry an async operation with exponential backoff
* @param operation - The async operation to retry
* @param maxRetries - Maximum number of retry attempts (default: 3)
* @param baseDelay - Base delay in milliseconds for exponential backoff (default: 50)
* @param errorMatcher - Optional function to determine if error should trigger retry
* @returns The result of the successful operation
* @throws The last error if all retries fail
*/
async function retryWithBackoff<T>(
operation: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 50,
errorMatcher?: (error: Error) => boolean
): Promise<T> {
let lastError: Error;

for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
return await operation();
} catch (error) {
lastError = error as Error;

// If errorMatcher provided, only retry matching errors
if (errorMatcher && !errorMatcher(error as Error)) {
throw error;
}

// Don't retry on last attempt
if (attempt === maxRetries) {
break;
}

// Calculate delay with exponential backoff
const delay = baseDelay * Math.pow(2, attempt - 1);
if (process.env.NODE_ENV === 'development') {
console.warn(
`SqlocalWorkerDatabase: Retry attempt ${attempt}/${maxRetries} ` +
`after error: ${lastError.message}. Waiting ${delay}ms...`
);
}

await new Promise(resolve => setTimeout(resolve, delay));
}
}

throw lastError!;
}

/**
* Worker-side database implementation that runs wa-sqlite operations
* in a Web Worker to avoid blocking the main thread.
Expand Down Expand Up @@ -147,7 +196,9 @@ export class SqlocalWorkerDatabase {

registerCustomFunction(name: string, argsCount = 1) {
if (!this.connection) {
console.warn('SqlocalWorkerDatabase: Database not connected, cannot register custom function');
if (process.env.NODE_ENV === 'development') {
console.warn('SqlocalWorkerDatabase: Database not connected, cannot register custom function');
}
return Promise.resolve();
}

Expand Down Expand Up @@ -524,94 +575,107 @@ export class SqlocalWorkerDatabase {
async select(statement: string, frontmatter: Record<string, unknown>) {
if (!this.connection) throw new Error('Database not connected');
if (this.isRecreating) {
console.warn('SqlocalWorkerDatabase: Database is being recreated, cannot execute select');
if (process.env.NODE_ENV === 'development') {
console.warn('SqlocalWorkerDatabase: Database is being recreated, cannot execute select');
}
return { data: [], columns: [], executionTime: 0 };
}

try {
// Replace frontmatter placeholders in the query
let processedStatement = statement;
const params: any[] = [];

// Support both {{key}} and @key parameter formats for compatibility
for (const [key, value] of Object.entries(frontmatter)) {
// Handle {{key}} format (used in user queries)
const doubleBracePlaceholder = `{{${key}}}`;
const doubleBraceRegex = new RegExp(doubleBracePlaceholder.replace(/[{}]/g, '\\$&'), 'g');
const doubleBraceMatches = (processedStatement.match(doubleBraceRegex) || []).length;
if (doubleBraceMatches > 0) {
processedStatement = processedStatement.replace(doubleBraceRegex, '?');
for (let i = 0; i < doubleBraceMatches; i++) {
params.push(value);
}
}
// Wrap the select operation with retry logic
return retryWithBackoff(
async () => {
try {
// Replace frontmatter placeholders in the query
let processedStatement = statement;
const params: any[] = [];

// Support both {{key}} and @key parameter formats for compatibility
for (const [key, value] of Object.entries(frontmatter)) {
// Handle {{key}} format (used in user queries)
const doubleBracePlaceholder = `{{${key}}}`;
const doubleBraceRegex = new RegExp(doubleBracePlaceholder.replace(/[{}]/g, '\\$&'), 'g');
const doubleBraceMatches = (processedStatement.match(doubleBraceRegex) || []).length;
if (doubleBraceMatches > 0) {
processedStatement = processedStatement.replace(doubleBraceRegex, '?');
for (let i = 0; i < doubleBraceMatches; i++) {
params.push(value);
}
}

// Handle @key format (used in repository queries)
const atPlaceholder = `@${key}`;
const atRegex = new RegExp(`@${key}\\b`, 'g');
const atMatches = (processedStatement.match(atRegex) || []).length;
if (atMatches > 0) {
processedStatement = processedStatement.replace(atRegex, '?');
for (let i = 0; i < atMatches; i++) {
params.push(value);
// Handle @key format (used in repository queries)
const atPlaceholder = `@${key}`;
const atRegex = new RegExp(`@${key}\\b`, 'g');
const atMatches = (processedStatement.match(atRegex) || []).length;
if (atMatches > 0) {
processedStatement = processedStatement.replace(atRegex, '?');
for (let i = 0; i < atMatches; i++) {
params.push(value);
}
}
}
}
}

const startTime = performance.now();
const data: any[] = [];
let columns: string[] = [];

// Create string in WASM memory
const str = this.sqlite3.str_new(this.connection, processedStatement);
let prepared = null;
try {
prepared = await this.sqlite3.prepare_v2(this.connection, this.sqlite3.str_value(str));
const startTime = performance.now();
const data: any[] = [];
let columns: string[] = [];

if (prepared && prepared.stmt) {
await this.sqlite3.bind_collection(prepared.stmt, params);

// Get column names
const columnCount = await this.sqlite3.column_count(prepared.stmt);

for (let i = 0; i < columnCount; i++) {
columns.push(await this.sqlite3.column_name(prepared.stmt, i));
}

// Fetch all rows
let stepResult;
while ((stepResult = await this.sqlite3.step(prepared.stmt)) === SQLite.SQLITE_ROW) {
const row: any = {};
for (let i = 0; i < columnCount; i++) {
const columnName = columns[i];
row[columnName] = await this.sqlite3.column(prepared.stmt, i);
}
data.push(row);
}
}
} finally {
// Finalize statement before finishing string
if (prepared && prepared.stmt) {
// Create string in WASM memory
const str = this.sqlite3.str_new(this.connection, processedStatement);
let prepared = null;
try {
await this.sqlite3.finalize(prepared.stmt);
} catch (finalizeError) {
console.error('SqlocalWorkerDatabase: Error finalizing statement:', finalizeError);
prepared = await this.sqlite3.prepare_v2(this.connection, this.sqlite3.str_value(str));

if (prepared && prepared.stmt) {
await this.sqlite3.bind_collection(prepared.stmt, params);

// Get column names
const columnCount = await this.sqlite3.column_count(prepared.stmt);

for (let i = 0; i < columnCount; i++) {
columns.push(await this.sqlite3.column_name(prepared.stmt, i));
}

// Fetch all rows
let stepResult;
while ((stepResult = await this.sqlite3.step(prepared.stmt)) === SQLite.SQLITE_ROW) {
const row: any = {};
for (let i = 0; i < columnCount; i++) {
const columnName = columns[i];
row[columnName] = await this.sqlite3.column(prepared.stmt, i);
}
data.push(row);
}
}
} finally {
// Finalize statement before finishing string
if (prepared && prepared.stmt) {
try {
await this.sqlite3.finalize(prepared.stmt);
} catch (finalizeError) {
console.error('SqlocalWorkerDatabase: Error finalizing statement:', finalizeError);
}
}
this.sqlite3.str_finish(str);
}

const executionTime = performance.now() - startTime;
return {
data,
columns,
executionTime
};
} catch (error) {
console.error('SqlocalWorkerDatabase: Error executing select:', error);
console.error('SqlocalWorkerDatabase: Failed statement:', statement);
throw error;
}
this.sqlite3.str_finish(str);
},
3, // maxRetries
50, // baseDelay (50ms, 100ms, 200ms pattern)
(error: Error) => {
// Only retry on "no such table" errors
return error.message.toLowerCase().includes('no such table');
}

const executionTime = performance.now() - startTime;
return {
data,
columns,
executionTime
};
} catch (error) {
console.error('SqlocalWorkerDatabase: Error executing select:', error);
console.error('SqlocalWorkerDatabase: Failed statement:', statement);
throw error;
}
);
}

async explain(statement: string, frontmatter: Record<string, unknown>) {
Expand Down
3 changes: 0 additions & 3 deletions src/modules/main/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,5 @@ export const mainInit = (
apiInit();
globalTablesInit();
explorerInit();

console.log('🚀 SQL Seal initialized with wa-sqlite test command available');
console.log('📋 Use Ctrl/Cmd+P -> "Test wa-sqlite Implementation" to test wa-sqlite');
};
};
2 changes: 1 addition & 1 deletion src/modules/main/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ const obsidian = new Registrator(process.env.NODE_ENV === 'development' ? { logg
.export('app', 'plugin', 'vault')


export const mainModule = new Registrator({logger: console.log})
export const mainModule = new Registrator(process.env.NODE_ENV === 'development' ? {logger: console.log} : undefined)
.module('obsidian', obsidian)
.module('db', db)
.module('editor', editor)
Expand Down
4 changes: 2 additions & 2 deletions src/modules/sync/sync/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ export class Sync {

// Configuration
this.configRepo = new ConfigurationRepository(this.db)
await this.configRepo.init()

let version
try {
Expand All @@ -78,10 +79,9 @@ export class Sync {

if (version < SQLSEAL_DATABASE_VERSION) {
await this.db.recreateDatabase()
await this.configRepo.init()
}

await this.configRepo.init()

this.tableDefinitionsRepo = new TableDefinitionsRepository(this.db)
await this.tableDefinitionsRepo.init()

Expand Down
1 change: 0 additions & 1 deletion src/modules/syntaxHighlight/cellParser/ModernCellParser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,7 +173,6 @@ export class ModernCellParser {

// FIXME: this should be extracted to separate class / function but for now it's fine.
registerDbFunctions(db: SqlocalDatabaseProxy) {
console.trace('register db functions called')
this.functions.forEach(funct => {
db.registerCustomFunction(funct.name, funct.sqlFunctionArgumentsCount)
})
Expand Down
19 changes: 17 additions & 2 deletions src/utils/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,15 @@ export class Logger {
} else {
this.console = {
log: () => { },
error: () => { }
error: () => { },
warn: () => { },
debug: () => { },
trace: () => { }
}
}
}

private console: Pick<typeof console, 'log' | 'error'>
private console: Pick<typeof console, 'log' | 'error' | 'warn' | 'debug' | 'trace'>

log(...args: any[]) {
this.console.log(...args)
Expand All @@ -19,4 +22,16 @@ export class Logger {
error(...args: any[]) {
this.console.error(...args)
}

warn(...args: any[]) {
this.console.warn(...args)
}

debug(...args: any[]) {
this.console.debug(...args)
}

trace(...args: any[]) {
this.console.trace(...args)
}
}
Loading