.gen 文件定义

.gen 是 GenePad 的完整项目文件,文件本质是标准 SQLite 3 数据库 (文件头 16 字节为 SQLite format 3\0,MIME 登记为 application/vnd.genepad.database)。它比 .gjson 更完整, 除序列与注释外还保存引物结合位点、测序图谱(chromatogram)、比对条目、撤销/重做栈、 编辑审计、附件,以及 编辑历史树(完整克隆谱系 + 每步的序列快照)。 任何语言的标准 SQLite 驱动都能直接读写——不需要安装任何专用解析库。

SQLite format 3 14 张表 1-based inclusive BLOCK_SIZE = 10000 WAL + checkpoint SCHEMA_VERSION = 6

Overview

读取原则

  • 无需自定义二进制 parser——用任意 SQLite 驱动打开 .gen,再按下方表结构查询即可。
  • sequence_blocks 必须按 block_index 升序拼接,不能依赖数据库默认返回顺序。
  • segments、qualifiers、components、alignment、references、history_op、annotations_json 等字段是 JSON 字符串;history_tree.tree 是整棵历史树的 JSON;history_snapshots.nested_base64 是原始字节的 base64;chromatogram 的 trace 是「UTF-8 JSON 装进 BLOB」——这些都需二次解析。
  • .gen 不持久化 restrictionSites(酶切位点),由读取程序根据主序列重新检测:GenePad 在序列长度 ≤ 50,000 bp 时同步检测,超长序列走后台流式检测。
  • 保存出的 .gen 永远是自包含单文件——GenePad 落盘前执行 PRAGMA wal_checkpoint(TRUNCATE),用户目录里不会出现 -wal/-shm 伴生文件。
  • 布尔值全部以字符串 'true'/'false' 保存;键可能缺失,读取端要给默认值(见 project_meta 键参考)。
  • 编辑历史两张表(history_tree + history_snapshots)记录编辑历史(克隆谱系):根节点 = 当前文件状态,子节点 = 产生它的输入,树向过去生长。语义与字段详见编辑历史树一节。

Getting Started

快速上手:读一个 .gen(Python)

Python 内置的 sqlite3 模块就够,零第三方依赖。下面 20 行代码读出元信息、完整序列和全部 feature:

import json
import sqlite3

con = sqlite3.connect("plasmid.gen")
con.row_factory = sqlite3.Row

# —— 元信息:key-value 表,布尔以字符串保存
meta = {row["key"]: row["value"] for row in con.execute(
    "SELECT key, value FROM project_meta")}
is_circular = meta.get("isCircular") == "true"

# —— 主序列:必须按 block_index 升序拼接
sequence = "".join(row["bases"] for row in con.execute(
    "SELECT bases FROM sequence_blocks ORDER BY block_index"))
print(meta.get("name", "Untitled"), len(sequence), "bp,",
      "环状" if is_circular else "线状")

# —— features:segments / qualifiers 列是 JSON 字符串
for f in con.execute("SELECT * FROM features ORDER BY start_pos"):
    length = f["end_pos"] - f["start_pos"] + 1
    if f["start_pos"] > f["end_pos"]:                  # 跨原点的环形 feature
        length = len(sequence) - f["start_pos"] + f["end_pos"] + 1
    qualifiers = json.loads(f["qualifiers"]) if f["qualifiers"] else {}
    print(f["name"], f["type"], f["strand"], length, "bp", qualifiers)

提示:坐标是 1-based inclusive,start=1, end=10 表示第 1~10 号碱基共 10 个;环形序列跨原点时 start > end(详见坐标与环形序列)。

可以下载真实示例文件试跑:41-pPUR-P2A-BFPnls-sgRNA1-RNF2_4AtoG_MS2-MA.gen(112 KB)——7,952 bp 的环形慢病毒 sgRNA 载体,含 25 个 feature、2 条引物及结合位点。它也是一个很好的教学样本:碱基全为小写(验证「不强制大写」)、isCircular/isDamMethylated 等布尔以字符串保存、feature Misc_Feature_1 带 4 段 segments。对它运行上面代码的输出:

41-pPUR-P2A-BFPnls-sgRNA1-RNF2_4AtoG_MS2-MA 7952 bp, 环状
hPGK promoter misc_feature forward 507 bp {}
Puromycin misc_feature forward 594 bp {}
P2A misc_feature forward 57 bp {}
TagBFP2 misc_feature forward 711 bp {}
...(共 25 个 feature)

Walkthrough

实例解剖:ITR-CMV-AAV5-AfeI.gen(逐表过一遍)

下面用一份真实文件把上面所有概念串起来:ITR-CMV-AAV5-AfeI.gen——一个 8,206 bp 的环形 AAV 载体(ITR 骨架 + CMV enhancer / chicken β-actin 启动子 + AAV5 cap 基因 + NeoR/KanR + AmpR,带 AfeI 定点突变与 NNK 简并引物文库位点)。它由 GenePad 打开同名 .dna 文件后「另存为 .gen」得到,因此携带完整的编辑历史(225 个历史快照),是观察历史树表结构的最佳样本。文件总大小 37,810,176 字节(约 36 MB),其中约九成是历史快照的 base64——这也直观展示了「历史随文件走」的代价。

所有命令与输出都是对这份文件的实际运行结果,可逐条复现(换成本页提供的 小样本或任何一份 .gen 同样能跑,只是行数不同):

# ① 魔头验证:SQLite 3
$ xxd -l 16 ITR-CMV-AAV5-AfeI.gen
00000000: 5351 4c69 7465 2066 6f72 6d61 7420 3300  SQLite format 3.

# ② 打开并列出全部表(sqlite_sequence 是 AUTOINCREMENT 的簿记表,不算业务表)
$ sqlite3 ITR-CMV-AAV5-AfeI.gen
sqlite> .tables
alignment_entries     features              project_meta
attachments           history_snapshots     schema_version
base_color_ranges     history_tree          sequence_blocks
chromatogram_data     primer_binding_sites  undo_entries
edit_history          primers

# ③ 每张表各有多少行
sqlite> SELECT 'sequence_blocks', COUNT(*) FROM sequence_blocks
   ...> UNION ALL SELECT 'features', COUNT(*) FROM features
   ...> UNION ALL SELECT 'base_color_ranges', COUNT(*) FROM base_color_ranges
   ...> UNION ALL SELECT 'primers', COUNT(*) FROM primers
   ...> UNION ALL SELECT 'primer_binding_sites', COUNT(*) FROM primer_binding_sites
   ...> UNION ALL SELECT 'alignment_entries', COUNT(*) FROM alignment_entries
   ...> UNION ALL SELECT 'chromatogram_data', COUNT(*) FROM chromatogram_data
   ...> UNION ALL SELECT 'attachments', COUNT(*) FROM attachments
   ...> UNION ALL SELECT 'undo_entries', COUNT(*) FROM undo_entries
   ...> UNION ALL SELECT 'edit_history', COUNT(*) FROM edit_history
   ...> UNION ALL SELECT 'history_tree', COUNT(*) FROM history_tree
   ...> UNION ALL SELECT 'history_snapshots', COUNT(*) FROM history_snapshots
   ...> UNION ALL SELECT 'schema_version', COUNT(*) FROM schema_version;
sequence_blocks|1
features|18
base_color_ranges|4
primers|30
primer_binding_sites|30
alignment_entries|0
chromatogram_data|0
attachments|0
undo_entries|0
edit_history|0
history_tree|1
history_snapshots|225
schema_version|0

这份文件刚从 .dna 转存、尚未在 GenePad 里做新编辑,所以 undo / 审计 / 比对 / 图谱 / 附件表全空,内容都集中在序列、注释与历史三块。逐块看:

④ project_meta——19 个键全在(保存流程幂等 upsert 所致,GenBank 元数据没有就落空串):

sqlite> SELECT key, value FROM project_meta;
name|ITR-CMV-AAV5-AfeI
description|
isCircular|true
isDoubleStranded|true
isDamMethylated|false
isDcmMethylated|false
accession|
organism|
date|
moleculeType|
division|
version|
keywords|
source|
comments|
references|
typeOfDisplay|file_name
customName|ITR-CMV-AAV5-AfeI
fileName|ITR-CMV-AAV5-AfeI.dna
isDirty|0

看点:fileName 记录了它的出身(同名 .dna 转存);isCircular=true 环形质粒;Dam/Dcm 甲基化在源文件里显式为否(键值 'false';若源文件未设置,GenePad 写空串 '',读取时空串同样按 false 处理,键缺失才是「未知」)。

⑤ sequence_blocks——8,206 bp 一块装下(块上限 10,000,故只有 block 0):

sqlite> SELECT block_index, length(bases), substr(bases, 1, 50) FROM sequence_blocks;
0|8206|TAAGAAACCATTATTATCATGACATTAACCTATAAAAATAGGCGTATCACGAGG

⑥ features——18 个注释(注意多来源 CDS 的重叠与 none 方向):

sqlite> SELECT name, type, start_pos, end_pos, strand, color FROM features ORDER BY start_pos;
telL|misc_feature|625|652|none|#a6acb3
telR|misc_feature|653|680|none|#a6acb3
CMV enhancer|enhancer|911|1290|none|#ffffff
chicken β-actin promoter|promoter|1293|1568|forward|#ffffff
Cap9|CDS|2685|3209|forward|#ffcc99
VP2|CDS|3096|3209|forward|#993366
AAV5 CAP|CDS|3210|4877|forward|#ffcc99
VP2|CDS|3210|4877|forward|#ff0000
AAP5|CDS|3211|3816|forward|#993366
Variable Region|CDS|4425|4445|forward|#993366
AGC-ACC(Ser-Thr)|misc_feature|4833|4835|none|#ff0000
telR|misc_feature|5128|5155|none|#a6acb3
telL|misc_feature|5156|5183|none|#a6acb3
f1 ori|rep_origin|5184|5638|reverse|#ffff00
ITR|repeat_region|6172|6274|none|#ffe4c4
ori|rep_origin|6517|7105|reverse|#ffff00
NeoR/KanR|CDS|7276|8070|reverse|#ccffcc
AmpR promoter|promoter|8071|8175|reverse|#ffffff

看点:VP1/VP2/VP3 共用 cap 读码框(Cap9 与 VP2 区段重叠、坐标交叠是正常的);无方向注释(ITR、telL/telR、enhancer)strand='none';中文名与全角括号原样存取。qualifiers 列也带着完整 GenBank 限定词,例如 NeoR/KanR 一行:

sqlite> SELECT qualifiers FROM features WHERE name = 'NeoR/KanR';
{"transl_table":["1"],"label":["NeoR/KanR"],"gene":["aph(3')-II (or nptII)"],
 "codon_start":["1"],"product":["aminoglycoside phosphotransferase from Tn5"],
 "translation":["MIEQDGLHAGSPAAWVERLFGYDWAQQTIG…"]}

⑦ base_color_ranges——手工碱基着色(同一区间正反链各一条,对应序列视图上下链颜色):

sqlite> SELECT id, start_pos, end_pos, strand, color FROM base_color_ranges;
base-color-0|4414|4430|forward|#ef4444
base-color-2|4414|4430|reverse|#ef4444
base-color-1|4437|4453|forward|#3b82f6
base-color-3|4437|4453|reverse|#3b82f6

4414–4453 正是 AAV5 cap 的 variable region(上面 features 表里的 Variable Region 4425–4445)——作者用红/蓝两段底色把突变文库区域标了出来。

⑧ primers / primer_binding_sites——30 条引物各一个结合位点:

sqlite> SELECT id, name, sequence FROM primers LIMIT 4;
primer-0|AAV5-AfeI-NNK-R|TACGTGCCGGTCGCGGGaGC
primer-1|AAV5-AfeI-NNK|GGCCACCAACAACCAGAGCTCCACCnnknnknnknnknnknnknnkACTGCtCCCGCGACCGGCAC
primer-2|AAV5-L8-AfeI-F|CCAACAACCAGAGCgcttaaAgcGCtCCCGCGACCGGCA
primer-3|NQJ p5enhancer F|CCATGATGCTCATCAAGAACA

sqlite> SELECT id, start_pos, end_pos, bound_strand,
   ...>        round(melting_temperature, 1) AS tm, length(annealed_bases) AS annealed
   ...> FROM primer_binding_sites LIMIT 4;
primer-0-site-4437|4437|4456|reverse|69.9|20
primer-1-site-4409|4409|4453|forward|66.0|36
primer-2-site-4414|4414|4452|forward|73.6|39
primer-3-site-4603|4603|4623|forward|44.4|21

看点:primer-1 是 NNK 简并引物(连续小写 nnk 21 个简并位,用于 cap 文库饱和突变),大小写原样保留;位点 id 遵循 {primer_id}-site-{start} 固定格式;components / alignment 两列是 camelCase JSON,例如 {"hybridizedRange":"4436-4455","bases":"GCtCCCGCGACCGGCACGTA"}。

⑨ history_tree——一行存整棵克隆谱系。tree 列是 1,377,563 字符的 JSON(仅节点元数据,不含序列)。根节点原样片段(键序即写出顺序):

sqlite> SELECT substr(tree, 1, 720) FROM history_tree WHERE id = 1;
{"id":225,"name":"ITR-CMV-AAV5-AfeI.dna","seqLen":8206,"circular":true,
 "operation":"inFusionCloning","resurrectable":true,"strandedness":"double",
 "type":"DNA","upstreamModification":"Unmodified","downstreamModification":"Unmodified",
 "inputSummaries":[
   {"manipulation":"replace","val1":905,"val2":4893,
    "enzymes":[["BglII",2],["BglII",2]]},
   {"manipulation":"overlapAndInsert","val1":0,"val2":1796},
   {"manipulation":"overlapAndInsert","val1":0,"val2":1767},
   {"manipulation":"overlapAndInsert","val1":0,"val2":483}],
 "children":[ … 四个输入:骨架 + 三个片段 … ]}

这段 JSON 说的是:最终载体由一次 In-Fusion 无缝克隆组装而成——用 BglII 双酶切替换单元(905–4893,0-based 闭区间),再让三个带重叠末端的片段(1,796 / 1,767 / 483 bp)重组插入;根的四个 children 正是骨架(#20)与三个片段供体(#88 / #156 / #224,均为 PCR 扩增产物)。每个 children 元素是产生它的一个输入,递归向过去生长。用十来行 Python 就能把整棵树摘要出来:

import json, sqlite3

con = sqlite3.connect("ITR-CMV-AAV5-AfeI.gen")
tree = json.loads(con.execute(
    "SELECT tree FROM history_tree WHERE id = 1").fetchone()[0])

def walk(node, depth=0):
    inputs = "; ".join(
        f"{s['manipulation']}({s['val1']},{s['val2']})"
        for s in node.get("inputSummaries", []))
    print("  " * depth +
          f"#{node['id']} {node.get('name','')} op={node.get('operation','')} "
          f"len={node.get('seqLen')} circ={node.get('circular')} [{inputs}]")
    for child in node.get("children", []):
        walk(child, depth + 1)

walk(tree)          # 共 226 个节点(含根)

实际输出(截选——根的骨架分支、片段分支与最深处的「原始文件」节点):

#225 ITR-CMV-AAV5-AfeI.dna op=inFusionCloning len=8206 circ=True [replace(905,4893); overlapAndInsert(0,1796); overlapAndInsert(0,1767); overlapAndInsert(0,483)]
  #20 ITR-CB-Rh74.dna op=insert len=8221 circ=True [insertAt(4418,0)]
    #19 ITR-CB-Rh74.dna op=remove len=8200 circ=True [remove(4418,4438)]
      #18 ITR-CB-Rh74.dna op=remove len=8221 circ=True [remove(1402,1437)]
        …(骨干上一长串 insert / remove / insertRestrictionSite 微编辑)…
          #1 Untitled 7.dna op=replace len=8301 circ=True [replace(2684,4849)]
            #0 Untitled 7.dna op=invalid len=8250 circ=True []      ← 原始导入文件
  #88 CB.dna op=amplifyFragment len=1797 circ=False [amplify(887,2683)]
    #87 ITR-CMV-AAV5-AfeI.dna op=insert len=8106 circ=True [insertAt(4433,0)]
      …(cap 供体分支,一路追溯到)…
          #180 pAAV5-XR5-right-NQJ.dna op=invalid len=7438 circ=True []   ← 另一原始文件
  #156 Cap-1.dna op=amplifyFragment len=1768 circ=False [amplify(2666,4433)]   ┐
  #224 Cap-2.dna op=amplifyFragment len=484  circ=False [amplify(4413,4896)]   ┘ 另两个片段供体

读法:op=invalid 是「从外部文件导入」的起点节点;骨干分支(Untitled 7 → AarI → ITR-CB-Rh74)记录了载体骨架的每一步酶切/插入/定点突变,cap 供体分支(pAAV5-XR5… → CB.dna 的 PCR 扩增 amplifyFragment)最终作为 In-Fusion 片段汇入根节点——一份完整可追溯的克隆实验记录。GenePad 的「历史」面板渲染的就是这两张表。

⑩ history_snapshots——225 个历史时点的序列快照:

sqlite> SELECT seq_type, COUNT(*), SUM(length(nested_base64)) FROM history_snapshots;
1|225|34367936          -- 全部为压缩 DNA 快照;nested 共 34.4 MB base64

sqlite> SELECT node_id, length(sequence) AS seq_len, seq_type,
   ...>        length(nested_base64) AS nested FROM history_snapshots
   ...> ORDER BY node_id LIMIT 4;
node_id|seq_len|seq_type|nested
0|8250|1|8032       -- 原始导入文件(树里 #0)的序列与嵌套标注
1|8301|1|14092
2|8304|1|14308
3|8307|1|14308

sqlite> SELECT substr(sequence, 1, 60) FROM history_snapshots WHERE node_id = 0;
TAAGAAACCATTATTATCATGACATTAACCTATAAAAATAGGCGTATCACGAGGCCCTTT

sqlite> SELECT COUNT(*) FROM history_snapshots WHERE node_id = 225;
0                       -- 根节点永远没有快照(= 当前文件状态)

sequence 列已经是 GenePad 解好的可读碱基串(打开 .dna 时把分段压缩快照解码后存入),nested_base64 是快照随附的嵌套 TLV 原始字节(该历史时点的 features/primers/notes 等标注包)的 base64,供「打开快照」时惰性解码。34.4 MB base64 ≈ 25.8 MB 原始字节,就是这份文件 36 MB 体积的来源——不需要历史时可用写回一节的瘦身配方清掉。

如何获得同类文件:拿任意一份带历史的 .dna 文件(商业合成载体常自带克隆谱系)在 GenePad 里打开,「另存为」选 .gen 即可——历史树会随转换完整落入这两张表;此后在 GenePad 里的每次编辑也会增量追加快照。

Schema

核心表结构(全部 14 张)

project_meta项目元信息 key-value 表
字段类型说明
keyTEXT PRIMARY KEY元信息键
valueTEXT元信息值,布尔值也以字符串保存

保存时按 key upsert(INSERT ... ON CONFLICT(key) DO UPDATE),从不整表清空——因此旧文件里多余的自定义 key 会原样保留。下表是 GenePad 会写入的全部键:

key写入值读取行为 / 默认说明
namestring缺失 → 'Untitled'项目名(GenBank LOCUS 名)
descriptionstring缺失 → ''描述(GenBank DEFINITION)
isCircular'true'/'false'缺失 → false拓扑:环状 / 线状,无独立 topology 列
isDoubleStranded'true'/'false'缺失或任何非 'false' 值 → true双链 / 单链
isDamMethylated'true'/'false'/''键缺失 → 未知(undefined);'' → falseDam 甲基化状态
isDcmMethylated'true'/'false'/''同上Dcm 甲基化状态
accessionstring缺失 → undefinedGenBank ACCESSION
organismstring缺失 → undefinedGenBank ORGANISM
datestring缺失 → undefinedGenBank LOCUS 行日期
moleculeTypestring缺失 → undefinedLOCUS 分子类型(DNA 等)
divisionstring缺失 → undefinedGenBank division 代码
versionstring缺失 → undefinedGenBank VERSION 行
keywordsstring缺失 → undefinedGenBank KEYWORDS
sourcestring缺失 → undefinedGenBank SOURCE
commentsstring缺失 → undefinedGenBank COMMENT
referencesJSON 字符串 GenBankRef[]非法 JSON → undefined文献条目,形状见下方
typeOfDisplay'file_name'/'custom'缺失 → 'file_name'标签页标题来源
customNamestring缺失 → ''自定义显示名(typeOfDisplay 为 custom 时生效)
fileNamestring缺失 → undefined原始导入文件名;桌面端打开时会被实际文件名覆盖
isDirty'0'加载时不读取保存流程的簿记字段,每次落盘后写 '0'

GenBankRef 的 JSON 形状(references 键的值):

// JSON: GenBankRef[]
{
  "number": 1,                  // 必有
  "authors":  "Smith J., Lee K.",
  "title":    "Cloning of pBR322",
  "journal":  "Nucleic Acids Res 2026",
  "pubmed":   "12345678",
  "remark":   "..."             // 其余字段均可省略
}
sequence_blocks主序列分块
字段类型说明
block_indexINTEGER PRIMARY KEY0-based block index
basesTEXT NOT NULL该块碱基字符串,除最后一块外恰为 10,000 bases
SELECT bases FROM sequence_blocks ORDER BY block_index;

分块规则:每块 BLOCK_SIZE = 10000 bp,block_index = floor(offset / 10000)(offset 为 0-based 偏移)——块 0 存偏移 0–9999(即 1-based 位置 1–10000),块 1 存 10000–19999,最后一块可以不足 10,000。没有 start_pos 列:块起点 = block_index × 10000。序列总长用 SELECT COALESCE(SUM(LENGTH(bases)), 0) FROM sequence_blocks 计算。写入时全删全写(非增量更新),block_index 必须从 0 起连续无空洞——GenePad 按范围取块拼接,中间缺块会静默错位。碱基大小写保留原样(不强制大写),字符集为 IUPAC。

features序列注释
字段类型说明
idTEXT PRIMARY KEYfeature id,GenePad 生成格式 {prefix}-{timestamp}-{counter}
nameTEXT NOT NULLfeature 名称
typeTEXT NOT NULL如 gene, CDS, promoter, rep_origin, misc_feature
start_posINTEGER NOT NULL1-based inclusive start
end_posINTEGER NOT NULL1-based inclusive end
strandTEXT NOT NULLforward / reverse / none
colorTEXT显示颜色,CSS 颜色字符串(如 #e0502a),NULL 走默认色
labelTEXT显示标签(缺省用 name)
noteTEXT备注
frameINTEGER阅读框(0/1/2),可 NULL
visibleINTEGER DEFAULT 11 可见,0 隐藏;缺省按 1 处理
segmentsTEXTJSON:FeatureSegment[],仅多片段(>1 段)feature 写入,否则 NULL
qualifiersTEXTJSON:Record<string, string[]>,GenBank 限定词(/translation、/codon_start、/gene…)

索引:idx_features_range ON features(start_pos, end_pos);GenePad 读取时按 ORDER BY start_pos 返回,范围查询用闭区间重叠判定 start_pos <= rangeEnd AND end_pos >= rangeStart。

两个 JSON 列的形状:

// segments 列 → JSON: FeatureSegment[](段数 > 1 时才写入)
[
  { "start": 1,    "end": 120,  "strand": "forward" },
  { "start": 500,  "end": 874,  "strand": "forward",
    "name": " linker ",  "color": "#3aaea0" }   // name / color / strand 可选
]

// qualifiers 列 → JSON: Record<string, string[]>
{
  "gene":        ["AmpR"],
  "codon_start": ["1"],
  "translation": ["MSIQ..."]
}
primers / primer_binding_sites引物与结合位点(按 primer_id 关联)
字段类型说明
primers.idTEXT PRIMARY KEY引物 id
primers.nameTEXT NOT NULL引物名称
primers.sequenceTEXT NOT NULL引物序列(5'→3')
primers.descriptionTEXT描述
primers.visibleINTEGER DEFAULT 11 显示,0 隐藏
primer_binding_sites.idTEXT PRIMARY KEY固定格式 {primer_id}-site-{start}
primer_binding_sites.primer_idTEXT NOT NULL所属引物
primer_binding_sites.start_pos / end_posINTEGER NOT NULL1-based inclusive 结合范围
primer_binding_sites.bound_strandTEXT NOT NULLforward / reverse
primer_binding_sites.annealed_basesTEXT退火碱基,读取时 NULL → ''
primer_binding_sites.melting_temperatureREAL解链温度,读取时 NULL → 0
primer_binding_sites.componentsTEXTJSON:PrimerComponent[],恒写入(可为 [])
primer_binding_sites.alignmentTEXTJSON:AlignmentSegment[],可 NULL

索引:idx_binding_range ON primer_binding_sites(start_pos, end_pos)、idx_binding_primer ON primer_binding_sites(primer_id)。两个 JSON 列的形状:

// components 列 → JSON: PrimerComponent[]
[ { "hybridizedRange": "25-48", "bases": "ATGGC..." } ]  // hybridizedRange 可选

// alignment 列 → JSON: AlignmentSegment[]
[
  { "type": "annealed",   "primerBases": "ATG",
    "templateBases": "TAC", "templateStart": 101, "templateEnd": 103 },
  { "type": "unannealed", "primerBases": "AAA",
    "templateBases": "CGT", "templateStart": 104, "templateEnd": 106 },
  { "type": "del",        "primerBases": "TT",
    "templateBases": "",   "templateStart": 107, "templateEnd": 107 }
]
base_color_ranges碱基颜色范围(手工着色)
字段类型说明
idTEXT PRIMARY KEY颜色段 id
start_posINTEGER NOT NULL1-based inclusive start
end_posINTEGER NOT NULL1-based inclusive end
strandTEXT NOT NULL仅 forward / reverse(读取时其他值一律归为 forward)
colorTEXT NOT NULL颜色值

索引:idx_base_color_ranges ON base_color_ranges(start_pos, end_pos, strand)。整表替换式写入(DELETE 后逐行 INSERT)。

alignment_entries比对条目(Sanger 测序比对到主序列)
字段类型说明
idTEXT PRIMARY KEY比对条目 id
nameTEXT NOT NULL条目名(通常为样品/引物名)
sequenceTEXT NOT NULL被比对的读段序列
visibleINTEGER DEFAULT 11 显示,0 隐藏
features_jsonTEXT预留列,当前版本不写入
chromatogram_jsonTEXTJSON:ChromatogramData(见下),可 NULL

chromatogram_json 的形状(与 chromatogram_data 表同构,但这里存 JSON 字符串、非 BLOB):

// chromatogram_json → JSON: ChromatogramData
{
  "traceA":        [12, 45, 178, ...],   // 四通道荧光强度,number[]
  "traceC":        [8,  31, 96,  ...],
  "traceG":        [0,  12, 240, ...],
  "traceT":        [3,  60, 15,  ...],
  "peakLocations": [14, 25, 36, ...]     // 峰位索引,number[]
}
chromatogram_data主序列测序图谱(id 固定 'primary')
字段类型说明
idTEXT PRIMARY KEY主序列图谱固定为 'primary',整表仅此一行
trace_a / trace_c / trace_g / trace_tBLOBA/C/G/T 四通道荧光曲线
peak_locationsBLOB碱基峰位索引数组

BLOB 不是二进制浮点——内容是 JSON.stringify(number[]) 的 UTF-8 字节(TextEncoder 编码)。解码:JSON.parse(bytes.decode('utf-8')) / new TextDecoder().decode(blob)。写入流程为 DELETE WHERE id='primary' 再 INSERT,无该行表示主序列无图谱。

undo_entries撤销/重做栈(持久化,跨会话可用)
字段类型说明
sequence_numberINTEGER PRIMARY KEY AUTOINCREMENT单调递增序号,栈顶 = 最大值
stack_typeTEXT NOT NULL DEFAULT 'undo''undo' 或 'redo'(同一张表存两条栈)
operation_typeTEXT NOT NULL操作类型,取值见下
target_idTEXT目标实体 id,序列级操作为 NULL
before_diff / after_diffTEXTJSON:操作前/后差异负载,形状见下
history_opTEXTJSON:{ nodeId, operation, manipulation, val1, val2 }——序列编辑对应的编辑历史树节点元数据;undo 按 nodeId 弹根、redo 按相同 id 重挂。2026-09 起写入;旧库由迁移补列,旧行 NULL 时 undo 退化为仅同步根节点长度

栈机制:undo 栈上限 200 条,超出删最旧(按 sequence_number);每次压入新操作先清空整个 redo 栈;撤销 = 把最新 undo 行的 stack_type 翻转为 'redo';重做 = 读取后直接删除该行。operation_type 取值:addFeature, removeFeature, updateFeature, applyBaseColor, clearBaseColor, addPrimer, removePrimer, updatePrimer, insertBases, deleteBases, deleteBasesWrapped, replaceBases, replaceBasesWrapped, replaceSequence, addAlignmentEntry, removeAlignmentEntry(*Wrapped = 环形序列跨起点的删除/替换)。diff payload 形状:

// 实体操作:before/after 为完整对象或 null
add:    { before: null,            after: Feature | Primer }
update: { before: { id, changes }, after: { id, changes } }   // changes = Partial<Feature|Primer>
applyBaseColor / clearBaseColor:
        { baseColorRanges: BaseColorRange[] }

// 序列编辑:附带编辑时刻的注释快照 + 编辑参数
InsertBasesDiff  = Snapshot & { start: number, bases: string }
DeleteBasesDiff  = Snapshot & { start: number, bases?: string, length?: number }
ReplaceBasesDiff = Snapshot & { start: number, oldBases?: string, newBases?: string }
// Snapshot = { features, baseColorRanges, primers, restrictionSites }
edit_history编辑审计日志(滚动保留最近 500 条)
字段类型说明
idINTEGER PRIMARY KEY AUTOINCREMENT行号
timestampTEXT NOT NULLnew Date().toISOString() 格式(如 2026-08-22T03:14:07.000Z)
operation_typeTEXT NOT NULL与 undo_entries 同一套操作类型
target_idTEXT目标实体 id,可 NULL
descriptionTEXT人类可读描述,如 Added feature 'AmpR' at 12..874
before_snapshot / after_snapshotTEXTJSON:受影响实体(增删为完整对象)或小编辑对象(如 { position, bases }、{ start, oldBases })

只追加、保存时不清空,但不是无限增长:每累计 50 次写入清理一次超限旧行,只保留最近 500 条(防止 .gen 随编辑无限膨胀;撤销用的是 undo_entries,不受此限)。可以用它做外部审计或改动统计;需要更长审计请自行定期导出。

history_tree / history_snapshots编辑历史(2026-09 起;克隆谱系 + 序列快照)
表字段说明
history_treeid INTEGER PK(CHECK id=1)全表恒一行
tree TEXT NOT NULL整棵历史树的 JSON——只有节点元数据,不含序列;每次树结构变化(编辑/undo/裁切/清空)整行重写
history_snapshotsnode_id INTEGER PK对应树节点 id;根节点永远没有快照(根 = 当前文件状态)
sequence TEXT NOT NULL该历史时点的完整序列(GenePad 已解好的可读碱基串)
seq_type INTEGER1=压缩 DNA(默认)、0/21/32=明文(蛋白/RNA)、29=仅修饰符(序列由父状态反推)
nested_base64 TEXT快照随附的嵌套 TLV 原始字节(该时点 features/primers/notes 标注包)的 base64,可 NULL
annotations_json TEXTGenePad 自产快照的编辑前时点标注(结构化 JSON,见下),可 NULL;由 .dna 文件转存而来的 .gen 此列为 NULL(时点标注随 nested_base64 携带)

写入时序:编辑时快照按 node_id 增量 upsert(ON CONFLICT DO UPDATE),分支裁切按 id 批量 DELETE;保存(另存为)时两张表全删全写。树 JSON 的节点形状与操作语义见编辑历史树一节;真实文件示例见ITR 解剖。

// annotations_json → SnapshotAnnotations(.dna 嵌套 TLV 的结构化对应物)
{
  "features": [ /* Feature[],形状同 features 表行 → JSON */ ],
  "primers":  [ /* Primer[],形状同 primers + 结合位点 */ ],
  "notes":      { "name": "...", "description": "..." },   // 可选
  "baseColorRanges": [ /* BaseColorRange[],可选 */ ],
  "isDoubleStranded": true                                   // 可选
}
attachments / schema_version预留表
表结构状态
attachmentsid TEXT PK, name TEXT NOT NULL, mime_type TEXT NOT NULL, data BLOB NOT NULL预留:schema 每次都会建表,但当前版本无写入入口(id 约定为 att-{timestamp},data 存原始字节)
schema_versionkey TEXT PK, value TEXT预留:当前版本从不写入行。常量 SCHEMA_VERSION = 6 仅存在于代码中

兼容机制因此不是版本号门控:GenePad 打开文件时执行幂等 DDL(全部 CREATE TABLE IF NOT EXISTS,外加四条吞掉「列已存在」错误的 ALTER TABLE:features ADD COLUMN segments / qualifiers、undo_entries ADD COLUMN history_op、history_snapshots ADD COLUMN annotations_json),旧文件缺列/缺表会在打开时自动补齐。外部读取程序应对缺列容错(老文件 features 没有 segments/qualifiers 列,更老的库没有 history_* 两表)。

连接参数:GenePad 打开数据库即执行 PRAGMA journal_mode=WAL 与 PRAGMA synchronous=NORMAL;保存前执行 PRAGMA wal_checkpoint(TRUNCATE) 再整体复制字节,所以成品 .gen 永远自包含。外部工具用默认 journal(delete 模式)写入同样合法——GenePad 重新打开时会自动切回 WAL。

Conventions

坐标与环形序列

feature、primer binding site、base_color_range 的 start_pos / end_pos 均为 1-based inclusive:第一个碱基的位置是 1,start=1, end=10 表示第 1 到第 10 个碱基,长度 = end - start + 1。

环形序列(isCircular='true')的 feature 跨越原点时直接存 start > end,长度公式变为 seqLength - start + end + 1——从 start 走到序列末尾,再从 1 绕回到 end。例:1000 bp 环形质粒上 start=995, end=5 覆盖 995–1000 加 1–5 共 11 个碱基。除 isCircular 标志外没有任何额外的环形专用字段。

多段 feature(如移码拼接、内含子跳过的 CDS)不用多行表示,而是把各段写进 segments JSON 数组,每段独立 { start, end, strand?, name?, color? },同样是 1-based inclusive、支持 start > end 跨原点。

注意两套体系:sequence_blocks.block_index 与序列偏移是 0-based,而所有注释坐标是 1-based。换算:1-based 位置 p 位于块 floor((p-1) / 10000)。

History Tree

编辑历史树:字段与语义

GenePad 的编辑历史是一棵向过去生长的树:根节点 = 当前文件状态,子节点 = 产生它的输入。每次编辑时,旧根降级为新根的子节点;undo「弹根」、redo「重挂」。在 .gen 里树元数据进 history_tree.tree,每个非根节点的序列快照进 history_snapshots。节点 JSON 形状:

字段类型说明
idnumber节点 id,整树唯一;快照表按它关联
namestring来源文件名(如 ITR-CB-Rh74.dna)或 GenePad 生成的操作描述名
seqLennumber该时点序列长度
circularboolean该时点拓扑
operationstring产生该节点的操作:invalid(原始导入文件)、insert / remove / replace / flip(反向互补)/ changeTopology、insertRestrictionSite、setOrigin、digest / amplifyFragment(酶切/PCR)、ligateFragments / insertFragments / inFusionCloning(组装)等
resurrectableboolean?undo 弹根后的节点标记(可「复活」重挂)
strandedness / typestring?链数(double/single)与分子类型(DNA 等)
upstreamModification / downstreamModificationstring?末端修饰状态(如 Unmodified)
inputSummariesInputSummary[]父编辑对该输入的处理,见下
oligos{ name, sequence, phosphorylated? }[]?PCR 扩增等操作使用的引物
parameters[string, string][]?操作参数对(如聚合酶、blunt 末端),保持写出顺序
childrenNode[]递归子节点(输入),树深可达数百层
extraAttrs / rawChildrenobject? / string[]?未建模 XML 属性 / 子元素的原样透传(.dna 往返保真用)

InputSummary({ manipulation, val1, val2, enzymes?, extras? })描述父编辑对该输入做了什么。⚠️ val1/val2 是 0-based 闭区间——与 .gen 注释列的 1-based 不同,换算时务必 ±1。常见 manipulation:日常编辑 insert/insertAt(val1=0-based 位点)、remove、replace、flip、changeTopology;克隆组装 overlapAndInsert、ligateFragments、insertFragments;片段处理 amplify、digest(enzymes 为 [酶名, 位点数] 对的数组)。环形跨起点的编辑存为 val1 > val2 的环绕区间。

快照侧:seq_type=1 的压缩 DNA 在 GenePad 打开 .dna 时已解码为可读序列落库;seq_type=29 的「仅修饰符」快照没有序列载荷,序列由父状态经修饰符反推后同样落为可读序列。nested_base64 只做原样透传(打开快照时才惰性解码其中的时点标注),annotations_json 则是 GenePad 自己记录编辑时捕获的结构化时点标注——两者并存互不覆盖,解析时结构化 annotations 优先。

格式间流转:.gen ↔ .dna 互转(另存为)时历史随文件自动携带(.gen 两表 ↔ .dna Block 7/11);.gjson、GenBank、FASTA 没有历史字段,另存为这些格式会丢弃历史。

Behavior

保存与兼容行为

行为规则
保存策略内容表(sequence_blocks、features、base_color_ranges、primers、primer_binding_sites、chromatogram_data、history_tree、history_snapshots,及已加载比对的 alignment_entries)保存时全删全写;project_meta 按 key upsert;undo_entries、attachments 从不清空,edit_history 滚动保留 500 条。没有「精简导出」选项——历史随文件走
编辑期增量写编辑过程中序列整表重建(writeSequence 从位置 0 重写全部分块,不做块级增量);每次序列编辑同时 upsert 一条历史快照(25 行/批多值 upsert)并整行重写 history_tree.tree;实体操作(feature/引物增删改)走单行 INSERT/UPDATE/DELETE
批量写入sequence_blocks 每 100 行一条多值 INSERT,features 每 40 行一条(13 列 × 40 = 520 参数),history_snapshots 每 25 行一条;无显式事务,逐条语句自动提交
工作副本打开 .gen 时先整体复制到临时目录(genepad-<random>.gen)再编辑,保存时 commitWorkingCopy(全量重写 + isDirty='0' + checkpoint)后把临时库字节整体写回目标路径
损坏容错所有 JSON 列解析失败一律降级为 undefined/空,不报错(历史树 JSON 损坏则视为无历史);Android 导入路径会先校验 16 字节魔头 SQLite format 3\0
大小写碱基大小写按原样存取(不强制大写);字符集为 IUPAC 扩展字母
加密无——标准未加密 SQLite 文件,任何工具可读

Recipe

导出为 .gjson

把整个项目导出成 .gjson——GenePad 的文本 JSON 交换格式。下面这个脚本复刻了 GenePad 写出器的全部行为(键名、null 语义、空数组省略规则都一致),可直接交给 GenePad 或任何工具读回:

import json, sqlite3, time

def gen_to_gjson(path):
    con = sqlite3.connect(path)
    con.row_factory = sqlite3.Row
    j = lambda s: json.loads(s) if s else None
    meta = {r["key"]: r["value"] for r in con.execute("SELECT * FROM project_meta")}
    n = lambda k: meta.get(k) or None          # '' -> None:空串等同未设置
    sequence = "".join(r["bases"] for r in con.execute(
        "SELECT bases FROM sequence_blocks ORDER BY block_index"))

    def segs_of(raw):                          # 多段(>1)feature 才带 segments 键
        if not raw: return {}
        segs = json.loads(raw)
        if len(segs) <= 1: return {}
        def seg(s):                            # 段级 strand/name/color 逐段可选
            out = {"start": s["start"], "end": s["end"]}
            for k in ("strand", "name", "color"):
                if s.get(k): out[k] = s[k]
            return out
        return {"segments": [seg(s) for s in segs]}

    # .gen 里这两个 JSON 列是 camelCase,.gjson 里是 snake_case
    comp  = lambda c: [{"hybridized_range": x.get("hybridizedRange"),
                        "bases": x["bases"]} for x in c]
    align = lambda a: [{"type": x["type"], "primer_bases": x["primerBases"],
                        "template_bases": x["templateBases"],
                        "template_start": x["templateStart"],
                        "template_end": x["templateEnd"]} for x in a]

    features = [{
        "id": f["id"], "name": f["name"], "type": f["type"],
        "start": f["start_pos"], "end": f["end_pos"], "strand": f["strand"],
        "color": f["color"], "label": f["label"], "note": f["note"],
        "frame": f["frame"], "visible": bool(f["visible"]),
        **segs_of(f["segments"]), "qualifiers": j(f["qualifiers"]),
    } for f in con.execute("SELECT * FROM features ORDER BY start_pos")]

    primers = [{
        "id": p["id"], "name": p["name"], "sequence": p["sequence"],
        "description": p["description"],
        "bindingSites": [{
            "start": b["start_pos"], "end": b["end_pos"], "boundStrand": b["bound_strand"],
            "annealedBases": b["annealed_bases"] or "",
            "meltingTemperature": b["melting_temperature"] or 0.0,
            "components": comp(json.loads(b["components"]) if b["components"] else []),
            **({"alignment": align(json.loads(b["alignment"]))} if b["alignment"] else {}),
        } for b in con.execute("SELECT * FROM primer_binding_sites WHERE primer_id = ?",
                               (p["id"],))],
        "visible": None,                       # 写出器固定写 null
    } for p in con.execute("SELECT * FROM primers")]

    cnt = lambda t: con.execute(f"SELECT COUNT(*) FROM {t}").fetchone()[0]
    doc = {
        "version": "1.9",
        "id": f"gjson-{int(time.time() * 1000)}",      # 毫秒时间戳
        "type_of_display": "file_name",
        "custom_name": meta.get("name", "Untitled"),
        "description": meta.get("description", ""),
        "sequence": sequence,
        "length": len(sequence),
        "isCircular": meta.get("isCircular") == "true",
        "isDoubleStranded": meta.get("isDoubleStranded") != "false",
        "accession": n("accession"), "organism": n("organism"), "date": n("date"),
        "moleculeType": n("moleculeType"), "division": n("division"),
        "gbVersion": n("version"), "keywords": n("keywords"),
        "source": n("source"), "comments": n("comments"),
        "references": j(meta.get("references")),
        "features": features,
        **({"baseColorRanges": [{"id": r["id"], "color": r["color"],
                                 "start": r["start_pos"], "end": r["end_pos"],
                                 "strand": r["strand"]}
                                for r in con.execute("SELECT * FROM base_color_ranges")]}
           if cnt("base_color_ranges") else {}),
        **({"primers": primers} if primers else {}),
        **({"alignmentEntries": [{"id": a["id"], "name": a["name"],
                                  "sequence": a["sequence"], "visible": bool(a["visible"])}
                                 for a in con.execute("SELECT * FROM alignment_entries")]}
           if cnt("alignment_entries") else {}),
    }
    con.close()
    return doc

# GenePad 写出 LF 换行;Windows 上 Python 需显式 newline="\n" 才能完全一致
with open("plasmid.gjson", "w", encoding="utf-8", newline="\n") as f:
    json.dump(gen_to_gjson("plasmid.gen"), f, ensure_ascii=False, indent=2)

此脚本已与 GenePad 自身导出的同项目 .gjson 逐一核对:除两处原理性差异外完全一致——id(写出时刻的毫秒时间戳)与 feature 顺序(GenePad 按导出时的内存顺序,重开 .gen 后只能得到 start_pos 顺序)。可以用本页提供的两个样本文件自行复核:.gen 源文件、GenePad 导出的 .gjson。

转换要点:organism 等 GenBank 元数据在 .gen 里是空串 ''、在 .gjson 里写 null(空串等同未设置);结合位点 JSON 的键名从 camelCase 转为 snake_case;primers[].visible 恒为 null;baseColorRanges/primers/alignmentEntries 为空时整个键省略;segments 的段级 name/color 与 .gen JSON 列同形、直接透传即可;测序图谱(chromatogram)与编辑历史(history_* 两表)不进 .gjson。完整规则见 .gjson 文件定义。

Recipe

写回:外部修改 .gen

GenePad 对外部改动完全宽容——只要仍是合法 SQLite 且符合上述约定,重新打开即生效:

import sqlite3

con = sqlite3.connect("plasmid.gen")

# 改 feature 颜色 / 项目名 / 拓扑
con.execute("UPDATE features SET color = ? WHERE name = ?", ("#7c5cff", "AmpR"))
con.execute("UPDATE project_meta SET value = 'true' WHERE key = 'isCircular'")
con.execute("UPDATE project_meta SET value = ?   WHERE key = 'name'", ("pBR322-mod",))

# 增删碱基必须整表重建 sequence_blocks:block_index 从 0 连续,块长 10000
new_seq = "ATG" + "GCT" * 100          # ...你的新序列
con.execute("DELETE FROM sequence_blocks")
con.executemany("INSERT INTO sequence_blocks VALUES (?, ?)",
                [(i // 10000, new_seq[i:i + 10000])
                 for i in range(0, len(new_seq), 10000)])
con.commit()

注意:改动碱基长度后要同步修正受影响 feature 的坐标(GenePad 不会自动重算);isDirty 只是保存簿记,加载时不读取,外部改动无需维护它。用 Python 默认 journal 写出的文件即为自包含单文件。

常用配方——给带历史的 .gen 瘦身:历史快照可能占掉文件九成体积(见ITR 解剖的 36 MB 例子)。不需要克隆谱系时,外部清空两张历史表再 VACUUM 回收空间即可,GenePad 重新打开一切正常(无历史 = 普通文件):

import sqlite3

con = sqlite3.connect("ITR-CMV-AAV5-AfeI.gen")
con.execute("DELETE FROM history_tree")       # 等价于应用内「清空历史」
con.execute("DELETE FROM history_snapshots")
con.commit()
con.execute("VACUUM")                          # 关键:真正把字节还给文件系统
con.close()
# 实测:37,810,176 B → 151,552 B(约 148 KB),序列与注释完好

Recipe

从零创建 .gen

最小可用文件只需要三张表——GenePad 打开时会自动补齐其余表和索引。控制解读方式的 project_meta 键如下:

key推荐写入缺省时读取结果
isCircular'true'(环形)或 'false'(线形)线形
isDoubleStranded'true'(双链)或 'false'(单链)双链
isDamMethylated'true' / 'false',不写则不设置未知(undefined)
isDcmMethylated'true' / 'false',不写则不设置未知(undefined)
import json, sqlite3

name, seq = "pDemo", "ATGGCTAGC" * 111           # 999 bp 示例序列

con = sqlite3.connect("demo.gen")
con.executescript("""
CREATE TABLE project_meta   (key TEXT PRIMARY KEY, value TEXT);
CREATE TABLE sequence_blocks(block_index INTEGER PRIMARY KEY, bases TEXT NOT NULL);
CREATE TABLE 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,
    segments TEXT, qualifiers TEXT);
""")

con.executemany("INSERT INTO project_meta VALUES (?, ?)", {
    "name":       name,
    "isCircular": "true",          # 见上表;isDoubleStranded 缺省即双链
    "isDirty":    "0",
}.items())

BLOCK = 10000                       # 与 GenePad 的 BLOCK_SIZE 一致
con.executemany("INSERT INTO sequence_blocks VALUES (?, ?)",
                [(i // BLOCK, seq[i:i + BLOCK])
                 for i in range(0, len(seq), BLOCK)])

con.execute("INSERT INTO features VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)", (
    "feat-1", "GFP", "CDS", 101, 804, "forward",
    None, None, None, None, 1, None,
    json.dumps({"gene": ["gfp"]}),          # qualifiers:Record<string, string[]>
))
con.commit()

要点:feature 的 13 列一个都不能少(后几列写 NULL 即可);块长 10000 且 block_index 连续;布尔键用小写字符串。之后 GenePad(或任何读取程序)就能直接打开这个文件。

Quick Look

用 sqlite3 命令行看一眼

# 打开 .gen(本质就是 SQLite;可换成本页提供的示例文件)
sqlite3 example.gen

# 列出所有表 / 建表语句
.tables
.schema features

# 读取项目名 / 拓扑
SELECT key, value FROM project_meta WHERE key IN ('name','isCircular');

# 拼接完整序列(注意按 block_index 排序)
SELECT group_concat(bases, '') FROM (SELECT bases FROM sequence_blocks ORDER BY block_index);

# 查看前几个 feature(坐标 1-based inclusive)
SELECT name, type, start_pos, end_pos, strand FROM features ORDER BY start_pos LIMIT 5;

# 看绑定位点 / 编辑历史
SELECT primer_id, start_pos, end_pos, bound_strand FROM primer_binding_sites LIMIT 5;
SELECT timestamp, description FROM edit_history ORDER BY id DESC LIMIT 5;

# 编辑历史树:根节点元数据 + 各快照长度(详见「编辑历史树」与「实例解剖」两节)
SELECT json_extract(tree, '$.id'), json_extract(tree, '$.operation'),
       json_extract(tree, '$.seqLen'), json_extract(tree, '$.circular')
FROM history_tree WHERE id = 1;
SELECT node_id, length(sequence), seq_type FROM history_snapshots ORDER BY node_id LIMIT 5;

# 解码主序列测序图谱的 A 通道(BLOB 里是 UTF-8 JSON 数组)
SELECT CAST(trace_a AS TEXT) FROM chromatogram_data WHERE id = 'primary';

# 顺手验证魔头:应为 "SQLite format 3" + \0
xxd -l 16 example.gen

只读场景请用 sqlite3 "file:example.gen?immutable=1" 打开——避免在文件旁产生 -wal/-shm 副产品(GenePad 自带的质粒库扫描器就是这么做的,Rust 示例见Rust 读取)。

See Also

延伸

  • .gjson 文件定义——单文件 JSON 交换格式,适合脚本生成与轻量共享。
  • .dna 转换——SnapGene .dna 的读取与转换说明。
  • Rust 读取示例——rusqlite 依赖配置、只读(immutable)打开与 .gjson 读取。