1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
use std::sync::Arc;
use arrow_array::{ArrayRef, RecordBatch};
use arrow_schema::{ArrowError, SchemaRef};
use rusqlite::Connection;
use rusqlite::types::ValueRef;
use crate::value::DateTimeParsing;
use crate::{BoundingBox, Result};
use super::aggregate::AggregateState;
use super::builder::ColumnBuilder;
use super::options::DEFAULT_BATCH_SIZE;
/// One layer's batches read on the calling thread.
pub(crate) struct SequentialBatches<'a> {
pub(crate) conn: &'a Connection,
pub(crate) schema: SchemaRef,
/// The direct-loop query, selecting the columns as ordinary result columns.
pub(crate) sql: String,
/// The aggregate-path query, wrapping the same columns in the registered
/// function. Built on first use.
pub(crate) aggregate_sql: String,
/// Index of the field that is also the pagination key, when it is one of
/// them. `None` means the key is selected as an extra leading column.
pub(crate) key_field: Option<usize>,
/// Index of the geometry field, whose values need the GPB header stripped.
pub(crate) geometry_index: Option<usize>,
pub(crate) names: Vec<String>,
pub(crate) datetime: DateTimeParsing,
/// Ceiling on the geometry bytes one batch may contain.
pub(crate) max_batch_bytes: usize,
/// Rows in the batch just produced. The parallel path reads it to tell a
/// batch cut short by the byte ceiling from one that filled its window.
pub(crate) last_batch_rows: usize,
pub(crate) batch_size: usize,
/// Rows with a key at or above this are still to be read.
pub(crate) next_key: i64,
pub(crate) exhausted: bool,
/// The candidate key ranges from the one-time rtree scan, walked in
/// order. `None` for an unfiltered read or the indexless full scan.
pub(crate) segments: Option<Vec<(i64, i64)>>,
/// Index into [`Self::segments`] of the range being walked.
pub(crate) segment: usize,
/// The caller's `WHERE` parameters, bound first (`?1` to `?N`) on every
/// page. Empty for a reader with no `WHERE` clause.
pub(crate) user_params: Vec<rusqlite::types::Value>,
/// Absolute row index of the geometry selected only for the bbox re-test,
/// set when a projection keeps the geometry out of the schema but a
/// filter still needs it. Never a field, so never appended to a builder.
pub(crate) hidden_geometry: Option<usize>,
/// The spatial filter, set only by `read_arrow_in`. Boxed so an unfiltered
/// reader, which is every reader the threaded path builds, stores one
/// pointer rather than the whole of it.
pub(crate) filter: Option<Box<SpatialFilter>>,
/// The aggregate function, when this reader uses it. `None` falls back to
/// the direct loop.
pub(crate) aggregate: Option<AggregateState>,
}
impl Drop for SequentialBatches<'_> {
fn drop(&mut self) {
if let Some(state) = &self.aggregate {
// Best effort: the reader is going away either way, and a failure
// here would only leave an unused function registered on the
// connection under a name nothing else uses.
drop(
self.conn
.remove_function(state.name.as_str(), state.arg_count),
);
}
}
}
impl SequentialBatches<'_> {
/// Reads up to `limit` rows starting at `key`, ignoring where the reader
/// had got to.
///
/// Used by the parallel path, whose workers each read whole batches at
/// offsets assigned to them rather than walking the layer in sequence.
/// `limit` is what remains of the worker's window: the byte ceiling can
/// cut a batch short, and the rest of that window still has to be read.
pub(crate) fn read_batch_at(&mut self, key: i64, limit: usize) -> Result<Option<RecordBatch>> {
self.next_key = key;
self.exhausted = false;
let full = self.batch_size;
self.batch_size = limit.max(1);
let batch = self.next_batch();
self.batch_size = full;
batch
}
/// Reads one batch, or `None` once the layer is exhausted.
pub(crate) fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
if self.aggregate.is_some() {
return self.next_batch_aggregate();
}
self.next_batch_direct()
}
/// The aggregate path: one function call per row, inside SQLite's own loop.
fn next_batch_aggregate(&mut self) -> Result<Option<RecordBatch>> {
let queried = self.conn.query_row(
&self.aggregate_sql,
rusqlite::params![
self.next_key,
i64::try_from(self.batch_size).unwrap_or(i64::MAX)
],
|row| row.get::<_, i64>(0),
);
// A failed append stops the scan and stores the real reason; the error
// the query returns is only the signal that it stopped.
if let Some(state) = &self.aggregate
&& let Ok(mut slot) = state.failure.lock()
&& let Some(error) = slot.take()
{
self.exhausted = true;
return Err(error);
}
let rows_read = queried?;
let filled = self
.aggregate
.as_ref()
.and_then(|state| state.output.lock().ok().and_then(|mut slot| slot.take()));
let Some(filled) = filled else {
self.exhausted = true;
return Ok(None);
};
let rows_read = usize::try_from(rows_read).unwrap_or(0);
if rows_read == 0 {
self.exhausted = true;
return Ok(None);
}
// The aggregate counts every row it was given, including any it left
// for the next batch, so the appended count is what the builders hold.
let rows_appended = filled.rows;
self.last_batch_rows = rows_appended;
self.advance(filled.last_key, rows_appended, filled.truncated);
let arrays: Vec<ArrayRef> = filled
.builders
.into_iter()
.map(ColumnBuilder::finish)
.collect();
Ok(Some(RecordBatch::try_new(
Arc::clone(&self.schema),
arrays,
)?))
}
/// Record where the next batch starts, and whether there can be one.
fn advance(&mut self, last_key: Option<i64>, rows_read: usize, truncated: bool) {
// A short batch normally means the current range ran out: the whole
// layer for an unsegmented read, the current segment for a segmented
// one. It does not when the byte ceiling cut the batch short: there
// are rows left, and treating this as the end would silently drop
// them.
if rows_read < self.batch_size && !truncated {
// The key still advances first: a parallel worker resumes its
// window from `next_key` even when the page ended short, and a
// segmented read overwrites it with the next segment's start.
if let Some(next) = last_key.and_then(|key| key.checked_add(1)) {
self.next_key = next;
}
self.next_segment();
return;
}
match last_key.and_then(|key| key.checked_add(1)) {
Some(next) => self.next_key = next,
// The key space is exhausted at i64::MAX; there can be no next
// row in this segment, nor a later segment to hold one.
None => self.exhausted = true,
}
}
/// Move to the next candidate segment, or the end of the read.
///
/// For an unsegmented read there is no next range, so this is where a
/// finished scan becomes `exhausted`.
fn next_segment(&mut self) {
let Some(segments) = &self.segments else {
self.exhausted = true;
return;
};
self.segment += 1;
match segments.get(self.segment) {
Some((lo, _)) => self.next_key = *lo,
None => self.exhausted = true,
}
}
/// The direct path: step the rows and fetch each value. Kept as the
/// fallback for a table too wide for the aggregate's argument list.
fn next_batch_direct(&mut self) -> Result<Option<RecordBatch>> {
// Looped rather than recursive: under a selective filter many pages in
// a row can come back with every candidate dropped, and one stack frame
// per empty page would be a stack overflow waiting for a large enough
// layer.
loop {
match self.next_batch_page()? {
Page::Batch(batch) => return Ok(Some(batch)),
Page::Exhausted => return Ok(None),
Page::AllFiltered => {}
}
}
}
/// One page of candidates: a batch, nothing left, or a page whose rows were
/// all filtered out and which the caller should follow with another.
fn next_batch_page(&mut self) -> Result<Page> {
// Pre-size the arrays so a batch does not spend its time growing them.
// Capped rather than taken from `batch_size` directly, so an enormous
// batch size does not reserve enormous buffers for a small table.
let capacity = self.batch_size.min(DEFAULT_BATCH_SIZE);
let mut builders: Vec<ColumnBuilder> = self
.schema
.fields()
.iter()
.enumerate()
.map(|(index, field)| {
ColumnBuilder::new(
field.data_type(),
Some(index) == self.geometry_index,
capacity,
)
})
.collect::<Result<_>>()?;
let mut rows_read = 0usize;
let mut last_key = None;
let mut bytes = 0usize;
let mut truncated = false;
// Candidates the query returned, as against rows that survived the
// filter. Pagination and exhaustion run off this one: a page whose rows
// were all filtered out is not the end of the layer, and treating it as
// one would silently drop everything after it.
let mut candidates = 0usize;
{
let mut stmt = self.conn.prepare_cached(&self.sql)?;
// Binding is positional, so the order here is the placeholder
// numbering: the caller's params take `?1` to `?N`, then the key,
// the limit, and the current segment's upper bound where the read
// is segmented, as the query text numbered them.
let mut params: Vec<rusqlite::types::Value> = self.user_params.clone();
params.push(rusqlite::types::Value::Integer(self.next_key));
params.push(rusqlite::types::Value::Integer(
i64::try_from(self.batch_size).unwrap_or(i64::MAX),
));
if let Some(segments) = &self.segments {
// `next_batch_page` is only reached while a segment remains,
// since exhausting the last one sets `exhausted`; an i64::MAX
// bound on a missing segment would read past the candidates,
// and the exact re-test would still drop every extra row.
let (_, hi) = segments.get(self.segment).copied().unwrap_or((0, i64::MAX));
params.push(rusqlite::types::Value::Integer(hi));
}
let mut rows = stmt.query(rusqlite::params_from_iter(params.iter()))?;
// Where the fields start: at 0 when the key is one of them, at 1
// when it had to be selected separately.
let offset = usize::from(self.key_field.is_none());
while let Some(row) = rows.next()? {
let geometry_bytes = match self.geometry_index {
Some(index) => match row.get_ref(index + offset)? {
ValueRef::Blob(blob) => blob.len(),
_ => 0,
},
None => 0,
};
// Same ceiling as the aggregate path, for the same reason: the
// geometry column's i32 offsets cannot address more.
if rows_read > 0 && bytes.saturating_add(geometry_bytes) > self.max_batch_bytes {
truncated = true;
break;
}
// Counted, and its key remembered, before the filter: a
// candidate that is filtered out has still been read, and the
// next page must start after it.
candidates += 1;
last_key = Some(row.get::<_, i64>(self.key_field.unwrap_or(0))?);
// The exact re-test, against the true f64 envelope. Done before
// any value is converted, so a row outside the box costs
// nothing beyond reading its geometry, and conversion errors
// surface only for rows that are actually returned. Same rule
// and same helper as the row path. The geometry is a schema
// field or, on a projected read, the hidden trailing column
// selected for exactly this test.
if let Some(filter) = &self.filter {
let geometry_index = self
.geometry_index
.map(|index| index + offset)
.or(self.hidden_geometry);
if !crate::layer::row_in_box(row, geometry_index, &filter.bbox)? {
continue;
}
}
for (index, builder) in builders.iter_mut().enumerate() {
builder.append(
&self.names,
index,
row.get_ref(index + offset)?,
self.datetime,
)?;
}
rows_read += 1;
bytes = bytes.saturating_add(geometry_bytes);
}
}
if candidates == 0 {
// An empty page ends the current segment, not necessarily the
// read: a segmented read moves to its next range and tries again.
self.next_segment();
return Ok(if self.exhausted {
Page::Exhausted
} else {
Page::AllFiltered
});
}
if rows_read == 0 {
// Every candidate on this page was filtered out. Advance past them
// and let the caller try the next page rather than reporting the
// layer exhausted.
self.advance(last_key, candidates, truncated);
return Ok(if self.exhausted {
Page::Exhausted
} else {
Page::AllFiltered
});
}
self.last_batch_rows = rows_read;
self.advance(last_key, candidates, truncated);
let arrays: Vec<ArrayRef> = builders.into_iter().map(ColumnBuilder::finish).collect();
Ok(Page::Batch(RecordBatch::try_new(
Arc::clone(&self.schema),
arrays,
)?))
}
}
/// The exact re-filter a `read_arrow_in` applies.
///
/// The index stores `f32` envelopes and is queried with outward-widened
/// bounds, and a candidate segment additionally folds small key gaps, so the
/// fetched rows are a superset twice over: every one has to be re-tested
/// against its true `f64` envelope, or a filtered columnar read would return
/// rows `features_in` does not.
pub(crate) struct SpatialFilter {
pub(crate) bbox: BoundingBox,
}
/// What one page of candidates produced.
enum Page {
/// A batch with at least one row in it.
Batch(RecordBatch),
/// Nothing left to read.
Exhausted,
/// Candidates were read and every one was filtered out; there may be more.
AllFiltered,
}
impl Iterator for SequentialBatches<'_> {
type Item = std::result::Result<RecordBatch, ArrowError>;
fn next(&mut self) -> Option<Self::Item> {
if self.exhausted {
return None;
}
match self.next_batch() {
Ok(Some(batch)) => Some(Ok(batch)),
Ok(None) => None,
Err(error) => {
// A failed batch ends the stream: retrying would re-run the
// same query against the same state.
self.exhausted = true;
Some(Err(ArrowError::ExternalError(Box::new(error))))
}
}
}
}