Technical Reference

.gen 文件格式

基因工坊的原生文件格式 .gen 基于 SQLite 3 数据库,以结构化方式存储 DNA 序列、注释特征、引物、测序图谱和操作历史。本文档描述当前格式版本(Schema 2)的完整规范。

SQLite 3 数据库
标准文件头,通用工具可直接打开
Schema 版本 2
10+ 张数据表,覆盖序列、注释与操作历史

Overview

格式概述

01

基于 SQLite 3

.gen 文件本质是标准 SQLite 3 数据库,以 .gen 为扩展名。文件头前 16 字节为 "SQLite format 3\0",任何 SQLite 工具均可直接打开查看。

02

版本管理

通过 schema_version 表追踪格式版本。当前版本为 2,包含 edit_historychromatogram_data 等核心表,支持完整的编辑审计与测序图谱存储。

03

无压缩无加密

数据以 SQLite 原生的 TEXT、INTEGER、REAL、BLOB 类型存储,不使用额外的压缩或加密。SQLite WAL 模式提供崩溃恢复能力。

04

分块序列存储

DNA 序列按 10,000 碱基为单位分块存入 sequence_blocks 表,支持按需加载,适合大序列场景。

Schema

数据表结构

project_meta 项目元数据(键值对)
列名 类型 说明
keyTEXT PK键名
valueTEXT

已知键:name(项目名称)、description(描述)、isCircular(是否环状,true/false)、accession(GenBank 编号)、organism(物种)、date(日期,如 25-APR-2026)、isDirty(未保存标记,0/1)等。

sequence_blocks 序列分块存储
列名类型说明
block_indexINTEGER PK块索引,从 0 开始
basesTEXT NOT NULL碱基字符串(≤10,000 字符)

序列被分割为最多 10,000 碱基的块。碱基使用标准字母(A/T/C/G)及 IUPAC 歧义码表示,区分大小写。

features 序列标注特征
列名类型说明
idTEXT PKfeature-0
nameTEXT NOT NULL特征名称
typeTEXT NOT NULL类型(gene、CDS、promoter 等)
start_posINTEGER NOT NULL起始位置(1-based)
end_posINTEGER NOT NULL结束位置(1-based,包含)
strandTEXT NOT NULLforward / reverse / none
colorTEXT颜色,如 #479f71
labelTEXT显示标签
noteTEXT备注
frameINTEGER阅读框
visibleINTEGER DEFAULT 1是否可见

建有索引 idx_features_range(start_pos, end_pos) 以加速范围查询。

primers 引物定义
列名类型说明
idTEXT PKprimer-0
nameTEXT NOT NULL引物名称
sequenceTEXT NOT NULL碱基序列
descriptionTEXT描述
visibleINTEGER DEFAULT 1是否可见
primer_binding_sites 引物结合位点
列名类型说明
idTEXT PKprimer-0-site-100
primer_idTEXT NOT NULL关联 primers.id
start_posINTEGER NOT NULL起始位置(1-based)
end_posINTEGER NOT NULL结束位置(1-based,包含)
bound_strandTEXT NOT NULL结合链方向
annealed_basesTEXT退火碱基
melting_temperatureREAL熔解温度 (Tm)
componentsTEXTJSON: PrimerComponent[]
alignmentTEXTJSON: AlignmentSegment[]

建有索引 idx_binding_rangeidx_binding_primer。components 和 alignment 列存储 JSON 编码的数组。

alignment_entries 序列比对条目
列名类型说明
idTEXT PK条目 ID
nameTEXT NOT NULL名称
sequenceTEXT NOT NULL比对序列
visibleINTEGER DEFAULT 1是否可见
features_jsonTEXTJSON: Feature[]
chromatogram_jsonTEXTJSON: ChromatogramData
attachments 附件存储
列名类型说明
idTEXT PKatt-1714032000000
nameTEXT NOT NULL文件名
mime_typeTEXT NOT NULLMIME 类型
dataBLOB NOT NULL二进制数据
undo_entries 撤销/重做栈(上限 200 条)
列名类型说明
sequence_numberINTEGER PK AI自增序号
stack_typeTEXT NOT NULL DEFAULT 'undo'undoredo
operation_typeTEXT NOT NULL操作类型
target_idTEXT目标元素 ID
before_diffTEXTJSON: 操作前状态
after_diffTEXTJSON: 操作后状态

操作类型包括:addFeatureremoveFeatureupdateFeatureaddPrimerremovePrimerupdatePrimerinsertBasesdeleteBasesreplaceBasesreplaceSequenceaddAlignmentEntryremoveAlignmentEntry

edit_history 编辑审计日志
列名类型说明
idINTEGER PK AI自增 ID
timestampTEXT NOT NULLISO 8601 时间戳
operation_typeTEXT NOT NULL操作类型
target_idTEXT目标元素 ID
descriptionTEXT人类可读描述
before_snapshotTEXTJSON: 操作前快照
after_snapshotTEXTJSON: 操作后快照
chromatogram_data 测序图谱数据
列名类型说明
idTEXT PK数据 ID
trace_aBLOBA 通道信号数据
trace_cBLOBC 通道信号数据
trace_gBLOBG 通道信号数据
trace_tBLOBT 通道信号数据
peak_locationsBLOB峰位置数据

存储主序列(非仅比对条目)的测序图谱数据,四通道信号以二进制 BLOB 存储。

schema_version 格式版本追踪
列名类型说明
keyTEXT PK键名
valueTEXT

固定一行:{ key: 'version', value: '2' }

Encoding

数据编码方式

DNA 序列

纯文本碱基字母,按 10,000 碱基分块存储在 sequence_blocks 表中。支持标准 A/T/C/G 及 IUPAC 歧义码。

注释与引物

以独立 SQL 行存储。引物结合位点的 components 和 alignment 字段使用 JSON 字符串编码。

图谱数据

测序图谱(chromatogram)的四通道信号数据以二进制 BLOB 存储。比对条目的图谱以 JSON 字符串存储。

元数据

项目元信息以键值对 TEXT 形式存储在 project_meta 表。撤销/重做差异和编辑历史快照均为 JSON 编码。

附件

文件附件以 BLOB 二进制存储,附带文件名和 MIME 类型元信息。

数据库配置

桌面端使用 WAL 日志模式(PRAGMA journal_mode=WAL),同步级别为 NORMAL。保存时执行 WAL checkpoint。

Workflow

文件读写流程

1

打开文件

验证文件头(前 16 字节 = "SQLite format 3\0"),复制到临时位置(桌面端)或加载到内存(浏览器端 sql.js)。

2

加载数据

project_meta 读取项目元信息,加载 features 和 primers 表。序列按需通过 getSequenceChunk() 按块加载,不全量读取。

3

编辑操作

所有修改操作(碱基编辑、特征增删、引物设计等)实时写入 undo_entriesedit_history,确保可追溯和可撤销。

4

保存文件

调用 commitWorkingCopy():写入元数据、序列块、特征和引物,设置 isDirty = '0'。桌面端执行 WAL checkpoint 刷盘;浏览器端导出为 Uint8Array 触发下载。

Source Code

完整读写源码

以下为 .gen 文件读写的完整实现代码,共 6 个核心文件。

schema.ts 表结构与常量定义
const BLOCK_SIZE = 10000;

export const SCHEMA_VERSION = 2;

export const CREATE_TABLES_SQL = `
CREATE TABLE IF NOT EXISTS schema_version (
  key TEXT PRIMARY KEY,
  value TEXT
);

CREATE TABLE IF NOT EXISTS project_meta (
  key TEXT PRIMARY KEY,
  value TEXT
);

CREATE TABLE IF NOT EXISTS sequence_blocks (
  block_index INTEGER PRIMARY KEY,
  bases TEXT NOT NULL
);

CREATE TABLE IF NOT EXISTS features (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  type TEXT NOT NULL,
  start_pos INTEGER NOT NULL,
  end_pos INTEGER NOT NULL,
  strand TEXT NOT NULL,
  color TEXT,
  label TEXT,
  note TEXT,
  frame INTEGER,
  visible INTEGER DEFAULT 1
);
CREATE INDEX IF NOT EXISTS idx_features_range ON features(start_pos, end_pos);

CREATE TABLE IF NOT EXISTS primers (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  sequence TEXT NOT NULL,
  description TEXT,
  visible INTEGER DEFAULT 1
);

CREATE TABLE IF NOT EXISTS primer_binding_sites (
  id TEXT PRIMARY KEY,
  primer_id TEXT NOT NULL,
  start_pos INTEGER NOT NULL,
  end_pos INTEGER NOT NULL,
  bound_strand TEXT NOT NULL,
  annealed_bases TEXT,
  melting_temperature REAL,
  components TEXT,
  alignment TEXT
);
CREATE INDEX IF NOT EXISTS idx_binding_range ON primer_binding_sites(start_pos, end_pos);
CREATE INDEX IF NOT EXISTS idx_binding_primer ON primer_binding_sites(primer_id);

CREATE TABLE IF NOT EXISTS alignment_entries (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  sequence TEXT NOT NULL,
  visible INTEGER DEFAULT 1,
  features_json TEXT,
  chromatogram_json TEXT
);

CREATE TABLE IF NOT EXISTS attachments (
  id TEXT PRIMARY KEY,
  name TEXT NOT NULL,
  mime_type TEXT NOT NULL,
  data BLOB NOT NULL
);

CREATE TABLE IF NOT EXISTS undo_entries (
  sequence_number INTEGER PRIMARY KEY AUTOINCREMENT,
  stack_type TEXT NOT NULL DEFAULT 'undo',
  operation_type TEXT NOT NULL,
  target_id TEXT,
  before_diff TEXT,
  after_diff TEXT
);

CREATE TABLE IF NOT EXISTS edit_history (
  id INTEGER PRIMARY KEY AUTOINCREMENT,
  timestamp TEXT NOT NULL,
  operation_type TEXT NOT NULL,
  target_id TEXT,
  description TEXT,
  before_snapshot TEXT,
  after_snapshot TEXT
);

CREATE TABLE IF NOT EXISTS chromatogram_data (
  id TEXT PRIMARY KEY,
  trace_a BLOB,
  trace_c BLOB,
  trace_g BLOB,
  trace_t BLOB,
  peak_locations BLOB
);
`;

export { BLOCK_SIZE };
dataAccessLayer.ts 数据访问层接口
import type { DNASequence, Feature, Primer, AlignmentEntry } from '@/types/dna';
import type { SequenceInfo, EditHistoryEntry } from '@/types/sequenceInfo';

export interface ProjectMeta {
  name: string;
  description: string;
  isCircular: boolean;
  accession?: string;
  organism?: string;
  date?: string;
  typeOfDisplay: 'file_name' | 'custom';
  customName: string;
  fileName?: string;
  originalFormat?: string;
  originalPath?: string;
}

export type UndoOperationType =
  | 'addFeature' | 'removeFeature' | 'updateFeature'
  | 'addPrimer' | 'removePrimer' | 'updatePrimer'
  | 'insertBases' | 'deleteBases' | 'replaceBases' | 'replaceSequence'
  | 'addAlignmentEntry' | 'removeAlignmentEntry';

export interface UndoEntry {
  operationType: UndoOperationType;
  targetId?: string;
  beforeDiff: unknown;
  afterDiff: unknown;
}

export interface AttachmentMeta {
  id: string;
  name: string;
  mimeType: string;
}

export interface DataAccessLayer {
  open(path: string): Promise<void>;
  openFromBytes(data: Uint8Array): Promise<void>;
  close(): Promise<void>;
  isOpen(): boolean;

  getAllMetadata(): Promise<Record<string, string>>;
  setMetadata(values: Record<string, string>): Promise<void>;

  getSequenceLength(): Promise<number>;
  getSequenceChunk(startIndex: number, length: number): Promise<string>;
  writeSequence(startIndex: number, newBases: string): Promise<void>;

  getSequenceInfo(): Promise<SequenceInfo>;

  getAllFeatures(): Promise<Feature[]>;
  getFeaturesInRange(start: number, end: number): Promise<Feature[]>;
  addFeature(feature: Feature): Promise<void>;
  updateFeature(id: string, changes: Partial<Feature>): Promise<void>;
  removeFeature(id: string): Promise<void>;
  batchUpdateFeaturePositions(adjustments: Array<{ id: string; start?: number; end?: number }>): Promise<void>;

  getAllPrimers(): Promise<Primer[]>;
  getPrimersInRange(start: number, end: number): Promise<Primer[]>;
  addPrimer(primer: Primer): Promise<void>;
  updatePrimer(id: string, changes: Partial<Primer>): Promise<void>;
  removePrimer(id: string): Promise<void>;

  getAlignmentEntries(): Promise<AlignmentEntry[]>;
  addAlignmentEntry(entry: AlignmentEntry): Promise<void>;
  removeAlignmentEntry(id: string): Promise<void>;

  getAttachments(): Promise<AttachmentMeta[]>;
  addAttachment(name: string, data: ArrayBuffer, mimeType: string): Promise<void>;
  getAttachmentData(id: string): Promise<ArrayBuffer>;
  removeAttachment(id: string): Promise<void>;

  pushUndoEntry(entry: UndoEntry): Promise<void>;
  popUndo(): Promise<UndoEntry | null>;
  peekRedo(): Promise<UndoEntry | null>;
  popRedo(): Promise<UndoEntry | null>;
  clearUndoHistory(): Promise<void>;
  getUndoStackSize(): Promise<number>;
  getRedoStackSize(): Promise<number>;

  getEditHistory(limit: number, offset: number): Promise<EditHistoryEntry[]>;
  addEditHistory(entry: Omit<EditHistoryEntry, 'id'>): Promise<void>;

  importFromSequence(seq: DNASequence): Promise<void>;
  exportToSequence(): Promise<DNASequence>;
  getRawBytes(): Promise<Uint8Array>;

  commitWorkingCopy(seq: DNASequence): Promise<void>;
}
dalFactory.ts 平台自适应工厂
import type { DataAccessLayer } from '@/utils/dataAccessLayer';
import { TauriSqlDataAccessLayer } from '@/utils/tauriSqlDAL';
import { SqlJsDataAccessLayer } from '@/utils/sqljsDAL';

export async function createDataAccessLayer(): Promise<DataAccessLayer> {
  const isTauri = typeof window !== 'undefined' && (
    '__TAURI_INTERNALS__' in window || '__TAURI__' in window
  );

  if (isTauri) {
    return new TauriSqlDataAccessLayer();
  }

  return new SqlJsDataAccessLayer();
}
fileParser.ts 文件格式检测与解析
import type { DNASequence } from '@/types/dna';
import { parseGenBank } from '@/utils/genbankParser';
import { parseGjson } from '@/utils/gjsonParser';
import { parseSnapGeneDna } from '@/utils/snapgeneParser';
import { parseAb1Async } from '@/utils/tauriCommands';

export interface FileParser {
  format: string;
  extensions: string[];
  canParse(content: string | Uint8Array, fileName: string): boolean;
  parse(content: string | Uint8Array): Promise<DNASequence> | DNASequence;
}

function parseFastaContent(content: string): DNASequence {
  const lines = content.split(/\r?\n/);
  const seqLines: string[] = [];
  let name = '';
  for (const line of lines) {
    const trimmed = line.trim();
    if (trimmed.startsWith('>')) {
      if (!name && seqLines.length === 0) {
        name = trimmed.substring(1).trim();
      }
      continue;
    }
    if (trimmed) {
      seqLines.push(trimmed);
    }
  }
  const sequence = seqLines.join('').toUpperCase().replace(/[^ATCG]/g, '');
  return {
    id: name || `seq-${Date.now()}`,
    name: name || 'Unnamed',
    typeOfDisplay: 'file_name',
    customName: '',
    description: '',
    sequence,
    length: sequence.length,
    isCircular: false,
    features: [],
    primers: [],
    restrictionSites: [],
  };
}

export const fileParsers: FileParser[] = [
  {
    format: 'genbank',
    extensions: ['.gb', '.gbk', '.genbank'],
    canParse: (content, fileName) => {
      if (typeof content !== 'string') return false;
      return fileName.endsWith('.gb') || fileName.endsWith('.gbk')
        || fileName.endsWith('.genbank') || content.startsWith('LOCUS');
    },
    parse: (content) => parseGenBank(content as string),
  },
  {
    format: 'gjson',
    extensions: ['.gjson'],
    canParse: (content, fileName) => {
      if (typeof content !== 'string') return false;
      return fileName.endsWith('.gjson') || content.trim().startsWith('{');
    },
    parse: (content) => parseGjson(content as string),
  },
  {
    format: 'snapgene',
    extensions: ['.dna'],
    canParse: (_content, fileName) => {
      return fileName.endsWith('.dna');
    },
    parse: (content) => parseSnapGeneDna((content as Uint8Array).buffer as ArrayBuffer),
  },
  {
    format: 'ab1',
    extensions: ['.ab1'],
    canParse: (_content, fileName) => {
      return fileName.endsWith('.ab1');
    },
    parse: async (content) => {
      const result = await parseAb1Async(content as Uint8Array);
      return {
        id: `ab1-${Date.now()}`,
        name: 'AB1 Trace',
        typeOfDisplay: 'file_name',
        customName: '',
        description: '',
        sequence: result.sequence || '',
        length: (result.sequence || '').length,
        isCircular: false,
        features: [],
        primers: [],
        restrictionSites: [],
        chromatogram: {
          traceA: result.traceA,
          traceC: result.traceC,
          traceG: result.traceG,
          traceT: result.traceT,
          peakLocations: result.peakLocations,
        },
      };
    },
  },
  {
    format: 'gen',
    extensions: ['.gen'],
    canParse: (_content, fileName) => {
      return fileName.toLowerCase().endsWith('.gen');
    },
    parse: (content) => {
      const header = new TextDecoder().decode(((content as Uint8Array).slice(0, 16)));
      if (header !== 'SQLite format 3\u0000') {
        throw new Error('Invalid .gen file');
      }
      throw new Error('.gen files must be loaded via DataAccessLayer, not via parse().');
    },
  },
  {
    format: 'fasta',
    extensions: ['.fa', '.fasta', '.fna', '.ffn', '.faa', '.mpfa'],
    canParse: (content, fileName) => {
      if (typeof content !== 'string') return false;
      const name = fileName.toLowerCase();
      return name.endsWith('.fa') || name.endsWith('.fasta') || name.endsWith('.fna')
        || name.endsWith('.ffn') || name.endsWith('.faa') || name.endsWith('.mpfa')
        || content.trim().startsWith('>');
    },
    parse: (content) => parseFastaContent(content as string),
  },
];

export function findParser(content: string | Uint8Array, fileName: string): FileParser | undefined {
  return fileParsers.find(p => p.canParse(content, fileName));
}
sqljsDAL.ts 浏览器端实现(sql.js)
import type { DataAccessLayer, UndoEntry, AttachmentMeta } from '@/utils/dataAccessLayer';
import { CREATE_TABLES_SQL, BLOCK_SIZE } from '@/utils/schema';
import type { DNASequence, Feature, Primer, AlignmentEntry } from '@/types/dna';
import type { SequenceInfo, EditHistoryEntry } from '@/types/sequenceInfo';

let SqlJsInitPromise: Promise<unknown> | null = null;

async function getSqlJs() {
  if (!SqlJsInitPromise) {
    SqlJsInitPromise = import('sql.js').then(mod => {
      return mod.default({
        locateFile: (file: string) => `https://sql.js.org/dist/${file}`,
      });
    });
  }
  return SqlJsInitPromise;
}

interface SqlJsDatabase {
  run(sql: string, params?: unknown[]): void;
  exec(sql: string, params?: unknown[]): Array<{ columns: string[]; values: unknown[][] }>;
  export(): Uint8Array;
  close(): void;
}

function createId(prefix: string): string {
  return `${prefix}-${Date.now()}`;
}

function resultToRows<T>(columns: string[], values: unknown[][]): T[] {
  return values.map(row => {
    const obj: Record<string, unknown> = {};
    columns.forEach((col, i) => { obj[col] = row[i]; });
    return obj as T;
  });
}

export class SqlJsDataAccessLayer implements DataAccessLayer {
  private sqlJsDb: SqlJsDatabase | null = null;

  async open(_path: string): Promise<void> {
    const dbConstructor = await getSqlJs() as { Database: new (data?: ArrayLike<number> | null) => SqlJsDatabase };
    this.sqlJsDb = new dbConstructor.Database();
    this.sqlJsDb.run(CREATE_TABLES_SQL);
  }

  async openFromBytes(data: Uint8Array): Promise<void> {
    const dbConstructor = await getSqlJs() as { Database: new (data?: ArrayLike<number> | null) => SqlJsDatabase };
    this.sqlJsDb = new dbConstructor.Database(data);
    this.sqlJsDb.run(CREATE_TABLES_SQL);
  }

  async close(): Promise<void> {
    this.sqlJsDb?.close();
    this.sqlJsDb = null;
  }

  isOpen(): boolean {
    return this.sqlJsDb !== null;
  }

  private ensureDb(): SqlJsDatabase {
    if (!this.sqlJsDb) throw new Error('Database not opened');
    return this.sqlJsDb;
  }

  private execSingle(sql: string, params?: unknown[]): Array<{ columns: string[]; values: unknown[][] }> {
    const db = this.ensureDb();
    const result = db.exec(sql, params);
    if (result.length === 0) return [{ columns: [], values: [] }];
    return result;
  }

  async getAllMetadata(): Promise<Record<string, string>> {
    const result = this.execSingle('SELECT key, value FROM project_meta')[0];
    if (result.values.length === 0) return {};
    const rows = resultToRows<{ key: string; value: string }>(result.columns, result.values);
    const out: Record<string, string> = {};
    for (const r of rows) { out[r.key] = r.value; }
    return out;
  }

  async setMetadata(values: Record<string, string>): Promise<void> {
    const db = this.ensureDb();
    for (const [key, value] of Object.entries(values)) {
      db.run(
        'INSERT INTO project_meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value=excluded.value',
        [key, value]
      );
    }
  }

  async getSequenceLength(): Promise<number> {
    const result = this.execSingle('SELECT COALESCE(SUM(LENGTH(bases)), 0) AS total FROM sequence_blocks')[0];
    if (result.values.length === 0) return 0;
    return resultToRows<{ total: number }>(result.columns, result.values)[0]?.total ?? 0;
  }

  async getSequenceChunk(startIndex: number, length: number): Promise<string> {
    const blockStart = Math.floor(startIndex / BLOCK_SIZE);
    const endIndex = startIndex + length;
    const blockEnd = Math.floor((endIndex - 1) / BLOCK_SIZE);

    const result = this.execSingle(
      'SELECT block_index, bases FROM sequence_blocks WHERE block_index BETWEEN ? AND ? ORDER BY block_index',
      [blockStart, blockEnd]
    )[0];

    if (result.values.length === 0) return '';

    const rows = resultToRows<{ block_index: number; bases: string }>(result.columns, result.values);
    const blockMap = new Map<number, string>();
    for (const row of rows) { blockMap.set(row.block_index, row.bases); }

    let seq = '';
    for (let i = blockStart; i <= blockEnd; i++) { seq += blockMap.get(i) ?? ''; }
    const offset = startIndex - blockStart * BLOCK_SIZE;
    return seq.slice(offset, offset + length);
  }

  async writeSequence(startIndex: number, newBases: string): Promise<void> {
    const db = this.ensureDb();
    const oldLen = await this.getSequenceLength();
    const oldSeq = await this.getSequenceChunk(0, oldLen);
    const updated = oldSeq.slice(0, startIndex) + newBases + oldSeq.slice(startIndex);

    db.run('DELETE FROM sequence_blocks');
    for (let i = 0; i < updated.length; i += BLOCK_SIZE) {
      db.run('INSERT INTO sequence_blocks (block_index, bases) VALUES (?, ?)', [
        Math.floor(i / BLOCK_SIZE),
        updated.slice(i, i + BLOCK_SIZE),
      ]);
    }
  }

  async getSequenceInfo(): Promise<SequenceInfo> {
    const meta = await this.getAllMetadata();
    const length = await this.getSequenceLength();
    return {
      id: meta['accession'] || `seq-${Date.now()}`,
      name: meta['name'] || 'Untitled',
      typeOfDisplay: (meta['typeOfDisplay'] as 'file_name' | 'custom') || 'file_name',
      customName: meta['customName'] || '',
      description: meta['description'] || '',
      length,
      isCircular: meta['isCircular'] === 'true',
      accession: meta['accession'] || undefined,
      organism: meta['organism'] || undefined,
      date: meta['date'] || undefined,
      fileName: meta['fileName'] || undefined,
    };
  }

  async getAllFeatures(): Promise<Feature[]> {
    const result = this.execSingle('SELECT * FROM features ORDER BY start_pos')[0];
    if (result.values.length === 0) return [];
    return resultToRows<{
      id: string; name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>(result.columns, result.values).map(r => ({
      id: r.id, name: r.name, type: r.type, start: r.start_pos, end: r.end_pos,
      strand: r.strand as 'forward' | 'reverse' | 'none',
      color: r.color ?? undefined, label: r.label ?? undefined,
      note: r.note ?? undefined, frame: r.frame ?? undefined, visible: r.visible === 1,
    }));
  }

  async getFeaturesInRange(start: number, end: number): Promise<Feature[]> {
    const result = this.execSingle(
      'SELECT * FROM features WHERE start_pos <= ? AND end_pos >= ? ORDER BY start_pos',
      [end, start]
    )[0];
    if (result.values.length === 0) return [];
    return resultToRows<{
      id: string; name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>(result.columns, result.values).map(r => ({
      id: r.id, name: r.name, type: r.type, start: r.start_pos, end: r.end_pos,
      strand: r.strand as 'forward' | 'reverse' | 'none',
      color: r.color ?? undefined, label: r.label ?? undefined,
      note: r.note ?? undefined, frame: r.frame ?? undefined, visible: r.visible === 1,
    }));
  }

  async addFeature(feature: Feature): Promise<void> {
    this.ensureDb().run(
      `INSERT INTO features (id, name, type, start_pos, end_pos, strand, color, label, note, frame, visible)
       VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
      [feature.id, feature.name, feature.type, feature.start, feature.end,
        feature.strand, feature.color ?? null, feature.label ?? null,
        feature.note ?? null, feature.frame ?? null, (feature.visible ?? true) ? 1 : 0]
    );
  }

  async updateFeature(id: string, changes: Partial<Feature>): Promise<void> {
    const db = this.ensureDb();
    const result = db.exec('SELECT * FROM features WHERE id = ?', [id]);
    if (result.length === 0 || result[0].values.length === 0) return;

    const rows = resultToRows<{
      name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>(result[0].columns, result[0].values);
    const cur = rows[0];

    db.run(
      `UPDATE features SET name=?, type=?, start_pos=?, end_pos=?, strand=?,
       color=?, label=?, note=?, frame=?, visible=? WHERE id=?`,
      [changes.name ?? cur.name, changes.type ?? cur.type,
        changes.start ?? cur.start_pos, changes.end ?? cur.end_pos,
        changes.strand ?? cur.strand,
        changes.color !== undefined ? (changes.color ?? null) : cur.color,
        changes.label !== undefined ? (changes.label ?? null) : cur.label,
        changes.note !== undefined ? (changes.note ?? null) : cur.note,
        changes.frame ?? cur.frame,
        changes.visible !== undefined ? (changes.visible ? 1 : 0) : cur.visible,
        id]
    );
  }

  async removeFeature(id: string): Promise<void> {
    this.ensureDb().run('DELETE FROM features WHERE id = ?', [id]);
  }

  async batchUpdateFeaturePositions(adjustments: Array<{ id: string; start?: number; end?: number }>): Promise<void> {
    const db = this.ensureDb();
    for (const adj of adjustments) {
      if (adj.start !== undefined) {
        db.run('UPDATE features SET start_pos=? WHERE id=?', [adj.start, adj.id]);
      }
      if (adj.end !== undefined) {
        db.run('UPDATE features SET end_pos=? WHERE id=?', [adj.end, adj.id]);
      }
    }
  }

  async getAllPrimers(): Promise<Primer[]> {
    const db = this.ensureDb();
    const prResult = db.exec('SELECT * FROM primers');
    if (prResult.length === 0 || prResult[0].values.length === 0) return [];

    const prRows = resultToRows<{
      id: string; name: string; sequence: string; description: string | null; visible: number;
    }>(prResult[0].columns, prResult[0].values);

    const primers: Primer[] = [];
    for (const pr of prRows) {
      const siteResult = db.exec('SELECT * FROM primer_binding_sites WHERE primer_id = ?', [pr.id]);
      const siteRows = siteResult.length > 0
        ? resultToRows<{
            id: string; start_pos: number; end_pos: number; bound_strand: string;
            annealed_bases: string | null; melting_temperature: number;
            components: string | null; alignment: string | null;
          }>(siteResult[0].columns, siteResult[0].values)
        : [];

      primers.push({
        id: pr.id, name: pr.name, sequence: pr.sequence,
        description: pr.description ?? undefined, visible: pr.visible === 1,
        bindingSites: siteRows.map(s => ({
          start: s.start_pos, end: s.end_pos,
          boundStrand: s.bound_strand as 'forward' | 'reverse' | 'none',
          annealedBases: s.annealed_bases ?? '', meltingTemperature: s.melting_temperature ?? 0,
          components: s.components ? JSON.parse(s.components) : [],
          alignment: s.alignment ? JSON.parse(s.alignment) : undefined,
        })),
      });
    }
    return primers;
  }

  async addPrimer(primer: Primer): Promise<void> {
    const db = this.ensureDb();
    db.run('INSERT INTO primers (id, name, sequence, description, visible) VALUES (?, ?, ?, ?, ?)',
      [primer.id, primer.name, primer.sequence, primer.description ?? null, (primer.visible ?? true) ? 1 : 0]);
    for (const site of primer.bindingSites) {
      db.run(
        `INSERT INTO primer_binding_sites (id, primer_id, start_pos, end_pos, bound_strand,
         annealed_bases, melting_temperature, components, alignment) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
        [`${primer.id}-site-${site.start}`, primer.id, site.start, site.end, site.boundStrand,
          site.annealedBases, site.meltingTemperature,
          JSON.stringify(site.components), site.alignment ? JSON.stringify(site.alignment) : null]
      );
    }
  }

  async updatePrimer(id: string, changes: Partial<Primer>): Promise<void> {
    const db = this.ensureDb();
    const res = db.exec('SELECT * FROM primers WHERE id = ?', [id]);
    if (res.length === 0 || res[0].values.length === 0) return;
    const cur = resultToRows<{
      name: string; sequence: string; description: string | null; visible: number;
    }>(res[0].columns, res[0].values)[0];
    db.run('UPDATE primers SET name=?, sequence=?, description=?, visible=? WHERE id=?',
      [changes.name ?? cur.name, changes.sequence ?? cur.sequence,
        changes.description !== undefined ? (changes.description ?? null) : cur.description,
        changes.visible !== undefined ? (changes.visible ? 1 : 0) : cur.visible, id]);
  }

  async removePrimer(id: string): Promise<void> {
    const db = this.ensureDb();
    db.run('DELETE FROM primer_binding_sites WHERE primer_id = ?', [id]);
    db.run('DELETE FROM primers WHERE id = ?', [id]);
  }

  async getAlignmentEntries(): Promise<AlignmentEntry[]> {
    const result = this.ensureDb().exec('SELECT * FROM alignment_entries');
    if (result.length === 0 || result[0].values.length === 0) return [];
    return resultToRows<{
      id: string; name: string; sequence: string; visible: number;
      features_json: string | null; chromatogram_json: string | null;
    }>(result[0].columns, result[0].values).map(r => ({
      id: r.id, name: r.name, sequence: r.sequence, visible: r.visible === 1,
      features: r.features_json ? JSON.parse(r.features_json) : undefined,
      chromatogram: r.chromatogram_json ? JSON.parse(r.chromatogram_json) : undefined,
    }));
  }

  async addAlignmentEntry(entry: AlignmentEntry): Promise<void> {
    this.ensureDb().run(
      'INSERT INTO alignment_entries (id, name, sequence, visible, features_json, chromatogram_json) VALUES (?, ?, ?, ?, ?, ?)',
      [entry.id, entry.name, entry.sequence, entry.visible ? 1 : 0,
        entry.features ? JSON.stringify(entry.features) : null,
        entry.chromatogram ? JSON.stringify(entry.chromatogram) : null]
    );
  }

  async removeAlignmentEntry(id: string): Promise<void> {
    this.ensureDb().run('DELETE FROM alignment_entries WHERE id = ?', [id]);
  }

  async getAttachments(): Promise<AttachmentMeta[]> {
    const result = this.ensureDb().exec('SELECT id, name, mime_type FROM attachments');
    if (result.length === 0 || result[0].values.length === 0) return [];
    return resultToRows<{ id: string; name: string; mime_type: string }>(result[0].columns, result[0].values).map(r => ({
      id: r.id, name: r.name, mimeType: r.mime_type,
    }));
  }

  async addAttachment(name: string, data: ArrayBuffer, mimeType: string): Promise<void> {
    const bytes = new Uint8Array(data);
    this.ensureDb().run('INSERT INTO attachments (id, name, mime_type, data) VALUES (?, ?, ?, ?)',
      [createId('att'), name, mimeType, bytes]);
  }

  async getAttachmentData(id: string): Promise<ArrayBuffer> {
    const result = this.ensureDb().exec('SELECT data FROM attachments WHERE id = ?', [id]);
    if (result.length === 0 || result[0].values.length === 0) throw new Error('Attachment not found');
    const rows = resultToRows<{ data: Uint8Array }>(result[0].columns, result[0].values);
    return rows[0].data.buffer as ArrayBuffer;
  }

  async removeAttachment(id: string): Promise<void> {
    this.ensureDb().run('DELETE FROM attachments WHERE id = ?', [id]);
  }

  async pushUndoEntry(entry: UndoEntry): Promise<void> {
    const db = this.ensureDb();
    db.run("DELETE FROM undo_entries WHERE stack_type = 'redo'");
    db.run(
      "INSERT INTO undo_entries (stack_type, operation_type, target_id, before_diff, after_diff) VALUES ('undo', ?, ?, ?, ?)",
      [entry.operationType, entry.targetId ?? null, JSON.stringify(entry.beforeDiff), JSON.stringify(entry.afterDiff)]
    );
    const cntResult = db.exec("SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'undo'");
    if (cntResult.length > 0 && cntResult[0].values.length > 0) {
      const cnt = resultToRows<{ cnt: number }>(cntResult[0].columns, cntResult[0].values)[0];
      if (cnt.cnt > 200) {
        db.run("DELETE FROM undo_entries WHERE stack_type = 'undo' AND sequence_number = (SELECT MIN(sequence_number) FROM undo_entries WHERE stack_type = 'undo')");
      }
    }
  }

  async popUndo(): Promise<UndoEntry | null> {
    const db = this.ensureDb();
    const result = db.exec("SELECT * FROM undo_entries WHERE stack_type = 'undo' ORDER BY sequence_number DESC LIMIT 1");
    if (result.length === 0 || result[0].values.length === 0) return null;

    const rows = resultToRows<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>(result[0].columns, result[0].values);
    if (rows.length === 0) return null;

    const row = rows[0];
    const entry: UndoEntry = {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };

    db.run("UPDATE undo_entries SET stack_type = 'redo' WHERE sequence_number = ?", [row.sequence_number]);
    return entry;
  }

  async peekRedo(): Promise<UndoEntry | null> {
    const result = this.execSingle("SELECT * FROM undo_entries WHERE stack_type = 'redo' ORDER BY sequence_number DESC LIMIT 1")[0];
    if (result.values.length === 0) return null;
    const rows = resultToRows<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>(result.columns, result.values);
    if (rows.length === 0) return null;
    const row = rows[0];
    return {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };
  }

  async popRedo(): Promise<UndoEntry | null> {
    const db = this.ensureDb();
    const result = db.exec("SELECT * FROM undo_entries WHERE stack_type = 'redo' ORDER BY sequence_number DESC LIMIT 1");
    if (result.length === 0 || result[0].values.length === 0) return null;
    const rows = resultToRows<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>(result[0].columns, result[0].values);
    if (rows.length === 0) return null;
    const row = rows[0];
    const entry: UndoEntry = {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };
    db.run('DELETE FROM undo_entries WHERE sequence_number = ?', [row.sequence_number]);
    return entry;
  }

  async clearUndoHistory(): Promise<void> {
    this.ensureDb().run('DELETE FROM undo_entries');
  }

  async getUndoStackSize(): Promise<number> {
    const result = this.execSingle("SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'undo'")[0];
    if (result.values.length === 0) return 0;
    return resultToRows<{ cnt: number }>(result.columns, result.values)[0]?.cnt ?? 0;
  }

  async getRedoStackSize(): Promise<number> {
    const result = this.execSingle("SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'redo'")[0];
    if (result.values.length === 0) return 0;
    return resultToRows<{ cnt: number }>(result.columns, result.values)[0]?.cnt ?? 0;
  }

  async getEditHistory(limit: number, offset: number): Promise<EditHistoryEntry[]> {
    const result = this.execSingle(
      'SELECT * FROM edit_history ORDER BY id DESC LIMIT ? OFFSET ?',
      [limit, offset]
    )[0];
    if (result.values.length === 0) return [];
    return resultToRows<{
      id: number; timestamp: string; operation_type: string; target_id: string | null;
      description: string | null; before_snapshot: string | null; after_snapshot: string | null;
    }>(result.columns, result.values).map(r => ({
      id: r.id,
      timestamp: r.timestamp,
      operationType: r.operation_type,
      targetId: r.target_id,
      description: r.description ?? '',
      beforeSnapshot: r.before_snapshot,
      afterSnapshot: r.after_snapshot,
    }));
  }

  async addEditHistory(entry: Omit<EditHistoryEntry, 'id'>): Promise<void> {
    this.ensureDb().run(
      'INSERT INTO edit_history (timestamp, operation_type, target_id, description, before_snapshot, after_snapshot) VALUES (?, ?, ?, ?, ?, ?)',
      [entry.timestamp, entry.operationType, entry.targetId ?? null, entry.description ?? null,
       entry.beforeSnapshot ?? null, entry.afterSnapshot ?? null]
    );
  }

  async importFromSequence(seq: DNASequence): Promise<void> {
    await this.setMetadata({
      name: seq.name, description: seq.description, isCircular: String(seq.isCircular),
      accession: seq.accession ?? '', organism: seq.organism ?? '', date: seq.date ?? '',
      typeOfDisplay: seq.typeOfDisplay, customName: seq.customName, fileName: seq.fileName ?? '',
    });

    const db = this.ensureDb();
    db.run('DELETE FROM sequence_blocks');
    db.run('DELETE FROM features');
    db.run('DELETE FROM primer_binding_sites');
    db.run('DELETE FROM primers');
    for (let i = 0; i < seq.sequence.length; i += BLOCK_SIZE) {
      db.run('INSERT INTO sequence_blocks (block_index, bases) VALUES (?, ?)',
        [Math.floor(i / BLOCK_SIZE), seq.sequence.slice(i, i + BLOCK_SIZE)]);
    }
    for (const feature of seq.features) { await this.addFeature(feature); }
    for (const primer of seq.primers || []) { await this.addPrimer(primer); }
  }

  async exportToSequence(): Promise<DNASequence> {
    const meta = await this.getAllMetadata();
    const length = await this.getSequenceLength();
    const sequence = await this.getSequenceChunk(0, length);
    const features = await this.getAllFeatures();
    const primers = await this.getAllPrimers();

    return {
      id: meta['accession'] || `seq-${Date.now()}`,
      name: meta['name'] || 'Untitled',
      typeOfDisplay: (meta['typeOfDisplay'] as 'file_name' | 'custom') || 'file_name',
      customName: meta['customName'] || '',
      description: meta['description'] || '',
      sequence,
      length,
      isCircular: meta['isCircular'] === 'true',
      accession: meta['accession'] || undefined,
      organism: meta['organism'] || undefined,
      date: meta['date'] || undefined,
      features,
      primers,
      restrictionSites: [],
      fileName: meta['fileName'] || undefined,
    };
  }

  async commitWorkingCopy(seq: DNASequence): Promise<void> {
    await this.importFromSequence(seq);
    await this.setMetadata({ isDirty: '0' });
  }

  async getRawBytes(): Promise<Uint8Array> {
    return this.ensureDb().export();
  }
}
tauriSqlDAL.ts 桌面端实现(Tauri SQLite)
import Database from '@tauri-apps/plugin-sql';
import type { DataAccessLayer, UndoEntry, AttachmentMeta } from '@/utils/dataAccessLayer';
import { CREATE_TABLES_SQL, BLOCK_SIZE } from '@/utils/schema';
import type { DNASequence, Feature, Primer, AlignmentEntry } from '@/types/dna';
import type { SequenceInfo, EditHistoryEntry } from '@/types/sequenceInfo';

function createId(prefix: string): string {
  return `${prefix}-${Date.now()}`;
}

export class TauriSqlDataAccessLayer implements DataAccessLayer {
  private db: Database | null = null;
  private dbPath: string | null = null;

  async open(path: string): Promise<void> {
    this.dbPath = path;
    this.db = await Database.load(`sqlite:${path}`);
    await this.db.execute('PRAGMA journal_mode=WAL');
    await this.db.execute('PRAGMA synchronous=NORMAL');
    await this.db.execute(CREATE_TABLES_SQL);
  }

  async openFromBytes(data: Uint8Array): Promise<void> {
    const { tempDir, join } = await import('@tauri-apps/api/path');
    const dir = await tempDir();
    const randomSuffix = Math.random().toString(36).substring(2, 8);
    const tempPath = await join(dir, `gene-editor-import-${randomSuffix}.gen`);
    const { writeFile } = await import('@tauri-apps/plugin-fs');
    await writeFile(tempPath, data);
    await this.open(tempPath);
  }

  async close(): Promise<void> {
    if (this.db) {
      await this.db.close();
      this.db = null;
    }
  }

  isOpen(): boolean {
    return this.db !== null;
  }

  private ensureDb(): Database {
    if (!this.db) throw new Error('Database not opened');
    return this.db;
  }

  async getAllMetadata(): Promise<Record<string, string>> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{ key: string; value: string }>>(
      'SELECT key, value FROM project_meta'
    );
    const result: Record<string, string> = {};
    for (const row of rows) { result[row.key] = row.value; }
    return result;
  }

  async setMetadata(values: Record<string, string>): Promise<void> {
    const db = this.ensureDb();
    for (const [key, value] of Object.entries(values)) {
      await db.execute(
        'INSERT INTO project_meta (key, value) VALUES ($1, $2) ON CONFLICT(key) DO UPDATE SET value=$2',
        [key, value]
      );
    }
  }

  async getSequenceLength(): Promise<number> {
    const db = this.ensureDb();
    const row = await db.select<Array<{ total: number }>>(
      'SELECT COALESCE(SUM(LENGTH(bases)), 0) AS total FROM sequence_blocks'
    );
    return row[0]?.total ?? 0;
  }

  async getSequenceChunk(startIndex: number, length: number): Promise<string> {
    const db = this.ensureDb();
    const blockStart = Math.floor(startIndex / BLOCK_SIZE);
    const endIndex = startIndex + length;
    const blockEnd = Math.floor((endIndex - 1) / BLOCK_SIZE);

    const rows = await db.select<Array<{ block_index: number; bases: string }>>(
      'SELECT block_index, bases FROM sequence_blocks WHERE block_index BETWEEN $1 AND $2 ORDER BY block_index',
      [blockStart, blockEnd]
    );

    const blockMap = new Map<number, string>();
    for (const row of rows) { blockMap.set(row.block_index, row.bases); }

    let result = '';
    for (let i = blockStart; i <= blockEnd; i++) { result += blockMap.get(i) ?? ''; }
    const offset = startIndex - blockStart * BLOCK_SIZE;
    return result.slice(offset, offset + length);
  }

  async writeSequence(startIndex: number, newBases: string): Promise<void> {
    const db = this.ensureDb();
    const oldLen = await this.getSequenceLength();
    const oldSeq = await this.getSequenceChunk(0, oldLen);
    const updated = oldSeq.slice(0, startIndex) + newBases + oldSeq.slice(startIndex);

    await db.execute('DELETE FROM sequence_blocks');
    for (let i = 0; i < updated.length; i += BLOCK_SIZE) {
      await db.execute('INSERT INTO sequence_blocks (block_index, bases) VALUES ($1, $2)', [
        Math.floor(i / BLOCK_SIZE),
        updated.slice(i, i + BLOCK_SIZE),
      ]);
    }
  }

  async getSequenceInfo(): Promise<SequenceInfo> {
    const meta = await this.getAllMetadata();
    const length = await this.getSequenceLength();
    return {
      id: meta['accession'] || `seq-${Date.now()}`,
      name: meta['name'] || 'Untitled',
      typeOfDisplay: (meta['typeOfDisplay'] as 'file_name' | 'custom') || 'file_name',
      customName: meta['customName'] || '',
      description: meta['description'] || '',
      length,
      isCircular: meta['isCircular'] === 'true',
      accession: meta['accession'] || undefined,
      organism: meta['organism'] || undefined,
      date: meta['date'] || undefined,
      fileName: meta['fileName'] || undefined,
    };
  }

  async getAllFeatures(): Promise<Feature[]> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      id: string; name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>>('SELECT * FROM features ORDER BY start_pos');

    return rows.map(r => ({
      id: r.id, name: r.name, type: r.type,
      start: r.start_pos, end: r.end_pos,
      strand: r.strand as 'forward' | 'reverse' | 'none',
      color: r.color ?? undefined, label: r.label ?? undefined,
      note: r.note ?? undefined, frame: r.frame ?? undefined, visible: r.visible === 1,
    }));
  }

  async getFeaturesInRange(start: number, end: number): Promise<Feature[]> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      id: string; name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>>(
      'SELECT * FROM features WHERE start_pos <= $2 AND end_pos >= $1 ORDER BY start_pos',
      [start, end]
    );
    return rows.map(r => ({
      id: r.id, name: r.name, type: r.type,
      start: r.start_pos, end: r.end_pos,
      strand: r.strand as 'forward' | 'reverse' | 'none',
      color: r.color ?? undefined, label: r.label ?? undefined,
      note: r.note ?? undefined, frame: r.frame ?? undefined, visible: r.visible === 1,
    }));
  }

  async addFeature(feature: Feature): Promise<void> {
    const db = this.ensureDb();
    await db.execute(
      `INSERT INTO features (id, name, type, start_pos, end_pos, strand, color, label, note, frame, visible)
       VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)`,
      [feature.id, feature.name, feature.type, feature.start, feature.end,
        feature.strand, feature.color ?? null, feature.label ?? null,
        feature.note ?? null, feature.frame ?? null, (feature.visible ?? true) ? 1 : 0]
    );
  }

  async updateFeature(id: string, changes: Partial<Feature>): Promise<void> {
    const db = this.ensureDb();
    const existing = await db.select<Array<{
      name: string; type: string; start_pos: number; end_pos: number;
      strand: string; color: string | null; label: string | null; note: string | null;
      frame: number | null; visible: number;
    }>>('SELECT * FROM features WHERE id = $1', [id]);
    if (existing.length === 0) return;
    const cur = existing[0];
    await db.execute(
      `UPDATE features SET name=$1, type=$2, start_pos=$3, end_pos=$4, strand=$5,
       color=$6, label=$7, note=$8, frame=$9, visible=$10 WHERE id=$11`,
      [changes.name ?? cur.name, changes.type ?? cur.type,
        changes.start ?? cur.start_pos, changes.end ?? cur.end_pos,
        changes.strand ?? cur.strand,
        changes.color ?? cur.color, changes.label ?? cur.label,
        changes.note ?? cur.note, changes.frame ?? cur.frame,
        changes.visible !== undefined ? (changes.visible ? 1 : 0) : cur.visible, id]
    );
  }

  async removeFeature(id: string): Promise<void> {
    const db = this.ensureDb();
    await db.execute('DELETE FROM features WHERE id = $1', [id]);
  }

  async batchUpdateFeaturePositions(adjustments: Array<{ id: string; start?: number; end?: number }>): Promise<void> {
    const db = this.ensureDb();
    for (const adj of adjustments) {
      const parts: string[] = [];
      const params: unknown[] = [];
      let idx = 1;
      if (adj.start !== undefined) {
        parts.push(`start_pos=$${idx}`);
        params.push(adj.start);
        idx++;
      }
      if (adj.end !== undefined) {
        parts.push(`end_pos=$${idx}`);
        params.push(adj.end);
        idx++;
      }
      if (parts.length > 0) {
        params.push(adj.id);
        await db.execute(`UPDATE features SET ${parts.join(', ')} WHERE id=$${idx}`, params);
      }
    }
  }

  async getAllPrimers(): Promise<Primer[]> {
    const db = this.ensureDb();
    const primerRows = await db.select<Array<{
      id: string; name: string; sequence: string; description: string | null; visible: number;
    }>>('SELECT * FROM primers');

    const primers: Primer[] = [];
    for (const pr of primerRows) {
      const siteRows = await db.select<Array<{
        id: string; start_pos: number; end_pos: number; bound_strand: string;
        annealed_bases: string | null; melting_temperature: number;
        components: string | null; alignment: string | null;
      }>>('SELECT * FROM primer_binding_sites WHERE primer_id = $1', [pr.id]);

      primers.push({
        id: pr.id, name: pr.name, sequence: pr.sequence,
        description: pr.description ?? undefined, visible: pr.visible === 1,
        bindingSites: siteRows.map(s => ({
          start: s.start_pos, end: s.end_pos,
          boundStrand: s.bound_strand as 'forward' | 'reverse' | 'none',
          annealedBases: s.annealed_bases ?? '', meltingTemperature: s.melting_temperature ?? 0,
          components: s.components ? JSON.parse(s.components) : [],
          alignment: s.alignment ? JSON.parse(s.alignment) : undefined,
        })),
      });
    }
    return primers;
  }

  async addPrimer(primer: Primer): Promise<void> {
    const db = this.ensureDb();
    await db.execute(
      'INSERT INTO primers (id, name, sequence, description, visible) VALUES ($1, $2, $3, $4, $5)',
      [primer.id, primer.name, primer.sequence, primer.description ?? null, (primer.visible ?? true) ? 1 : 0]
    );
    for (const site of primer.bindingSites) {
      await db.execute(
        `INSERT INTO primer_binding_sites (id, primer_id, start_pos, end_pos, bound_strand,
         annealed_bases, melting_temperature, components, alignment) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)`,
        [`${primer.id}-site-${site.start}`, primer.id, site.start, site.end, site.boundStrand,
          site.annealedBases, site.meltingTemperature,
          JSON.stringify(site.components), site.alignment ? JSON.stringify(site.alignment) : null]
      );
    }
  }

  async updatePrimer(id: string, changes: Partial<Primer>): Promise<void> {
    const db = this.ensureDb();
    const existing = await db.select<Array<{
      name: string; sequence: string; description: string | null; visible: number;
    }>>('SELECT * FROM primers WHERE id = $1', [id]);
    if (existing.length === 0) return;
    const cur = existing[0];
    await db.execute(
      'UPDATE primers SET name=$1, sequence=$2, description=$3, visible=$4 WHERE id=$5',
      [changes.name ?? cur.name, changes.sequence ?? cur.sequence,
        changes.description !== undefined ? (changes.description ?? null) : cur.description,
        changes.visible !== undefined ? (changes.visible ? 1 : 0) : cur.visible, id]
    );
  }

  async removePrimer(id: string): Promise<void> {
    const db = this.ensureDb();
    await db.execute('DELETE FROM primer_binding_sites WHERE primer_id = $1', [id]);
    await db.execute('DELETE FROM primers WHERE id = $1', [id]);
  }

  async getAlignmentEntries(): Promise<AlignmentEntry[]> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      id: string; name: string; sequence: string; visible: number;
      features_json: string | null; chromatogram_json: string | null;
    }>>('SELECT * FROM alignment_entries');
    return rows.map(r => ({
      id: r.id, name: r.name, sequence: r.sequence, visible: r.visible === 1,
      features: r.features_json ? JSON.parse(r.features_json) : undefined,
      chromatogram: r.chromatogram_json ? JSON.parse(r.chromatogram_json) : undefined,
    }));
  }

  async addAlignmentEntry(entry: AlignmentEntry): Promise<void> {
    const db = this.ensureDb();
    await db.execute(
      'INSERT INTO alignment_entries (id, name, sequence, visible, features_json, chromatogram_json) VALUES ($1, $2, $3, $4, $5, $6)',
      [entry.id, entry.name, entry.sequence, entry.visible ? 1 : 0,
        entry.features ? JSON.stringify(entry.features) : null,
        entry.chromatogram ? JSON.stringify(entry.chromatogram) : null]
    );
  }

  async removeAlignmentEntry(id: string): Promise<void> {
    const db = this.ensureDb();
    await db.execute('DELETE FROM alignment_entries WHERE id = $1', [id]);
  }

  async getAttachments(): Promise<AttachmentMeta[]> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{ id: string; name: string; mime_type: string }>>(
      'SELECT id, name, mime_type FROM attachments'
    );
    return rows.map(r => ({ id: r.id, name: r.name, mimeType: r.mime_type }));
  }

  async addAttachment(name: string, data: ArrayBuffer, mimeType: string): Promise<void> {
    const db = this.ensureDb();
    const bytes = new Uint8Array(data);
    await db.execute(
      'INSERT INTO attachments (id, name, mime_type, data) VALUES ($1, $2, $3, $4)',
      [createId('att'), name, mimeType, bytes]
    );
  }

  async getAttachmentData(id: string): Promise<ArrayBuffer> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{ data: number[] }>>(
      'SELECT data FROM attachments WHERE id = $1', [id]
    );
    if (rows.length === 0) throw new Error('Attachment not found');
    return new Uint8Array(rows[0].data).buffer;
  }

  async removeAttachment(id: string): Promise<void> {
    const db = this.ensureDb();
    await db.execute('DELETE FROM attachments WHERE id = $1', [id]);
  }

  async pushUndoEntry(entry: UndoEntry): Promise<void> {
    const db = this.ensureDb();
    await db.execute("DELETE FROM undo_entries WHERE stack_type = 'redo'");
    await db.execute(
      "INSERT INTO undo_entries (stack_type, operation_type, target_id, before_diff, after_diff) VALUES ('undo', $1, $2, $3, $4)",
      [entry.operationType, entry.targetId ?? null,
        JSON.stringify(entry.beforeDiff), JSON.stringify(entry.afterDiff)]
    );
    const count = await db.select<Array<{ cnt: number }>>(
      "SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'undo'"
    );
    if (count[0].cnt > 200) {
      await db.execute(
        "DELETE FROM undo_entries WHERE stack_type = 'undo' AND sequence_number = (SELECT MIN(sequence_number) FROM undo_entries WHERE stack_type = 'undo')"
      );
    }
  }

  async popUndo(): Promise<UndoEntry | null> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>>("SELECT * FROM undo_entries WHERE stack_type = 'undo' ORDER BY sequence_number DESC LIMIT 1");
    if (rows.length === 0) return null;
    const row = rows[0];
    const entry: UndoEntry = {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };
    await db.execute("UPDATE undo_entries SET stack_type = 'redo' WHERE sequence_number = $1", [row.sequence_number]);
    return entry;
  }

  async peekRedo(): Promise<UndoEntry | null> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>>("SELECT * FROM undo_entries WHERE stack_type = 'redo' ORDER BY sequence_number DESC LIMIT 1");
    if (rows.length === 0) return null;
    const row = rows[0];
    return {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };
  }

  async popRedo(): Promise<UndoEntry | null> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      sequence_number: number; operation_type: string; target_id: string | null;
      before_diff: string; after_diff: string;
    }>>("SELECT * FROM undo_entries WHERE stack_type = 'redo' ORDER BY sequence_number DESC LIMIT 1");
    if (rows.length === 0) return null;
    const row = rows[0];
    const entry: UndoEntry = {
      operationType: row.operation_type as UndoEntry['operationType'],
      targetId: row.target_id ?? undefined,
      beforeDiff: JSON.parse(row.before_diff),
      afterDiff: JSON.parse(row.after_diff),
    };
    await db.execute('DELETE FROM undo_entries WHERE sequence_number = $1', [row.sequence_number]);
    return entry;
  }

  async clearUndoHistory(): Promise<void> {
    const db = this.ensureDb();
    await db.execute('DELETE FROM undo_entries');
  }

  async getUndoStackSize(): Promise<number> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{ cnt: number }>>(
      "SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'undo'"
    );
    return rows[0].cnt;
  }

  async getRedoStackSize(): Promise<number> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{ cnt: number }>>(
      "SELECT COUNT(*) AS cnt FROM undo_entries WHERE stack_type = 'redo'"
    );
    return rows[0].cnt;
  }

  async getEditHistory(limit: number, offset: number): Promise<EditHistoryEntry[]> {
    const db = this.ensureDb();
    const rows = await db.select<Array<{
      id: number; timestamp: string; operation_type: string; target_id: string | null;
      description: string | null; before_snapshot: string | null; after_snapshot: string | null;
    }>>('SELECT * FROM edit_history ORDER BY id DESC LIMIT $1 OFFSET $2', [limit, offset]);
    return rows.map(r => ({
      id: r.id, timestamp: r.timestamp, operationType: r.operation_type,
      targetId: r.target_id, description: r.description ?? '',
      beforeSnapshot: r.before_snapshot, afterSnapshot: r.after_snapshot,
    }));
  }

  async addEditHistory(entry: Omit<EditHistoryEntry, 'id'>): Promise<void> {
    const db = this.ensureDb();
    await db.execute(
      'INSERT INTO edit_history (timestamp, operation_type, target_id, description, before_snapshot, after_snapshot) VALUES ($1, $2, $3, $4, $5, $6)',
      [entry.timestamp, entry.operationType, entry.targetId ?? null,
        entry.description ?? null, entry.beforeSnapshot ?? null, entry.afterSnapshot ?? null]
    );
  }

  async importFromSequence(seq: DNASequence): Promise<void> {
    await this.setMetadata({
      name: seq.name, description: seq.description, isCircular: String(seq.isCircular),
      accession: seq.accession ?? '', organism: seq.organism ?? '', date: seq.date ?? '',
      typeOfDisplay: seq.typeOfDisplay, customName: seq.customName, fileName: seq.fileName ?? '',
    });

    const db = this.ensureDb();
    await db.execute('DELETE FROM sequence_blocks');
    await db.execute('DELETE FROM features');
    await db.execute('DELETE FROM primer_binding_sites');
    await db.execute('DELETE FROM primers');

    for (let i = 0; i < seq.sequence.length; i += BLOCK_SIZE) {
      await db.execute('INSERT INTO sequence_blocks (block_index, bases) VALUES ($1, $2)', [
        Math.floor(i / BLOCK_SIZE),
        seq.sequence.slice(i, i + BLOCK_SIZE),
      ]);
    }

    for (const feature of seq.features) { await this.addFeature(feature); }
    for (const primer of seq.primers || []) { await this.addPrimer(primer); }
  }

  async exportToSequence(): Promise<DNASequence> {
    const meta = await this.getAllMetadata();
    const length = await this.getSequenceLength();
    const sequence = await this.getSequenceChunk(0, length);
    const features = await this.getAllFeatures();
    const primers = await this.getAllPrimers();

    return {
      id: meta['accession'] || `seq-${Date.now()}`,
      name: meta['name'] || 'Untitled',
      typeOfDisplay: (meta['typeOfDisplay'] as 'file_name' | 'custom') || 'file_name',
      customName: meta['customName'] || '',
      description: meta['description'] || '',
      sequence,
      length,
      isCircular: meta['isCircular'] === 'true',
      accession: meta['accession'] || undefined,
      organism: meta['organism'] || undefined,
      date: meta['date'] || undefined,
      features,
      primers,
      restrictionSites: [],
      fileName: meta['fileName'] || undefined,
    };
  }

  async commitWorkingCopy(seq: DNASequence): Promise<void> {
    await this.importFromSequence(seq);
    await this.setMetadata({ isDirty: '0' });
    const db = this.ensureDb();
    await db.execute('PRAGMA wal_checkpoint(TRUNCATE)');
  }

  async getRawBytes(): Promise<Uint8Array> {
    if (!this.dbPath) throw new Error('No database path');
    const { readFile } = await import('@tauri-apps/plugin-fs');
    return readFile(this.dbPath);
  }
}