5 Commits

Author SHA1 Message Date
b74dc5b3c6 Remove unused 2025-05-16 15:44:51 +02:00
474cb662e5 Remove fallback to like 2025-05-16 15:41:28 +02:00
a84973def6 Fix cyrillic 2025-05-16 15:32:58 +02:00
fcb8e41cde Add icu support for cyrillic fts 2025-05-16 14:56:22 +02:00
4f54b89689 Add a date to latest 2025-04-24 10:03:43 +02:00
4 changed files with 255 additions and 37 deletions

Binary file not shown.

View File

@@ -6,10 +6,23 @@ import 'package:sqflite_common_ffi/sqflite_ffi.dart';
const settingsDir = '.journaler'; const settingsDir = '.journaler';
const dbFileName = 'journaler.db'; const dbFileName = 'journaler.db';
// Add this at the top level
typedef ShowMessageCallback = void Function(String message);
class DB { class DB {
static late Database db; static late Database db;
static bool _hasIcuSupport = false;
static ShowMessageCallback? _showMessage;
static const String _schema = ''' // Add this to register the callback
static void registerMessageCallback(ShowMessageCallback callback) {
_showMessage = callback;
}
// Add this method to check ICU status
static bool get hasIcuSupport => _hasIcuSupport;
static const String _baseSchema = '''
CREATE TABLE IF NOT EXISTS notes ( CREATE TABLE IF NOT EXISTS notes (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT DEFAULT CURRENT_TIMESTAMP, date TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -17,10 +30,7 @@ CREATE TABLE IF NOT EXISTS notes (
); );
CREATE INDEX IF NOT EXISTS idx_notes_date ON notes (date); CREATE INDEX IF NOT EXISTS idx_notes_date ON notes (date);
CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_date_unique ON notes (date); CREATE UNIQUE INDEX IF NOT EXISTS idx_notes_date_unique ON notes (date);
-- Todos are "static", we are only interested in the latest entry
-- ie. they're not really a list but instead a string
-- But we will also keep a history of all todos
-- Because we might as well
CREATE TABLE IF NOT EXISTS scratches ( CREATE TABLE IF NOT EXISTS scratches (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
date TEXT DEFAULT CURRENT_TIMESTAMP, date TEXT DEFAULT CURRENT_TIMESTAMP,
@@ -33,15 +43,31 @@ CREATE TABLE IF NOT EXISTS settings (
key TEXT PRIMARY KEY NOT NULL, key TEXT PRIMARY KEY NOT NULL,
value TEXT NOT NULL value TEXT NOT NULL
); );
''';
-- Create virtual FTS5 table for searching notes content static const String _ftsSchemaWithIcu = '''
-- Create virtual FTS5 table with Unicode tokenizer
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5( CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
content, content,
date, date,
content='notes', content='notes',
content_rowid='id' content_rowid='id',
tokenize='unicode61 remove_diacritics 2 tokenchars "\u0401\u0451\u0410-\u044f"'
); );
''';
static const String _ftsSchemaBasic = '''
-- Create virtual FTS5 table with basic tokenizer
CREATE VIRTUAL TABLE IF NOT EXISTS notes_fts USING fts5(
content,
date,
content='notes',
content_rowid='id',
tokenize='unicode61'
);
''';
static const String _ftsTriggers = '''
-- Trigger to keep FTS table in sync with notes table when inserting -- Trigger to keep FTS table in sync with notes table when inserting
CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN CREATE TRIGGER IF NOT EXISTS notes_ai AFTER INSERT ON notes BEGIN
INSERT INTO notes_fts(rowid, content, date) VALUES (new.id, new.content, new.date); INSERT INTO notes_fts(rowid, content, date) VALUES (new.id, new.content, new.date);
@@ -58,6 +84,124 @@ CREATE TRIGGER IF NOT EXISTS notes_au AFTER UPDATE ON notes BEGIN
END; END;
'''; ''';
// Add this method to check ICU status with visible feedback
static Future<bool> checkAndShowIcuStatus() async {
final status = _hasIcuSupport;
final message =
status
? 'ICU support is enabled - Full Unicode search available'
: 'ICU support is not available - Unicode search will be limited\n'
'Looking for sqlite3_icu.dll in the application directory';
debugPrint(message);
_showMessage?.call(message);
return status;
}
static Future<bool> _checkIcuSupport(Database db) async {
try {
debugPrint(
'\n================== CHECKING UNICODE SUPPORT ==================',
);
// Test Unicode support with a simple query
try {
debugPrint('Testing Unicode support...');
// Test with some Cyrillic characters
await db.rawQuery("SELECT 'тест' LIKE '%ест%'");
await db.rawQuery("SELECT 'ТЕСТ' LIKE '%ЕСТ%'");
final message = 'Unicode support is available';
debugPrint(message);
_showMessage?.call(message);
return true;
} catch (e, stackTrace) {
final message = 'Unicode support test failed';
debugPrint('$message:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
// Try to get SQLite version and compile options for debugging
try {
final version = await db.rawQuery('SELECT sqlite_version()');
final compileOpts = await db.rawQuery('PRAGMA compile_options');
debugPrint('SQLite version: $version');
debugPrint('SQLite compile options: $compileOpts');
} catch (e) {
debugPrint('Could not get SQLite info: $e');
}
_showMessage?.call(message);
return false;
}
} catch (e, stackTrace) {
final message = 'Failed to test Unicode support';
debugPrint('$message:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
_showMessage?.call(message);
return false;
} finally {
debugPrint('=====================================================\n');
}
}
static Future<void> _recreateFtsTable(Database db, bool useIcu) async {
debugPrint('Updating FTS table configuration...');
try {
// Start a transaction to ensure data safety
await db.transaction((txn) async {
// First, create a temporary table with the new configuration
final tempTableName = 'notes_fts_temp';
final schema = useIcu ? _ftsSchemaWithIcu : _ftsSchemaBasic;
// Create temp table with new configuration
await txn.execute(schema.replaceAll('notes_fts', tempTableName));
// Copy data from old FTS table if it exists
try {
debugPrint('Copying existing FTS data to temporary table...');
await txn.execute('''
INSERT INTO $tempTableName(rowid, content, date)
SELECT rowid, content, date FROM notes_fts
''');
debugPrint('Data copied successfully');
} catch (e) {
debugPrint('No existing FTS data to copy: $e');
}
// Drop old triggers
debugPrint('Updating triggers...');
await txn.execute('DROP TRIGGER IF EXISTS notes_ai');
await txn.execute('DROP TRIGGER IF EXISTS notes_ad');
await txn.execute('DROP TRIGGER IF EXISTS notes_au');
// Drop old FTS table
await txn.execute('DROP TABLE IF EXISTS notes_fts');
// Rename temp table to final name
await txn.execute('ALTER TABLE $tempTableName RENAME TO notes_fts');
// Create new triggers
await txn.execute(_ftsTriggers);
// Rebuild FTS index from notes table to ensure consistency
debugPrint('Rebuilding FTS index from notes table...');
await txn.execute('''
INSERT OR REPLACE INTO notes_fts(rowid, content, date)
SELECT id, content, date FROM notes
''');
debugPrint('FTS table update completed successfully');
});
} catch (e, stackTrace) {
debugPrint('Error updating FTS table:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
}
static Future<String> _getDatabasePath() async { static Future<String> _getDatabasePath() async {
debugPrint('Attempting to get database path...'); debugPrint('Attempting to get database path...');
if (Platform.isWindows || Platform.isLinux) { if (Platform.isWindows || Platform.isLinux) {
@@ -88,26 +232,100 @@ END;
static Future<void> init() async { static Future<void> init() async {
debugPrint('Starting database initialization...'); debugPrint('Starting database initialization...');
// Initialize SQLite FFI
sqfliteFfiInit(); sqfliteFfiInit();
final databaseFactory = databaseFactoryFfi;
// Create a temporary database to check version
try {
debugPrint(
'\n================== SQLITE VERSION CHECK ==================',
);
final tempDb = await databaseFactory.openDatabase(
':memory:',
options: OpenDatabaseOptions(
version: 1,
onCreate: (db, version) async {
final results = await db.rawQuery('SELECT sqlite_version()');
debugPrint('SQLite version: ${results.first.values.first}');
final compileOpts = await db.rawQuery('PRAGMA compile_options');
debugPrint('SQLite compile options:');
for (var opt in compileOpts) {
debugPrint(' ${opt.values.first}');
}
},
),
);
await tempDb.close();
debugPrint('=====================================================\n');
} catch (e, stackTrace) {
debugPrint('Error checking SQLite version:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
}
await databaseFactory.setDatabasesPath(
await databaseFactory.getDatabasesPath(),
);
final dbPath = await _getDatabasePath(); final dbPath = await _getDatabasePath();
debugPrint('Database path: $dbPath'); debugPrint('Database path: $dbPath');
try { try {
db = await databaseFactoryFfi.openDatabase( db = await databaseFactory.openDatabase(
dbPath, dbPath,
options: OpenDatabaseOptions( options: OpenDatabaseOptions(
version: 1, version: 2,
onConfigure: (db) async {
debugPrint('Configuring database...');
await db.execute('PRAGMA foreign_keys = ON');
debugPrint('Database configured');
},
onCreate: (db, version) async { onCreate: (db, version) async {
debugPrint('Creating database schema...'); debugPrint('Creating database schema...');
await db.execute(_schema); await db.execute(_baseSchema);
// Check for Unicode support on first creation
_hasIcuSupport = await _checkIcuSupport(db);
await _recreateFtsTable(db, _hasIcuSupport);
debugPrint('Database schema created successfully'); debugPrint('Database schema created successfully');
}, },
onOpen: (db) async {
debugPrint('Database opened, checking Unicode support...');
try {
_hasIcuSupport = await _checkIcuSupport(db);
debugPrint('Unicode support check completed: $_hasIcuSupport');
} catch (e, stackTrace) {
debugPrint('Error during Unicode support check:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
rethrow;
}
},
onUpgrade: (db, oldVersion, newVersion) async {
debugPrint('Upgrading database from $oldVersion to $newVersion');
if (oldVersion < 2) {
// Check for Unicode support during upgrade
_hasIcuSupport = await _checkIcuSupport(db);
await _recreateFtsTable(db, _hasIcuSupport);
}
},
), ),
); );
debugPrint('Database opened and initialized');
} catch (e) { // Store Unicode support status in settings for future reference
debugPrint('Failed to initialize database: $e'); await setSetting('has_icu_support', _hasIcuSupport.toString());
debugPrint(
'Database opened and initialized (Unicode support: $_hasIcuSupport)',
);
} catch (e, stackTrace) {
debugPrint('Failed to initialize database:');
debugPrint('Error: $e');
debugPrint('Stack trace: $stackTrace');
rethrow; rethrow;
} }
} }
@@ -141,10 +359,8 @@ END;
return []; return [];
} }
// Process the query for partial word matching
// Split into individual terms, filter empty ones // Split into individual terms, filter empty ones
List<String> terms = List<String> terms = query
query
.trim() .trim()
.split(RegExp(r'\s+')) .split(RegExp(r'\s+'))
.where((term) => term.isNotEmpty) .where((term) => term.isNotEmpty)
@@ -154,16 +370,16 @@ END;
return []; return [];
} }
// Add wildcards to each term for prefix matching (e.g., "fuck*" will match "fucked") // Process terms for FTS5 query using proper tokenization
// Join terms with AND for all-term matching (results must contain ALL terms)
String ftsQuery = terms String ftsQuery = terms
.map((term) { .map((term) {
// Remove any special characters that might break the query // Remove dangerous characters but preserve Unicode
String sanitizedTerm = term.replaceAll(RegExp(r'[^\w]'), ''); String sanitizedTerm = term.replaceAll(RegExp(r'''['"]'''), '');
if (sanitizedTerm.isEmpty) return ''; if (sanitizedTerm.isEmpty) return '';
// Add wildcard for stemming/prefix matching // Use proper FTS5 syntax: each word becomes a separate token with prefix matching
return '$sanitizedTerm*'; List<String> words = sanitizedTerm.split(RegExp(r'\s+'));
return words.map((word) => '$word*').join(' OR ');
}) })
.where((term) => term.isNotEmpty) .where((term) => term.isNotEmpty)
.join(' AND '); .join(' AND ');
@@ -175,24 +391,25 @@ END;
debugPrint('FTS query: "$ftsQuery"'); debugPrint('FTS query: "$ftsQuery"');
// Execute the FTS query with AND logic // Execute the FTS query
final List<Map<String, dynamic>> results = await db.rawQuery( final List<Map<String, dynamic>> results = await db.rawQuery(
''' '''
SELECT n.id, n.date, n.content, snippet(notes_fts, 0, '<b>', '</b>', '...', 20) as snippet SELECT n.id, n.date, n.content,
snippet(notes_fts, -1, '<b>', '</b>', '...', 64) as snippet
FROM notes_fts FROM notes_fts
JOIN notes n ON notes_fts.rowid = n.id JOIN notes n ON notes_fts.rowid = n.id
WHERE notes_fts MATCH ? WHERE notes_fts MATCH ?
ORDER BY n.date DESC ORDER BY rank
LIMIT 100 LIMIT 100
''', ''',
[ftsQuery], [ftsQuery],
); );
debugPrint('Search query "$ftsQuery" returned ${results.length} results'); debugPrint('Search returned ${results.length} results');
return results; return results;
} catch (e) { } catch (e, stackTrace) {
debugPrint('Search failed: $e'); debugPrint('Search failed: $e');
// Return empty results rather than crashing on malformed queries debugPrint('Stack trace: $stackTrace');
return []; return [];
} }
} }

View File

@@ -1053,7 +1053,7 @@ class MainPageState extends State<MainPage> with WindowListener {
child: Text( child: Text(
_currentlyDisplayedNote?.displayDate == _currentlyDisplayedNote?.displayDate ==
previousNote?.displayDate previousNote?.displayDate
? 'Previous Entry (Latest)' ? 'Previous Entry: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}'
: 'Entry: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}', : 'Entry: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}',
style: TextStyle( style: TextStyle(
fontSize: 18, fontSize: 18,
@@ -1086,7 +1086,7 @@ class MainPageState extends State<MainPage> with WindowListener {
_currentlyDisplayedNote?.displayDate != _currentlyDisplayedNote?.displayDate !=
previousNote?.displayDate previousNote?.displayDate
? 'Viewing note from ${_currentlyDisplayedNote?.displayDate} (Editable)' ? 'Viewing note from ${_currentlyDisplayedNote?.displayDate} (Editable)'
: 'Latest Note', : 'Latest Note: ${_currentlyDisplayedNote?.displayDate ?? 'N/A'}',
border: const OutlineInputBorder(), border: const OutlineInputBorder(),
filled: filled:
_currentlyDisplayedNote?.displayDate != _currentlyDisplayedNote?.displayDate !=

View File

@@ -69,6 +69,7 @@ flutter:
assets: assets:
- assets/ # Include the main assets directory for the icon - assets/ # Include the main assets directory for the icon
- assets/sounds/ - assets/sounds/
- assets/windows/sqlite3_icu.dll
# - images/a_dot_burr.jpeg # - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg # - images/a_dot_ham.jpeg