writeSqliteFile function
- List<
SqliteWriteTable> tables, { - int pageSize = 4096,
- List<
SqliteWriteIndex> indexes = const [],
Build a complete SQLite file from a set of tables and indexes.
Layout: page 1 holds sqlite_schema (one table-leaf with the schema
rows), then each user table/index gets one or more pages — leaves
first, then interior B-tree pages above them, with the root being
the highest page number assigned for that tree. Overflow pages for
long records follow at the very end of the file.
Both tables (rowid B-trees, page types 0x05/0x0d) and indexes (page types 0x02/0x0a) support arbitrary depth — the writer keeps building interior levels until a single root remains.
Throws StateError if the schema itself doesn't fit in one
table-leaf page (extremely large CREATE TABLE/CREATE INDEX
strings can spill onto overflow pages, but the schema-leaf page
itself is single).
Implementation
Uint8List writeSqliteFile(
List<SqliteWriteTable> tables, {
int pageSize = 4096,
List<SqliteWriteIndex> indexes = const [],
}) {
if (pageSize < 512 || pageSize > 65536 || (pageSize & (pageSize - 1)) != 0) {
throw ArgumentError(
'pageSize must be a power of two between 512 and 65536');
}
const reservedSpace = 0;
final usable = pageSize - reservedSpace;
final maxLocalTable = usable - 35;
final maxLocalIndex = ((usable - 12) * 64 ~/ 255) - 23;
final minLocal = ((usable - 12) * 32 ~/ 255) - 23;
// 1. Plan leaf cells and build a B-tree (leaves + interior pages) per
// table and per index.
final tableTrees = <List<_BTreePage>>[];
for (final t in tables) {
if (t.withoutRowid) {
// WITHOUT ROWID: store the table as an index B-tree whose cells are
// the row records themselves. Rows must already be in on-disk order
// (PK first, then the rest in declared order); the index-B-tree
// writer sorts them by the full record so the on-disk B-tree is
// PK-ordered.
final sortedRows = [...t.rows]..sort(_compareIndexKeys);
final cells = <_PlannedCell>[
for (final r in sortedRows)
_planCell(
rowid: 0,
payload: _encodeRecord(r),
maxLocal: maxLocalIndex,
minLocal: minLocal,
usable: usable,
),
];
tableTrees.add(_buildIndexBTree(cells, sortedRows, pageSize));
continue;
}
final cells = <_PlannedCell>[];
for (var r = 0; r < t.rows.length; r++) {
final rowid = t.rowids != null ? t.rowids![r] : (r + 1);
cells.add(_planCell(
rowid: rowid,
payload: _encodeRecord(t.rows[r]),
maxLocal: maxLocalTable,
minLocal: minLocal,
usable: usable));
}
if (t.rowids != null && t.rowids!.length != t.rows.length) {
throw ArgumentError(
'Table ${t.name}: rowids length (${t.rowids!.length}) '
'does not match row count (${t.rows.length})');
}
final leaves = _packTableLeaves(cells, pageSize).cast<_BTreePage>();
tableTrees.add(_buildBTree(leaves,
isIndex: false,
pageSize: pageSize,
maxLocalIndex: maxLocalIndex,
minLocal: minLocal,
usable: usable));
}
final indexTrees = <List<_BTreePage>>[];
for (final ix in indexes) {
final sorted = [...ix.entries]..sort(_compareIndexKeys);
final cells = <_PlannedCell>[
for (final e in sorted)
_planCell(
rowid: 0,
payload: _encodeRecord(e),
maxLocal: maxLocalIndex,
minLocal: minLocal,
usable: usable,
),
];
indexTrees.add(_buildIndexBTree(cells, sorted, pageSize));
}
// 2. Assign page numbers (page 1 = schema; trees get the rest).
var nextPage = 2;
for (final tree in tableTrees) {
for (final p in tree) {
p.pageNo = nextPage++;
}
}
for (final tree in indexTrees) {
for (final p in tree) {
p.pageNo = nextPage++;
}
}
// 3. Build schema rows now that we know each tree's root page.
final schemaRows = <List<Object?>>[];
for (var i = 0; i < tables.length; i++) {
final t = tables[i];
schemaRows.add([
'table',
t.name,
t.name,
tableTrees[i].last.pageNo!,
t.createSql,
]);
}
for (var i = 0; i < indexes.length; i++) {
final ix = indexes[i];
schemaRows.add([
'index',
ix.name,
ix.tableName,
indexTrees[i].last.pageNo!,
ix.createSql,
]);
}
final schemaCells = <_PlannedCell>[
for (var i = 0; i < schemaRows.length; i++)
_planCell(
rowid: i + 1,
payload: _encodeRecord(schemaRows[i]),
maxLocal: maxLocalTable,
minLocal: minLocal,
usable: usable),
];
// 4. Collect every _PlannedCell that may have overflow (leaves and
// interior separator cells), then assign overflow page numbers.
final allPlannedCells = <_PlannedCell>[...schemaCells];
void collect(List<_BTreePage> tree) {
for (final p in tree) {
if (p is _LeafPageNode) {
allPlannedCells.addAll(p.cells);
} else if (p is _InteriorPageNode && p.isIndex) {
allPlannedCells.addAll(p.indexSeparators!);
}
}
}
for (final tree in tableTrees) {
collect(tree);
}
for (final tree in indexTrees) {
collect(tree);
}
var nextOverflowPage = nextPage;
for (final c in allPlannedCells) {
if (c.overflowChunks.isEmpty) continue;
final pages = <int>[];
for (var j = 0; j < c.overflowChunks.length; j++) {
pages.add(nextOverflowPage++);
}
c.overflowPages = pages;
}
final pageCount = nextOverflowPage - 1;
// 5. Allocate the file and write header.
final out = Uint8List(pageSize * pageCount);
final hdr = SqliteHeader(
pageSize: pageSize,
fileFormatWrite: 1,
fileFormatRead: 1,
reservedSpace: reservedSpace,
textEncoding: 1,
schemaCookie: 1,
schemaFormat: 4,
userVersion: 0,
applicationId: 0,
dbSizeInPages: pageCount,
);
out.setRange(0, 100, hdr.encode());
// 6. Write the schema leaf at page 1 (header inset by 100 bytes).
_writeTableLeafPage(out,
pageOffset: 0,
pageSize: pageSize,
headerInsetOffset: 100,
planned: _PlannedLeaf(cells: schemaCells));
// 7. Write every tree page.
void writeTree(List<_BTreePage> tree) {
for (final p in tree) {
final pageOffset = (p.pageNo! - 1) * pageSize;
if (p is _LeafPageNode) {
if (p.isIndex) {
_writeIndexLeafPage(out,
pageOffset: pageOffset,
pageSize: pageSize,
planned: _PlannedLeaf(cells: p.cells));
} else {
_writeTableLeafPage(out,
pageOffset: pageOffset,
pageSize: pageSize,
headerInsetOffset: 0,
planned: _PlannedLeaf(cells: p.cells));
}
} else if (p is _InteriorPageNode) {
if (p.isIndex) {
_writeIndexInteriorPage(out,
pageOffset: pageOffset, pageSize: pageSize, planned: p);
} else {
_writeTableInteriorPage(out,
pageOffset: pageOffset, pageSize: pageSize, planned: p);
}
}
}
}
for (final tree in tableTrees) {
writeTree(tree);
}
for (final tree in indexTrees) {
writeTree(tree);
}
// 8. Write overflow chains.
for (final c in allPlannedCells) {
if (c.overflowChunks.isEmpty) continue;
for (var j = 0; j < c.overflowChunks.length; j++) {
final pageNo = c.overflowPages[j];
final pageOff = (pageNo - 1) * pageSize;
final next = j + 1 < c.overflowPages.length ? c.overflowPages[j + 1] : 0;
ByteData.sublistView(out).setUint32(pageOff, next);
out.setRange(pageOff + 4, pageOff + 4 + c.overflowChunks[j].length,
c.overflowChunks[j]);
}
}
return out;
}