diff --git a/packages/loot-core/src/platform/server/sqlite/index.electron.ts b/packages/loot-core/src/platform/server/sqlite/index.electron.ts index 67bcacaa504..ac4224640e4 100644 --- a/packages/loot-core/src/platform/server/sqlite/index.electron.ts +++ b/packages/loot-core/src/platform/server/sqlite/index.electron.ts @@ -84,7 +84,16 @@ export async function asyncTransaction(db, fn) { } export function openDatabase(pathOrBuffer) { - return new Database(pathOrBuffer); + let db = new Database(pathOrBuffer); + // Define Unicode-aware LOWER and UPPER implementation. + // This is necessary because better-sqlite3 uses SQLite build without ICU support. + db.function('UNICODE_LOWER', { deterministic: true }, arg => + arg?.toLowerCase(), + ); + db.function('UNICODE_UPPER', { deterministic: true }, arg => + arg?.toUpperCase(), + ); + return db; } export function closeDatabase(db) { diff --git a/packages/loot-core/src/platform/server/sqlite/index.web.ts b/packages/loot-core/src/platform/server/sqlite/index.web.ts index 0ad16c16db7..8a2b3c4a79f 100644 --- a/packages/loot-core/src/platform/server/sqlite/index.web.ts +++ b/packages/loot-core/src/platform/server/sqlite/index.web.ts @@ -151,36 +151,48 @@ export async function asyncTransaction(db, fn) { } export async function openDatabase(pathOrBuffer?: string | Buffer) { + let db = null; if (pathOrBuffer) { if (typeof pathOrBuffer !== 'string') { - return new SQL.Database(pathOrBuffer); - } - - let path = pathOrBuffer; - if (path !== ':memory:') { - if (typeof SharedArrayBuffer === 'undefined') { - // @ts-expect-error FS missing in sql.js types - let stream = SQL.FS.open(SQL.FS.readlink(path), 'a+'); - await stream.node.contents.readIfFallback(); - // @ts-expect-error FS missing in sql.js types - SQL.FS.close(stream); + db = new SQL.Database(pathOrBuffer); + } else { + let path = pathOrBuffer; + if (path !== ':memory:') { + if (typeof SharedArrayBuffer === 'undefined') { + // @ts-expect-error FS missing in sql.js types + let stream = SQL.FS.open(SQL.FS.readlink(path), 'a+'); + await stream.node.contents.readIfFallback(); + // @ts-expect-error FS missing in sql.js types + SQL.FS.close(stream); + } + + db = new SQL.Database( + // @ts-expect-error FS missing in sql.js types + path.includes('/blocked') ? path : SQL.FS.readlink(path), + // @ts-expect-error 2nd argument missed in sql.js types + { filename: true }, + ); + db.exec(` + PRAGMA journal_mode=MEMORY; + PRAGMA cache_size=-10000; + `); } - - let db = new SQL.Database( - // @ts-expect-error FS missing in sql.js types - path.includes('/blocked') ? path : SQL.FS.readlink(path), - // @ts-expect-error 2nd argument missed in sql.js types - { filename: true }, - ); - db.exec(` - PRAGMA journal_mode=MEMORY; - PRAGMA cache_size=-10000; - `); - return db; } } - return new SQL.Database(); + if (db === null) { + db = new SQL.Database(); + } + + // Define Unicode-aware LOWER and UPPER implementation. + // This is necessary because sql.js uses SQLite build without ICU support. + // + // Note that this function should ideally be created with a deterministic flag + // to allow SQLite to better optimize calls to it by factoring them out of inner loops + // but SQL.js does not support this: https://github.com/sql-js/sql.js/issues/551 + db.create_function('UNICODE_LOWER', arg => arg?.toLowerCase()); + db.create_function('UNICODE_UPPER', arg => arg?.toUpperCase()); + return db; } export function closeDatabase(db) { diff --git a/packages/loot-core/src/server/accounts/payees.js b/packages/loot-core/src/server/accounts/payees.js index 23f726f3efb..9c4c55d0c3d 100644 --- a/packages/loot-core/src/server/accounts/payees.js +++ b/packages/loot-core/src/server/accounts/payees.js @@ -4,7 +4,7 @@ export async function createPayee(description) { // Check to make sure no payee already exists with exactly the same // name let row = await db.first( - `SELECT id FROM payees WHERE LOWER(name) = ? AND tombstone = 0`, + `SELECT id FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`, [description.toLowerCase()], ); diff --git a/packages/loot-core/src/server/aql/compiler.js b/packages/loot-core/src/server/aql/compiler.js index 47564fa3e53..fce92d9ebe5 100644 --- a/packages/loot-core/src/server/aql/compiler.js +++ b/packages/loot-core/src/server/aql/compiler.js @@ -564,7 +564,7 @@ const compileFunction = saveStack('function', (state, func) => { case '$lower': { validateArgLength(args, 1); let [arg1] = valArray(state, args, ['string']); - return typed(`LOWER(${arg1})`, 'string'); + return typed(`UNICODE_LOWER(${arg1})`, 'string'); } // integer/float functions diff --git a/packages/loot-core/src/server/db/index.js b/packages/loot-core/src/server/db/index.js index fc406c145ae..c3901d39b79 100644 --- a/packages/loot-core/src/server/db/index.js +++ b/packages/loot-core/src/server/db/index.js @@ -516,9 +516,10 @@ export async function getOrphanedPayees() { } export async function getPayeeByName(name) { - return first(`SELECT * FROM payees WHERE LOWER(name) = ? AND tombstone = 0`, [ - name.toLowerCase(), - ]); + return first( + `SELECT * FROM payees WHERE UNICODE_LOWER(name) = ? AND tombstone = 0`, + [name.toLowerCase()], + ); } export function insertPayeeRule(rule) { diff --git a/upcoming-release-notes/865.md b/upcoming-release-notes/865.md new file mode 100644 index 00000000000..e670684dcf1 --- /dev/null +++ b/upcoming-release-notes/865.md @@ -0,0 +1,6 @@ +--- +category: Bugfix +authors: [Jackenmen] +--- + +Fix case-insensitive matching of strings for uppercase letters from non-English alphabets