Merge feature/virtual-tables: Virtual Tables
This commit is contained in:
commit
df39e0174a
4 changed files with 255 additions and 2 deletions
|
|
@ -4,6 +4,6 @@
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"minAppVersion": "1.5.0",
|
"minAppVersion": "1.5.0",
|
||||||
"description": "Track all vault activity, query with SQL, visualize with charts and dashboards — all backed by SQLite.",
|
"description": "Track all vault activity, query with SQL, visualize with charts and dashboards — all backed by SQLite.",
|
||||||
"author": "Luca",
|
"author": "tolvitty",
|
||||||
"isDesktopOnly": true
|
"isDesktopOnly": true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -262,6 +262,18 @@ export class DatabaseManager {
|
||||||
return stmt.all(...params);
|
return stmt.all(...params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
exec(sql: string): void {
|
||||||
|
this.db.exec(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(sql: string, ...params: unknown[]): void {
|
||||||
|
this.db.prepare(sql).run(...params);
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction<T>(fn: () => T): T {
|
||||||
|
return this.db.transaction(fn)();
|
||||||
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Retention & Maintenance
|
// Retention & Maintenance
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
18
src/main.ts
18
src/main.ts
|
|
@ -15,6 +15,7 @@ import { StatusBar } from './ui/status-bar';
|
||||||
import { EventStreamView, EVENT_STREAM_VIEW_TYPE } from './ui/event-stream-view';
|
import { EventStreamView, EVENT_STREAM_VIEW_TYPE } from './ui/event-stream-view';
|
||||||
import { registerLogfireBlock, registerLogfireSqlBlock, cleanupAllRefreshTimers } from './query/processor';
|
import { registerLogfireBlock, registerLogfireSqlBlock, cleanupAllRefreshTimers } from './query/processor';
|
||||||
import { QueryModal } from './query/query-modal';
|
import { QueryModal } from './query/query-modal';
|
||||||
|
import { VirtualTableManager } from './query/virtual-tables';
|
||||||
|
|
||||||
export default class LogfirePlugin extends Plugin {
|
export default class LogfirePlugin extends Plugin {
|
||||||
settings!: LogfireSettings;
|
settings!: LogfireSettings;
|
||||||
|
|
@ -29,6 +30,7 @@ export default class LogfirePlugin extends Plugin {
|
||||||
private editorCollector!: EditorCollector;
|
private editorCollector!: EditorCollector;
|
||||||
private systemCollector!: SystemCollector;
|
private systemCollector!: SystemCollector;
|
||||||
private statusBar!: StatusBar;
|
private statusBar!: StatusBar;
|
||||||
|
private virtualTables!: VirtualTableManager;
|
||||||
|
|
||||||
private paused = false;
|
private paused = false;
|
||||||
|
|
||||||
|
|
@ -115,7 +117,7 @@ export default class LogfirePlugin extends Plugin {
|
||||||
this.paused = true;
|
this.paused = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Initial scan + maintenance on startup (after layout ready)
|
// Initial scan + maintenance + virtual tables on startup (after layout ready)
|
||||||
this.app.workspace.onLayoutReady(() => {
|
this.app.workspace.onLayoutReady(() => {
|
||||||
if (!this.db.hasBaseline()) {
|
if (!this.db.hasBaseline()) {
|
||||||
this.runInitialScan();
|
this.runInitialScan();
|
||||||
|
|
@ -127,6 +129,10 @@ export default class LogfirePlugin extends Plugin {
|
||||||
console.error('[Logfire] Wartung beim Start fehlgeschlagen:', err);
|
console.error('[Logfire] Wartung beim Start fehlgeschlagen:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Virtual Tables
|
||||||
|
this.virtualTables = new VirtualTableManager(this.app, this.db);
|
||||||
|
this.virtualTables.initialize();
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId);
|
console.log('[Logfire] Plugin geladen. Session:', this.sessionManager.currentSessionId);
|
||||||
|
|
@ -136,6 +142,7 @@ export default class LogfirePlugin extends Plugin {
|
||||||
console.log('[Logfire] Entlade Plugin...');
|
console.log('[Logfire] Entlade Plugin...');
|
||||||
|
|
||||||
cleanupAllRefreshTimers();
|
cleanupAllRefreshTimers();
|
||||||
|
this.virtualTables?.destroy();
|
||||||
this.statusBar?.destroy();
|
this.statusBar?.destroy();
|
||||||
this.stopTracking();
|
this.stopTracking();
|
||||||
|
|
||||||
|
|
@ -282,6 +289,15 @@ export default class LogfirePlugin extends Plugin {
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.addCommand({
|
||||||
|
id: 'refresh-virtual-tables',
|
||||||
|
name: 'Virtual Tables neu aufbauen',
|
||||||
|
callback: () => {
|
||||||
|
this.virtualTables?.rebuild();
|
||||||
|
new Notice('Logfire: Virtual Tables aktualisiert.');
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
this.addCommand({
|
this.addCommand({
|
||||||
id: 'open-query',
|
id: 'open-query',
|
||||||
name: 'Query-Editor \u00f6ffnen',
|
name: 'Query-Editor \u00f6ffnen',
|
||||||
|
|
|
||||||
225
src/query/virtual-tables.ts
Normal file
225
src/query/virtual-tables.ts
Normal file
|
|
@ -0,0 +1,225 @@
|
||||||
|
import { App, TFile, CachedMetadata, TAbstractFile, EventRef } from 'obsidian';
|
||||||
|
import { DatabaseManager } from '../core/database';
|
||||||
|
|
||||||
|
const VIRTUAL_TABLE_NAMES = ['_files', '_links', '_tags', '_headings'] as const;
|
||||||
|
|
||||||
|
export class VirtualTableManager {
|
||||||
|
private initialized = false;
|
||||||
|
private eventRefs: EventRef[] = [];
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private app: App,
|
||||||
|
private db: DatabaseManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Lifecycle
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
initialize(): void {
|
||||||
|
this.createSchema();
|
||||||
|
this.rebuild();
|
||||||
|
this.registerListeners();
|
||||||
|
this.initialized = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
destroy(): void {
|
||||||
|
for (const ref of this.eventRefs) {
|
||||||
|
this.app.metadataCache.offref(ref);
|
||||||
|
}
|
||||||
|
this.eventRefs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
rebuild(): void {
|
||||||
|
this.db.transaction(() => {
|
||||||
|
this.db.exec('DELETE FROM _files');
|
||||||
|
this.db.exec('DELETE FROM _links');
|
||||||
|
this.db.exec('DELETE FROM _tags');
|
||||||
|
this.db.exec('DELETE FROM _headings');
|
||||||
|
|
||||||
|
for (const file of this.app.vault.getFiles()) {
|
||||||
|
this.indexFile(file);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Schema
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private createSchema(): void {
|
||||||
|
this.db.exec(`
|
||||||
|
CREATE TABLE IF NOT EXISTS _files (
|
||||||
|
path TEXT PRIMARY KEY,
|
||||||
|
name TEXT,
|
||||||
|
basename TEXT,
|
||||||
|
extension TEXT,
|
||||||
|
size INTEGER,
|
||||||
|
created INTEGER,
|
||||||
|
modified INTEGER,
|
||||||
|
folder TEXT
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS _links (
|
||||||
|
from_path TEXT NOT NULL,
|
||||||
|
to_path TEXT NOT NULL,
|
||||||
|
display_text TEXT,
|
||||||
|
link_type TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS _tags (
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
tag TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS _headings (
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
level INTEGER NOT NULL,
|
||||||
|
heading TEXT NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_files_folder ON _files(folder);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_files_ext ON _files(extension);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_links_from ON _links(from_path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_links_to ON _links(to_path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_tags_path ON _tags(path);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_tags_tag ON _tags(tag);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_vt_headings_path ON _headings(path);
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Indexing
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private indexFile(file: TFile): void {
|
||||||
|
this.db.run(
|
||||||
|
`INSERT OR REPLACE INTO _files (path, name, basename, extension, size, created, modified, folder)
|
||||||
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
|
file.path,
|
||||||
|
file.name,
|
||||||
|
file.basename,
|
||||||
|
file.extension,
|
||||||
|
file.stat.size,
|
||||||
|
file.stat.ctime,
|
||||||
|
file.stat.mtime,
|
||||||
|
file.parent?.path ?? '',
|
||||||
|
);
|
||||||
|
|
||||||
|
if (file.extension !== 'md') return;
|
||||||
|
|
||||||
|
const cache = this.app.metadataCache.getFileCache(file);
|
||||||
|
if (!cache) return;
|
||||||
|
|
||||||
|
this.indexMetadata(file.path, cache);
|
||||||
|
}
|
||||||
|
|
||||||
|
private indexMetadata(filePath: string, cache: CachedMetadata): void {
|
||||||
|
// Links
|
||||||
|
for (const link of cache.links ?? []) {
|
||||||
|
const resolved = this.app.metadataCache.getFirstLinkpathDest(link.link, filePath)?.path ?? link.link;
|
||||||
|
this.db.run(
|
||||||
|
'INSERT INTO _links (from_path, to_path, display_text, link_type) VALUES (?, ?, ?, ?)',
|
||||||
|
filePath, resolved, link.displayText ?? link.link, 'link',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Embeds
|
||||||
|
for (const embed of cache.embeds ?? []) {
|
||||||
|
const resolved = this.app.metadataCache.getFirstLinkpathDest(embed.link, filePath)?.path ?? embed.link;
|
||||||
|
this.db.run(
|
||||||
|
'INSERT INTO _links (from_path, to_path, display_text, link_type) VALUES (?, ?, ?, ?)',
|
||||||
|
filePath, resolved, embed.displayText ?? embed.link, 'embed',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags (inline)
|
||||||
|
for (const tag of cache.tags ?? []) {
|
||||||
|
this.db.run('INSERT INTO _tags (path, tag) VALUES (?, ?)', filePath, tag.tag);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tags (frontmatter)
|
||||||
|
const fmTags = cache.frontmatter?.tags;
|
||||||
|
if (Array.isArray(fmTags)) {
|
||||||
|
for (const tag of fmTags) {
|
||||||
|
const normalized = String(tag).startsWith('#') ? String(tag) : `#${tag}`;
|
||||||
|
this.db.run('INSERT INTO _tags (path, tag) VALUES (?, ?)', filePath, normalized);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Headings
|
||||||
|
for (const h of cache.headings ?? []) {
|
||||||
|
this.db.run(
|
||||||
|
'INSERT INTO _headings (path, level, heading) VALUES (?, ?, ?)',
|
||||||
|
filePath, h.level, h.heading,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Incremental updates
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
private registerListeners(): void {
|
||||||
|
// File created
|
||||||
|
this.app.vault.on('create', (file) => {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
this.db.transaction(() => this.indexFile(file));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File deleted
|
||||||
|
this.app.vault.on('delete', (file) => {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
this.removeFile(file.path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// File renamed/moved
|
||||||
|
this.app.vault.on('rename', (file, oldPath) => {
|
||||||
|
if (file instanceof TFile) {
|
||||||
|
this.db.transaction(() => {
|
||||||
|
this.removeFile(oldPath);
|
||||||
|
this.indexFile(file);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Metadata changed (content edits, frontmatter changes)
|
||||||
|
const metaRef = this.app.metadataCache.on('changed', (file, _data, cache) => {
|
||||||
|
this.db.transaction(() => {
|
||||||
|
this.removeMetadata(file.path);
|
||||||
|
// Update file stats
|
||||||
|
this.db.run(
|
||||||
|
`UPDATE _files SET size = ?, modified = ? WHERE path = ?`,
|
||||||
|
file.stat.size, file.stat.mtime, file.path,
|
||||||
|
);
|
||||||
|
this.indexMetadata(file.path, cache);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.eventRefs.push(metaRef);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeFile(path: string): void {
|
||||||
|
this.db.run('DELETE FROM _files WHERE path = ?', path);
|
||||||
|
this.removeMetadata(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
private removeMetadata(path: string): void {
|
||||||
|
this.db.run('DELETE FROM _links WHERE from_path = ?', path);
|
||||||
|
this.db.run('DELETE FROM _tags WHERE path = ?', path);
|
||||||
|
this.db.run('DELETE FROM _headings WHERE path = ?', path);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// API
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
getTableNames(): readonly string[] {
|
||||||
|
return VIRTUAL_TABLE_NAMES;
|
||||||
|
}
|
||||||
|
|
||||||
|
isVirtualTable(name: string): boolean {
|
||||||
|
return (VIRTUAL_TABLE_NAMES as readonly string[]).includes(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue