🏡 index : quine-global-labs/layer-wizard.git

import 'dart:ffi';
import 'dart:io';

import 'package:sqlite3/open.dart';
import 'package:sqlite3/sqlite3.dart';

/// Opens the local layer-history database.
///
/// Fedora/Aurora ship `libsqlite3.so.0` but not the unversioned
/// `libsqlite3.so` symlink that `package:sqlite3` looks for by default —
/// that symlink only shows up with `sqlite-devel`, a layer we don't want to
/// require just to run this app. Point `dlopen` at the versioned name
/// instead, same lib, no extra layer needed.
class HistoryDb {
  static Database? _instance;
  static bool _useInMemory = false;

  static String get _stateDir => '${Platform.environment['HOME']}/.local/state/layer_wizard';
  static String get _dbPath => '$_stateDir/history.db';

  static Database database() {
    final existing = _instance;
    if (existing != null) return existing;

    if (Platform.isLinux) {
      open.overrideFor(OperatingSystem.linux, _openLinuxLibSqlite3);
    }

    final db = _useInMemory ? sqlite3.openInMemory() : _openOnDisk();
    _migrate(db);
    _instance = db;
    return db;
  }

  static Database _openOnDisk() {
    final dir = Directory(_stateDir);
    if (!dir.existsSync()) {
      dir.createSync(recursive: true);
    }
    return sqlite3.open(_dbPath);
  }

  /// Test-only: discards any open connection and makes the next
  /// [database] call return a fresh in-memory database instead of touching
  /// the real `~/.local/state/layer_wizard/history.db`. Call this from
  /// `setUp` in every test that exercises [HistoryService].
  static void resetForTesting() {
    _instance?.dispose();
    _instance = null;
    _useInMemory = true;
  }

  static DynamicLibrary _openLinuxLibSqlite3() {
    for (final name in ['libsqlite3.so.0', 'libsqlite3.so']) {
      try {
        return DynamicLibrary.open(name);
      } catch (_) {
        // Try the next candidate.
      }
    }
    throw ArgumentError('Could not find libsqlite3.so.0 or libsqlite3.so');
  }

  static void _migrate(Database db) {
    db.execute('''
      CREATE TABLE IF NOT EXISTS nodes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        checksum TEXT NOT NULL UNIQUE,
        base_checksum TEXT,
        parent_id INTEGER REFERENCES nodes(id),
        action TEXT NOT NULL,
        package_name TEXT,
        version TEXT,
        container_image_ref TEXT,
        requested_packages TEXT,
        source TEXT NOT NULL,
        created_at TEXT NOT NULL,
        applied_at TEXT,
        notes TEXT
      );
    ''');
    db.execute('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id);');
    db.execute('''
      CREATE TABLE IF NOT EXISTS pointers (
        name TEXT PRIMARY KEY,
        node_id INTEGER NOT NULL REFERENCES nodes(id)
      );
    ''');
  }
}