openBlob method
Open an incremental BLOB I/O handle on a row's BLOB column,
analogous to SQLite's sqlite3_blob_open. rowid resolves the
target row by INTEGER PRIMARY KEY value when the table has one,
falling back to 1-based row position. The returned handle lets
callers stream bytes in/out of the column without copying the
entire blob through user code.
Writable handles cannot grow the blob — pre-size with
UPDATE t SET col = zeroblob(N) WHERE ... first if needed.
The handle holds a direct reference to the in-memory row, so concurrent mutations to the same row are visible (and may invalidate offsets). Treat handles as short-lived.
Implementation
BlobHandle openBlob({
required String table,
required String column,
required int rowid,
bool writable = false,
}) {
final t = _tables[table] ??
_tables[table.toLowerCase()] ??
_tables[table.toUpperCase()];
if (t == null) {
throw ArgumentError('No such table: $table');
}
final colIdx = t.columnIndex(column);
final col = t.columns[colIdx];
if (col.type != DataType.blob && col.type != DataType.any) {
throw ArgumentError(
'Column $table.$column is ${col.type.name}, not BLOB',
);
}
int? pkIdx;
for (var i = 0; i < t.columns.length; i++) {
final c = t.columns[i];
if (c.primaryKey && c.type == DataType.integer) {
pkIdx = i;
break;
}
}
List<Object?>? row;
if (pkIdx != null) {
for (final r in t.rows) {
final v = r[pkIdx];
if (v is int && v == rowid) {
row = r;
break;
}
}
} else {
final idx = rowid - 1;
if (idx >= 0 && idx < t.rows.length) row = t.rows[idx];
}
if (row == null) {
throw StateError('No row with rowid $rowid in $table');
}
return BlobHandle.internal(
row,
colIdx,
tableName: table,
columnName: column,
writable: writable,
);
}