parquet/column/page_store.rs
1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements. See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership. The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License. You may obtain a copy of the License at
8//
9// http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied. See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18//! Pluggable storage for completed, serialized page blobs.
19//!
20//! While a row group is being written the [`ArrowWriter`] must buffer every
21//! column's encoded pages, because Parquet requires each column chunk to be
22//! contiguous in the file while record batches arrive with all columns interleaved.
23//! By default that buffer lives on the heap, so the writer's peak memory grows
24//! with the row group size. A [`PageStore`] lets the buffer live somewhere else
25//! — a local temp file, object storage, etc. — bounding peak write memory
26//! independently of the row group size.
27//!
28//! [`ArrowWriter`]: crate::arrow::arrow_writer::ArrowWriter
29
30use std::fmt::Debug;
31
32use bytes::Bytes;
33
34use crate::errors::{ParquetError, Result};
35use crate::schema::types::ColumnDescriptor;
36
37/// An opaque, store-allocated handle to a blob held by a [`PageStore`].
38///
39/// Handles are allocated by the store — densely and sequentially — and are only
40/// meaningful to the store that produced them. The caller treats them as opaque
41/// tokens.
42#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43pub struct PageKey(u64);
44
45impl PageKey {
46 /// Create a handle wrapping `raw`.
47 ///
48 /// A [`PageStore`] implementation calls this to mint the handle it returns
49 /// from [`put`](PageStore::put). The value is opaque to the caller, so a
50 /// store is free to use a dense counter, a packed locator, or anything else
51 /// it can later resolve in [`take`](PageStore::take).
52 pub const fn new(raw: u64) -> Self {
53 Self(raw)
54 }
55
56 /// The raw value passed to [`new`](Self::new).
57 pub const fn get(self) -> u64 {
58 self.0
59 }
60}
61
62/// A pluggable store for completed, serialized page blobs.
63///
64/// The store is intentionally "dumb": it only maps an opaque [`PageKey`] to a
65/// blob of bytes. It knows nothing about pages, dictionaries, ordering, or
66/// offsets. The caller keeps the handles it gets back from [`put`](Self::put)
67/// and decides what they mean.
68///
69/// Each store instance is owned by a single column writer and mutated by one
70/// thread at a time (both methods take `&mut self`), so it needs no internal
71/// synchronization — hence only `Send`, not `Sync`.
72///
73/// The default ([`InMemoryPageStore`]) keeps blobs in memory on the heap.
74///
75/// For an example of configuring the Parquet writer to use an alternate
76/// `PageStore` see the [`ArrowWriterOptions::with_page_store_factory`] API.
77///
78/// [`ArrowWriterOptions::with_page_store_factory`]: crate::arrow::arrow_writer::ArrowWriterOptions::with_page_store_factory
79pub trait PageStore: Send {
80 /// Store `value`, returning a handle that can later be passed to
81 /// [`take`](Self::take).
82 fn put(&mut self, value: Bytes) -> Result<PageKey>;
83
84 /// Take back the blob previously stored under `key`.
85 ///
86 /// The caller takes ownership of the returned bytes and will **not** request
87 /// `key` again, so the store may release any resources backing it — eagerly
88 /// here, or when the store is dropped.
89 fn take(&mut self, key: PageKey) -> Result<Bytes>;
90
91 /// The number of bytes this store currently holds **in memory** (resident
92 /// on the heap), used to report the writer's memory footprint.
93 ///
94 /// The default is `0`, which is exactly right for a backend that moves
95 /// every blob off-heap (a temp file, object storage): the bytes it has been
96 /// handed no longer occupy heap. The in-memory backend overrides this to
97 /// report its resident blobs. A backend that keeps a partial in-memory
98 /// buffer should report that buffer's size.
99 fn memory_size(&self) -> usize {
100 0
101 }
102}
103
104/// Context for a single [`PageStoreFactory::create`] call.
105///
106/// Describes the leaf column chunk the store will buffer. It is held by
107/// reference for the duration of the call; a backend reads only what it needs.
108/// More fields may be added in future releases without breaking existing
109/// implementations — the type is constructed only by the writer, so an
110/// implementer only ever receives one and calls its accessors.
111pub struct PageStoreArgs<'a> {
112 column_index: usize,
113 column_descriptor: &'a ColumnDescriptor,
114}
115
116impl PageStoreArgs<'_> {
117 // Constructed only by the Arrow writer; without that feature there is no caller.
118 #[cfg(feature = "arrow")]
119 pub(crate) fn new(
120 column_index: usize,
121 column_descriptor: &ColumnDescriptor,
122 ) -> PageStoreArgs<'_> {
123 PageStoreArgs {
124 column_index,
125 column_descriptor,
126 }
127 }
128
129 /// Index of the leaf column within the row group.
130 ///
131 /// A backend may use this to e.g. name spill files or shard across a bounded
132 /// pool; it carries no ordering or coordination requirement.
133 pub fn column_index(&self) -> usize {
134 self.column_index
135 }
136
137 /// Descriptor for the leaf column: physical/logical type, path, and max
138 /// definition/repetition levels.
139 ///
140 /// Lets a backend tailor buffering to the column — for example spilling only
141 /// large `BYTE_ARRAY` columns while keeping small fixed-width ones on the
142 /// heap.
143 pub fn column_descriptor(&self) -> &ColumnDescriptor {
144 self.column_descriptor
145 }
146}
147
148/// Creates a fresh [`PageStore`] for each column chunk.
149///
150/// See
151/// [`ArrowWriterOptions::with_page_store_factory`](crate::arrow::arrow_writer::ArrowWriterOptions::with_page_store_factory).
152pub trait PageStoreFactory: Send + Sync + Debug {
153 /// Create a new, empty [`PageStore`] for the leaf column described by `args`.
154 fn create(&self, args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>>;
155}
156
157/// The default [`PageStore`], holding blobs on the heap in a `Vec<Bytes>`.
158///
159/// Peak memory grows with the row group size; use a spilling backend to bound
160/// it.
161#[derive(Debug, Default)]
162pub struct InMemoryPageStore {
163 blobs: Vec<Bytes>,
164 /// Running total of resident blob bytes, kept in step with `put`/`take`.
165 resident: usize,
166}
167
168impl PageStore for InMemoryPageStore {
169 fn put(&mut self, value: Bytes) -> Result<PageKey> {
170 let key = PageKey(self.blobs.len() as u64);
171 self.resident += value.len();
172 self.blobs.push(value);
173 Ok(key)
174 }
175
176 fn take(&mut self, key: PageKey) -> Result<Bytes> {
177 // Replace the slot with an empty `Bytes` so the stored blob is released
178 // as soon as it is taken, keeping memory bounded while the chunk is
179 // streamed into the output file.
180 let blob = self
181 .blobs
182 .get_mut(key.0 as usize)
183 .map(std::mem::take)
184 .ok_or_else(|| ParquetError::General(format!("invalid page key {}", key.0)))?;
185 self.resident -= blob.len();
186 Ok(blob)
187 }
188
189 fn memory_size(&self) -> usize {
190 self.resident
191 }
192}
193
194/// Factory for [`InMemoryPageStore`] — the default used by
195/// [`ArrowWriter`](crate::arrow::arrow_writer::ArrowWriter).
196#[derive(Debug, Default)]
197pub struct InMemoryPageStoreFactory;
198
199impl PageStoreFactory for InMemoryPageStoreFactory {
200 fn create(&self, _args: &PageStoreArgs<'_>) -> Result<Box<dyn PageStore>> {
201 Ok(Box::new(InMemoryPageStore::default()))
202 }
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn in_memory_round_trips_blobs_in_handle_order() {
211 let mut store = InMemoryPageStore::default();
212 let k0 = store.put(Bytes::from_static(b"hello")).unwrap();
213 let k1 = store.put(Bytes::from_static(b"world")).unwrap();
214 assert_ne!(k0, k1);
215 assert_eq!(&store.take(k0).unwrap()[..], b"hello");
216 assert_eq!(&store.take(k1).unwrap()[..], b"world");
217 }
218
219 #[test]
220 fn in_memory_take_releases_the_slot() {
221 let mut store = InMemoryPageStore::default();
222 let k = store.put(Bytes::from_static(b"abc")).unwrap();
223 assert_eq!(&store.take(k).unwrap()[..], b"abc");
224 // A second take yields the emptied placeholder rather than the blob,
225 // confirming the bytes were released on the first take.
226 assert!(store.take(k).unwrap().is_empty());
227 }
228
229 #[test]
230 fn in_memory_invalid_key_errors() {
231 let mut store = InMemoryPageStore::default();
232 assert!(store.take(PageKey(99)).is_err());
233 }
234
235 #[test]
236 fn in_memory_reports_resident_bytes() {
237 let mut store = InMemoryPageStore::default();
238 assert_eq!(store.memory_size(), 0);
239 let k0 = store.put(Bytes::from_static(b"hello")).unwrap();
240 let k1 = store.put(Bytes::from_static(b"!")).unwrap();
241 assert_eq!(store.memory_size(), 6);
242 store.take(k0).unwrap();
243 assert_eq!(store.memory_size(), 1);
244 store.take(k1).unwrap();
245 assert_eq!(store.memory_size(), 0);
246 }
247
248 #[test]
249 fn default_store_memory_size_is_zero() {
250 // A spilling backend that does not override `memory_size` reports 0,
251 // reflecting that its blobs no longer occupy the heap.
252 struct OffHeap;
253 impl PageStore for OffHeap {
254 fn put(&mut self, _value: Bytes) -> Result<PageKey> {
255 Ok(PageKey::new(0))
256 }
257 fn take(&mut self, _key: PageKey) -> Result<Bytes> {
258 Ok(Bytes::new())
259 }
260 }
261 assert_eq!(OffHeap.memory_size(), 0);
262 }
263}