Skip to main content

dre_protocol/
plugin.rs

1//! The plugin SDK: implement one of [`Source`], [`Format`] or [`Destination`] and call the
2//! matching `serve_*` function from `main`. The SDK owns stdin/stdout, the handshake, framing,
3//! error replies and panics; plugins log with `eprintln!`.
4//!
5//! Formats and destinations declare their options (`options()`) and add any rule a declaration
6//! can't express (`validate()`). The SDK checks a config block against both on `validate` and
7//! before every `write` and `deliver`, so plugin code only sees options that passed.
8
9use std::io::{BufReader, BufWriter, Read, Stdout};
10use std::path::{Path, PathBuf};
11
12use arrow::array::RecordBatch;
13use arrow::datatypes::SchemaRef;
14use serde_json::{Map, Value};
15
16use crate::frame::{self, Frame, FrameError};
17use crate::msg::{ConnectionField, Request, Response, ResultSetMeta};
18use crate::options::{self, OptionField};
19use crate::{CAP_VALIDATE, Kind, MAX_VERSION, MIN_VERSION, PluginId};
20
21pub type Error = Box<dyn std::error::Error + Send + Sync>;
22pub type Result<T> = std::result::Result<T, Error>;
23
24/// Identity reported in the handshake.
25#[derive(Debug, Clone)]
26pub struct About {
27    pub name: &'static str,
28    pub version: &'static str,
29    pub capabilities: &'static [&'static str],
30}
31
32impl About {
33    /// `version` "0.0.0" (the crate has no version yet) is reported as "unreleased".
34    pub fn new(name: &'static str, version: &'static str) -> About {
35        let version = if version == "0.0.0" { "unreleased" } else { version };
36        About {
37            name,
38            version,
39            capabilities: &[],
40        }
41    }
42
43    pub fn capabilities(mut self, caps: &'static [&'static str]) -> About {
44        self.capabilities = caps;
45        self
46    }
47}
48
49/// Where a source pushes the outcome of `execute`: either `no_result`, or `begin` followed by
50/// any number of `batch` calls.
51pub trait ResultSink {
52    fn no_result(&mut self, rows_affected: Option<u64>) -> Result<()>;
53    fn begin(&mut self, schema: SchemaRef) -> Result<()>;
54    /// Returns `false` once the row limit is reached: stop producing batches.
55    fn batch(&mut self, batch: RecordBatch) -> Result<bool>;
56}
57
58pub trait Source {
59    fn connection_fields(&self) -> Vec<ConnectionField> {
60        Vec::new()
61    }
62    fn open(&mut self, connection: &Map<String, Value>, read_only: bool) -> Result<()>;
63    /// Run one statement. `row_limit` is a hint; the SDK enforces it either way.
64    fn execute(&mut self, sql: &str, row_limit: Option<u64>, out: &mut dyn ResultSink) -> Result<()>;
65    /// Verify without executing. Only called when the plugin advertises `check`.
66    fn check(&mut self, _sql: &str) -> Result<()> {
67        Err("this source can't check statements".into())
68    }
69    /// Load `data` into a temporary table on the session, named after `name`. Only called when
70    /// the plugin advertises `load`.
71    fn load(&mut self, _name: &str, _data: &mut ResultSet<'_>) -> Result<Loaded> {
72        Err("this source can't load rows".into())
73    }
74    /// End the session cleanly (called on `close` and at end of input, before exiting).
75    fn close(&mut self) {}
76}
77
78/// The outcome of `Source::load`.
79pub struct Loaded {
80    /// How SQL refers to the loaded rows, e.g. the temp table's name.
81    pub relation: String,
82    pub rows: u64,
83    /// Shown to the user, e.g. when the database has no bulk path.
84    pub warning: Option<String>,
85}
86
87/// One incoming result set, streamed from core.
88pub struct ResultSet<'a> {
89    pub meta: ResultSetMeta,
90    pub schema: SchemaRef,
91    first: Option<Vec<RecordBatch>>,
92    input: &'a mut Input,
93    done: bool,
94}
95
96impl ResultSet<'_> {
97    /// The next batch, or `None` at the end of this result set.
98    pub fn next_batch(&mut self) -> Result<Option<RecordBatch>> {
99        loop {
100            if let Some(first) = self.first.as_mut() {
101                if !first.is_empty() {
102                    return Ok(Some(first.remove(0)));
103                }
104                self.first = None;
105            }
106            if self.done {
107                return Ok(None);
108            }
109            match self.input.read()? {
110                Frame::Arrow(ipc) => {
111                    let (_, batches) = frame::decode_batches(&ipc)?;
112                    self.first = Some(batches);
113                }
114                Frame::Json(v) => match serde_json::from_value::<Request>(v)? {
115                    Request::ResultSetEnd {} => {
116                        self.done = true;
117                        return Ok(None);
118                    }
119                    other => return Err(format!("expected result data, got {other:?}").into()),
120                },
121            }
122        }
123    }
124
125    fn drain(&mut self) -> Result<()> {
126        while self.next_batch()?.is_some() {}
127        Ok(())
128    }
129}
130
131/// A format `write` request.
132pub struct WriteRequest {
133    pub path: String,
134    pub format: String,
135    pub options: Map<String, Value>,
136    pub result_sets: Vec<ResultSetMeta>,
137    pub template: Option<Value>,
138}
139
140/// Hands a format plugin its result sets one at a time, in order.
141pub struct ResultSets<'a> {
142    metas: std::vec::IntoIter<ResultSetMeta>,
143    input: &'a mut Input,
144    warnings: Vec<String>,
145}
146
147impl ResultSets<'_> {
148    /// Tell the person something about the output, shown as a warning by core.
149    pub fn warn(&mut self, message: impl Into<String>) {
150        self.warnings.push(message.into());
151    }
152
153    /// The next result set, or `None` after the last. Each must be read (or is drained) before
154    /// the next is requested.
155    pub fn next_set(&mut self) -> Result<Option<ResultSet<'_>>> {
156        let Some(meta) = self.metas.next() else {
157            return Ok(None);
158        };
159        let ipc = match self.input.read()? {
160            Frame::Arrow(ipc) => ipc,
161            Frame::Json(v) => {
162                return Err(format!(
163                    "expected an Arrow frame starting result set `{}`, got {v}",
164                    meta.name
165                )
166                .into());
167            }
168        };
169        let (schema, batches) = frame::decode_batches(&ipc)?;
170        Ok(Some(ResultSet {
171            meta,
172            schema,
173            first: Some(batches),
174            input: &mut *self.input,
175            done: false,
176        }))
177    }
178}
179
180pub trait Format {
181    /// The options this format takes: every key of a report's `output:` block except `format`,
182    /// `destination`, `template` and `extension`, which core owns.
183    fn options(&self) -> Vec<OptionField> {
184        Vec::new()
185    }
186    /// Problems the declared [`Format::options`] can't catch, each a sentence naming the key.
187    /// Only called on options whose declared types already passed.
188    fn validate(&self, _options: &Map<String, Value>) -> Vec<String> {
189        Vec::new()
190    }
191    /// Write every result set to `req.path` (and siblings, if the format needs several files);
192    /// return the files written.
193    fn write(&mut self, req: &WriteRequest, sets: &mut ResultSets<'_>) -> Result<Vec<String>>;
194}
195
196/// A destination `deliver` request.
197pub struct Delivery {
198    /// One file, or every file of an output when the plugin advertises `multi_file`.
199    pub files: Vec<DeliveryFile>,
200    pub connection: Map<String, Value>,
201    /// The destination entry's plugin options, rendered by core (empty when none).
202    pub options: Map<String, Value>,
203}
204
205/// One file to deliver: the local copy in `target/run/` and its rendered remote path, if any.
206pub struct DeliveryFile {
207    pub local: PathBuf,
208    pub remote: Option<String>,
209}
210
211pub trait Destination {
212    fn connection_fields(&self) -> Vec<ConnectionField> {
213        Vec::new()
214    }
215    /// The options a report's destination entry takes: every key but `profile` and `path`.
216    /// String values may still hold Jinja when checked (see [`options::is_template`]); core
217    /// renders it before `deliver`.
218    fn options(&self) -> Vec<OptionField> {
219        Vec::new()
220    }
221    /// Problems the declared [`Destination::options`] can't catch, each a sentence naming the key.
222    /// Only called on options whose declared types already passed.
223    fn validate(&self, _options: &Map<String, Value>) -> Vec<String> {
224        Vec::new()
225    }
226    /// Deliver `local` to `remote` (rendered by core); return where it landed. Enough for a
227    /// destination that takes no options; others implement [`Destination::deliver_files`].
228    fn deliver(
229        &mut self,
230        _local: &Path,
231        _remote: Option<&str>,
232        _connection: &Map<String, Value>,
233    ) -> Result<String> {
234        Err("this destination doesn't implement `deliver`".into())
235    }
236    /// The whole request, options included (already checked). The default hands a single file
237    /// to [`Destination::deliver`]; several files arrive only with `multi_file` advertised.
238    fn deliver_files(&mut self, d: &Delivery) -> Result<String> {
239        match d.files.as_slice() {
240            [f] => self.deliver(&f.local, f.remote.as_deref(), &d.connection),
241            _ => Err("this destination takes one file per delivery".into()),
242        }
243    }
244}
245
246pub struct Input {
247    r: BufReader<Box<dyn Read>>,
248}
249
250impl Input {
251    fn read(&mut self) -> Result<Frame> {
252        frame::read_frame(&mut self.r).map_err(|e| -> Error {
253            match e {
254                FrameError::Eof => "core closed the connection".into(),
255                e => e.to_string().into(),
256            }
257        })
258    }
259}
260
261struct Output {
262    w: BufWriter<Stdout>,
263}
264
265impl Output {
266    fn send(&mut self, r: &Response) {
267        if frame::write_json(&mut self.w, r).is_err() {
268            // Core has gone; nothing left to talk to.
269            std::process::exit(1);
270        }
271    }
272
273    fn batch(&mut self, b: &RecordBatch) -> Result<()> {
274        let ipc = frame::encode_batch(b)?;
275        frame::write_arrow(&mut self.w, &ipc).map_err(|_| -> Error { "core closed the connection".into() })
276    }
277}
278
279enum Handler<'a> {
280    Source(&'a mut dyn Source),
281    Format(&'a mut dyn Format),
282    Destination(&'a mut dyn Destination),
283}
284
285impl Handler<'_> {
286    fn kind(&self) -> Kind {
287        match self {
288            Handler::Source(_) => Kind::Source,
289            Handler::Format(_) => Kind::Format,
290            Handler::Destination(_) => Kind::Destination,
291        }
292    }
293
294    fn option_fields(&self) -> Vec<OptionField> {
295        match self {
296            Handler::Source(_) => Vec::new(),
297            Handler::Format(f) => f.options(),
298            Handler::Destination(d) => d.options(),
299        }
300    }
301
302    /// Every problem with a config block: the declared checks, then the plugin's own once the
303    /// declared ones pass.
304    fn check(&self, name: &str, o: &Map<String, Value>) -> Vec<String> {
305        let errs = options::check(self.kind(), name, &self.option_fields(), o);
306        if !errs.is_empty() {
307            return errs;
308        }
309        match self {
310            Handler::Source(_) => Vec::new(),
311            Handler::Format(f) => f.validate(o),
312            Handler::Destination(d) => d.validate(o),
313        }
314    }
315
316    /// The config block's problems as one error, for `write` and `deliver`.
317    fn checked(&self, name: &str, o: &Map<String, Value>) -> Result<()> {
318        let errs = self.check(name, o);
319        if errs.is_empty() {
320            Ok(())
321        } else {
322            Err(errs.join("; ").into())
323        }
324    }
325}
326
327pub fn serve_source(about: About, s: impl Source + 'static) -> ! {
328    serve_package(vec![Plugin::Source(about, Box::new(s))])
329}
330
331pub fn serve_format(about: About, f: impl Format + 'static) -> ! {
332    serve_package(vec![Plugin::Format(about, Box::new(f))])
333}
334
335pub fn serve_destination(about: About, d: impl Destination + 'static) -> ! {
336    serve_package(vec![Plugin::Destination(about, Box::new(d))])
337}
338
339/// One of the plugins a package's executable serves (see [`serve_package`]).
340pub enum Plugin {
341    Source(About, Box<dyn Source>),
342    Format(About, Box<dyn Format>),
343    Destination(About, Box<dyn Destination>),
344}
345
346impl Plugin {
347    fn id(&self) -> PluginId {
348        let (kind, about) = match self {
349            Plugin::Source(a, _) => (Kind::Source, a),
350            Plugin::Format(a, _) => (Kind::Format, a),
351            Plugin::Destination(a, _) => (Kind::Destination, a),
352        };
353        PluginId::new(kind, about.name)
354    }
355
356    fn handler(&mut self) -> (&About, Handler<'_>) {
357        match self {
358            Plugin::Source(a, s) => (a, Handler::Source(s.as_mut())),
359            Plugin::Format(a, f) => (a, Handler::Format(f.as_mut())),
360            Plugin::Destination(a, d) => (a, Handler::Destination(d.as_mut())),
361        }
362    }
363}
364
365/// Serve every plugin of a package from one executable. Core's `hello` names the plugin it
366/// wants; without one, the first is served.
367pub fn serve_package(mut plugins: Vec<Plugin>) -> ! {
368    assert!(!plugins.is_empty(), "a package serves at least one plugin");
369    let mut input = Input {
370        r: BufReader::new(Box::new(std::io::stdin())),
371    };
372    let mut out = Output {
373        w: BufWriter::new(std::io::stdout()),
374    };
375    let provides: Vec<PluginId> = plugins.iter().map(Plugin::id).collect();
376    let chosen = loop {
377        let req = match next_request(&mut input, &mut out) {
378            Some(r) => r,
379            None => continue,
380        };
381        let Request::Hello {
382            min_version,
383            max_version,
384            plugin,
385            ..
386        } = req
387        else {
388            out.send(&Response::Error {
389                message: "the first request must be `hello`".into(),
390            });
391            continue;
392        };
393        let Some(chosen) = negotiate((min_version, max_version), (MIN_VERSION, MAX_VERSION)) else {
394            out.send(&Response::VersionMismatch {
395                min_version: MIN_VERSION,
396                max_version: MAX_VERSION,
397            });
398            std::process::exit(1);
399        };
400        let i = match &plugin {
401            None => 0,
402            Some(want) => match provides.iter().position(|p| p == want) {
403                Some(i) => i,
404                None => {
405                    let list: Vec<String> = provides.iter().map(|p| p.to_string()).collect();
406                    out.send(&Response::Error {
407                        message: format!("this executable provides {}, not {want}", list.join(", ")),
408                    });
409                    std::process::exit(1);
410                }
411            },
412        };
413        let (about, h) = plugins[i].handler();
414        out.send(&Response::Hello {
415            protocol_version: chosen,
416            kind: h.kind(),
417            name: about.name.to_string(),
418            version: about.version.to_string(),
419            capabilities: about
420                .capabilities
421                .iter()
422                .chain(std::iter::once(&CAP_VALIDATE))
423                .map(|c| c.to_string())
424                .collect(),
425            provides: if provides.len() > 1 {
426                provides.clone()
427            } else {
428                Vec::new()
429            },
430        });
431        break i;
432    };
433    let (about, h) = plugins[chosen].handler();
434    serve(about.name, h, input, out)
435}
436
437/// The next request, or `None` after answering one that can't be read. Exits at end of input.
438fn next_request(input: &mut Input, out: &mut Output) -> Option<Request> {
439    let frame = match frame::read_frame(&mut input.r) {
440        Ok(f) => f,
441        Err(FrameError::Eof) => std::process::exit(0),
442        Err(e) => {
443            eprintln!("{e}");
444            out.send(&Response::Error {
445                message: e.to_string(),
446            });
447            std::process::exit(2);
448        }
449    };
450    match frame {
451        Frame::Json(v) => match serde_json::from_value::<Request>(v.clone()) {
452            Ok(r) => Some(r),
453            Err(_) => {
454                let t = v.get("type").and_then(Value::as_str).unwrap_or("?").to_string();
455                out.send(&Response::Error {
456                    message: format!("unsupported request `{t}`"),
457                });
458                None
459            }
460        },
461        Frame::Arrow(_) => {
462            out.send(&Response::Error {
463                message: "unexpected Arrow frame".into(),
464            });
465            None
466        }
467    }
468}
469
470/// Serve requests after the handshake.
471fn serve(name: &str, mut h: Handler<'_>, mut input: Input, mut out: Output) -> ! {
472    loop {
473        let frame = match frame::read_frame(&mut input.r) {
474            Ok(f) => f,
475            Err(FrameError::Eof) => {
476                if let Handler::Source(src) = &mut h {
477                    src.close();
478                }
479                std::process::exit(0)
480            }
481            Err(e) => {
482                eprintln!("{e}");
483                out.send(&Response::Error {
484                    message: e.to_string(),
485                });
486                std::process::exit(2);
487            }
488        };
489        let req = match frame {
490            Frame::Json(v) => match serde_json::from_value::<Request>(v.clone()) {
491                Ok(r) => r,
492                Err(_) => {
493                    let t = v.get("type").and_then(Value::as_str).unwrap_or("?").to_string();
494                    out.send(&Response::Error {
495                        message: format!("unsupported request `{t}`"),
496                    });
497                    continue;
498                }
499            },
500            Frame::Arrow(_) => {
501                out.send(&Response::Error {
502                    message: "unexpected Arrow frame".into(),
503                });
504                continue;
505            }
506        };
507        if let Request::Hello { .. } = req {
508            out.send(&Response::Error {
509                message: "`hello` was already answered".into(),
510            });
511            continue;
512        }
513        if let Request::Close {} = req {
514            if let Handler::Source(src) = &mut h {
515                src.close();
516            }
517            out.send(&Response::Ok {});
518            std::process::exit(0);
519        }
520        let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
521            handle(&mut h, name, req, &mut input, &mut out)
522        }));
523        match result {
524            Ok(Ok(())) => {}
525            Ok(Err(e)) => out.send(&Response::Error {
526                message: e.to_string(),
527            }),
528            Err(p) => {
529                let msg = p
530                    .downcast_ref::<String>()
531                    .cloned()
532                    .or_else(|| p.downcast_ref::<&str>().map(|s| s.to_string()))
533                    .unwrap_or_else(|| "unknown panic".into());
534                out.send(&Response::Error {
535                    message: format!("plugin panicked: {msg}"),
536                });
537                std::process::exit(101);
538            }
539        }
540    }
541}
542
543fn handle(h: &mut Handler<'_>, name: &str, req: Request, input: &mut Input, out: &mut Output) -> Result<()> {
544    if let Request::Describe {} = req {
545        let connection_fields = match h {
546            Handler::Source(s) => s.connection_fields(),
547            Handler::Destination(d) => d.connection_fields(),
548            Handler::Format(_) => Vec::new(),
549        };
550        out.send(&Response::Describe {
551            connection_fields,
552            option_fields: h.option_fields(),
553        });
554        return Ok(());
555    }
556    if let Request::Validate { options } = req {
557        out.send(&Response::Validated {
558            errors: h.check(name, &options),
559        });
560        return Ok(());
561    }
562    let checked = match &req {
563        Request::Write { options, .. } | Request::Deliver { options, .. } => h.checked(name, options),
564        _ => Ok(()),
565    };
566    match (h, req) {
567        (
568            Handler::Source(s),
569            Request::Open {
570                connection,
571                read_only,
572            },
573        ) => {
574            s.open(&connection, read_only)?;
575            out.send(&Response::Ok {});
576        }
577        (Handler::Source(s), Request::Load { name }) => {
578            let ipc = match input.read()? {
579                Frame::Arrow(ipc) => ipc,
580                Frame::Json(v) => return Err(format!("expected the rows to load, got {v}").into()),
581            };
582            let (schema, batches) = frame::decode_batches(&ipc)?;
583            let mut data = ResultSet {
584                meta: ResultSetMeta {
585                    name: name.clone(),
586                    query: name.clone(),
587                    result_index: 1,
588                    anchor: None,
589                    header: None,
590                    columns: Default::default(),
591                },
592                schema,
593                first: Some(batches),
594                input,
595                done: false,
596            };
597            let r = s.load(&name, &mut data);
598            data.drain()?;
599            let loaded = r?;
600            out.send(&Response::Loaded {
601                relation: loaded.relation,
602                rows: loaded.rows,
603                warning: loaded.warning,
604            });
605        }
606        (Handler::Source(s), Request::Check { sql }) => {
607            s.check(&sql)?;
608            out.send(&Response::Ok {});
609        }
610        (Handler::Source(s), Request::Execute { sql, row_limit }) => {
611            let mut sink = FrameSink {
612                out,
613                row_limit,
614                rows: 0,
615                state: SinkState::Idle,
616                schema: None,
617                sent_any: false,
618            };
619            let r = s.execute(&sql, row_limit, &mut sink);
620            match (r, sink.state) {
621                (Err(e), _) => return Err(e),
622                (Ok(()), SinkState::Idle) => {
623                    return Err("the source produced neither a result nor `no_result`".into());
624                }
625                (Ok(()), SinkState::NoResult) => {}
626                (Ok(()), SinkState::Result) => {
627                    if !sink.sent_any {
628                        let schema = sink.schema.clone().unwrap();
629                        sink.out.batch(&RecordBatch::new_empty(schema))?;
630                    }
631                    let rows = sink.rows;
632                    sink.out.send(&Response::ResultEnd { rows });
633                }
634            }
635        }
636        (
637            Handler::Format(f),
638            Request::Write {
639                path,
640                format,
641                options,
642                result_sets,
643                template,
644            },
645        ) => {
646            let req = WriteRequest {
647                path,
648                format,
649                options,
650                result_sets: result_sets.clone(),
651                template,
652            };
653            let mut sets = ResultSets {
654                metas: result_sets.into_iter(),
655                input,
656                warnings: Vec::new(),
657            };
658            let written = checked.and_then(|()| f.write(&req, &mut sets));
659            // A failed write is reported at once, so core can stop streaming; this is the
660            // request's one reply.
661            if let Err(e) = &written {
662                out.send(&Response::Error {
663                    message: e.to_string(),
664                });
665            }
666            // Consume whatever the format didn't read (after an error, possibly the rest of a
667            // result set), so the stream stays in sync.
668            loop {
669                match sets.input.read()? {
670                    Frame::Json(v) if v.get("type").and_then(Value::as_str) == Some("finish") => break,
671                    Frame::Json(v) if v.get("type").and_then(Value::as_str) == Some("result_set_end") => {}
672                    Frame::Arrow(_) => {}
673                    other => return Err(format!("expected `finish`, got {other:?}").into()),
674                }
675            }
676            if let Ok(files) = written {
677                out.send(&Response::Written {
678                    files,
679                    warnings: sets.warnings,
680                });
681            }
682        }
683        (
684            Handler::Destination(d),
685            Request::Deliver {
686                local_path,
687                remote_path,
688                files,
689                connection,
690                options,
691            },
692        ) => {
693            checked?;
694            let files = match (local_path, files.is_empty()) {
695                (Some(local), true) => vec![DeliveryFile {
696                    local: PathBuf::from(local),
697                    remote: remote_path,
698                }],
699                (None, false) => files
700                    .into_iter()
701                    .map(|f| DeliveryFile {
702                        local: PathBuf::from(f.local_path),
703                        remote: f.remote_path,
704                    })
705                    .collect(),
706                _ => return Err("`deliver` needs exactly one of `local_path` or `files`".into()),
707            };
708            let location = d.deliver_files(&Delivery {
709                files,
710                connection,
711                options,
712            })?;
713            out.send(&Response::Delivered { location });
714        }
715        (h, req) => {
716            let t = serde_json::to_value(&req)
717                .ok()
718                .and_then(|v| v.get("type").cloned())
719                .unwrap_or_default();
720            return Err(format!("a {} plugin doesn't handle {t} requests", h.kind()).into());
721        }
722    }
723    Ok(())
724}
725
726#[derive(PartialEq)]
727enum SinkState {
728    Idle,
729    NoResult,
730    Result,
731}
732
733struct FrameSink<'a> {
734    out: &'a mut Output,
735    row_limit: Option<u64>,
736    rows: u64,
737    state: SinkState,
738    schema: Option<SchemaRef>,
739    sent_any: bool,
740}
741
742impl ResultSink for FrameSink<'_> {
743    fn no_result(&mut self, rows_affected: Option<u64>) -> Result<()> {
744        if self.state != SinkState::Idle {
745            return Err("`no_result` after the result started".into());
746        }
747        self.state = SinkState::NoResult;
748        self.out.send(&Response::NoResult { rows_affected });
749        Ok(())
750    }
751
752    fn begin(&mut self, schema: SchemaRef) -> Result<()> {
753        if self.state != SinkState::Idle {
754            return Err("a statement can only produce one result".into());
755        }
756        self.state = SinkState::Result;
757        self.out.send(&Response::Result {
758            columns: schema.fields().iter().map(|f| f.name().clone()).collect(),
759        });
760        self.schema = Some(schema);
761        Ok(())
762    }
763
764    fn batch(&mut self, b: RecordBatch) -> Result<bool> {
765        if self.state != SinkState::Result {
766            return Err("`batch` before `begin`".into());
767        }
768        if self.row_limit.is_some_and(|l| self.rows >= l) {
769            return Ok(false);
770        }
771        let b = match self.row_limit {
772            Some(l) if self.rows + b.num_rows() as u64 > l => b.slice(0, (l - self.rows) as usize),
773            _ => b,
774        };
775        if b.num_rows() > 0 || !self.sent_any {
776            self.rows += b.num_rows() as u64;
777            self.out.batch(&b)?;
778            self.sent_any = true;
779        }
780        Ok(self.row_limit.is_none_or(|l| self.rows < l))
781    }
782}
783
784/// The highest version in both ranges.
785fn negotiate(core: (u32, u32), ours: (u32, u32)) -> Option<u32> {
786    let (lo, hi) = (core.0.max(ours.0), core.1.min(ours.1));
787    (lo <= hi).then_some(hi)
788}
789
790/// Read a string field from a connection map.
791pub fn conn_str<'a>(c: &'a Map<String, Value>, key: &str) -> Option<&'a str> {
792    c.get(key).and_then(Value::as_str).filter(|s| !s.is_empty())
793}
794
795/// Read a required string field from a connection map.
796pub fn conn_required<'a>(c: &'a Map<String, Value>, key: &str) -> Result<&'a str> {
797    conn_str(c, key).ok_or_else(|| format!("the profile output needs a `{key}` field").into())
798}
799
800/// Read a flag that may be written as a boolean or a string.
801pub fn conn_bool(c: &Map<String, Value>, key: &str) -> Option<bool> {
802    match c.get(key)? {
803        Value::Bool(b) => Some(*b),
804        Value::String(s) => Some(matches!(s.as_str(), "true" | "yes" | "1")),
805        _ => None,
806    }
807}