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
//! Shared internals of the REST export representations.
//!
//! The row source every export opens (`export_rows`, #958), NDJSON batch serialisation and
//! error formatting, and — since #1274 — the header-column rule that CSV and XLSX both
//! apply (`export_columns`, `determine_columns`). The column rule lives here because both
//! writers held byte-identical copies of it, which is one place per writer for it to drift.
//!
//! The export-total bound used to be recovered here too, by `requested_total_limit`
//! re-reading the raw `?limit=` from the query pairs. It is now
//! `ExtractedParams::requested_pagination`, which the extractor records alongside the plan
//! it resolves — a second reader of a query parameter is a second answer to it (#1273).
use Arc;
use Bytes;
use ;
use StreamExt as _;
use crateRestError;
/// Open the row source behind every export representation (#958).
///
/// One statement over one portal, delivering rows as PostgreSQL produces them.
/// All three exports — NDJSON, CSV, XLSX — used to walk the same result set with
/// `LIMIT n OFFSET k` re-executions, which is `O(k)` per batch and gives each
/// batch its own snapshot: a concurrent insert or delete shifts rows across a
/// batch boundary, so an export silently emits one row twice and another not at
/// all. Nothing about a batch size fixes that; a single statement does.
///
/// # The two pagination arguments
///
/// `limit` is **removed** from the query's arguments rather than pushed into the
/// SQL. A client's `?limit=` bounds the export *total*, and `max_page_size` (#421)
/// bounds one page of an interactive read — pushing an export total through a page
/// guard would refuse every export larger than a page. The cap is applied to the
/// stream instead, which is the same bound with none of the confusion. `offset` is
/// left alone: it costs `O(offset)` once, not once per batch.
///
/// # Errors
///
/// Returns `RestError` when the read is refused before its first row —
/// authorization, a gated field, a missing principal for an RLS or tenant-scoped
/// query. A failure after that point cannot be an HTTP status any more (the
/// response has begun) and arrives as an `Err` item in the stream.
pub async
/// The column list an export writes, taken from the **projection**.
///
/// This is the one place either export writer learns its header, and the answer is the
/// field list the rows are actually projected by — `QueryMatch::fields`, which
/// `resolve_get_query` builds from `params.field_selection` (expanding `All` to the
/// type's declared fields, per #886).
///
/// # Why not re-parse `?select=`
///
/// Both writers used to parse the raw `?select=` string a second time, and the two
/// parses disagreed (#1274). `RestParamExtractor::extract` classifies with an
/// assignment, so a repeated `?select=` resolves **last**-wins into the projection;
/// the header parser searched with `.find`, so it resolved **first**-wins. A request
/// naming two different fields therefore got a header for one and rows for the other,
/// and `write_csv_payload` renders a key the row lacks as an empty cell — a named
/// column, empty in every row, under a `200`.
///
/// A second parse of the same input is a second source of truth whichever way it
/// resolves; the projection is the only list the rows can be guaranteed to fill. It is
/// also what makes the paren-awareness the old parser carried unnecessary: since #1268
/// an export *refuses* a `?select=` naming an embed or a count, so no header can be
/// asked for one.
///
/// `None` means "no column list is known" — the projection is empty, which
/// `resolve_get_query` produces only when the return type is not in the schema. The
/// writers fall back to the first row's keys there, as they did before.
pub
/// Decide the column ordering an export writes.
///
/// Preference:
/// 1. The projection, via [`export_columns`].
/// 2. The first row's keys, sorted alphabetically.
///
/// The fallback sorts explicitly rather than leaning on `serde_json::Map` iteration
/// order: that order is alphabetical only for the default (`BTreeMap`) build and becomes
/// insertion order when any dependency enables the `preserve_order` feature (e.g. under
/// `--all-features`), which would silently change export column order. Sorting here keeps
/// the header deterministic regardless of `serde_json`'s feature resolution.
///
/// Shared by the CSV and XLSX writers, which held byte-identical copies of this and of
/// the `?select=` parser it used to take its first branch from (#1274). Two copies of a
/// header rule are two places for it to drift with no compiler signal.
pub
/// Serialise one group of streamed rows as NDJSON bytes.
///
/// Returns the bytes and whether the group ended in a failure, which is the
/// export's terminal condition: rows serialised before the failure still go out,
/// followed by the error line. A truncated export that says why is recoverable;
/// one that simply stops is indistinguishable from a complete one.
pub
/// Serialize an error as an NDJSON error line.
pub