Skip to content

Commit 815f187

Browse files
imordependabot[bot]burmecia7ttpwobsoriano
authored
chore: bump Rust edition to 2024 (supabase#563)
* bump edition to 2024 * clippy fixes * cargo fmt * fixes due to edition change * clippy fixes * shorten unsafe blocks * remove commented out code * run cargo fmt * chore(deps): bump wasmtime in the cargo group across 1 directory (supabase#567) Bumps the cargo group with 1 update in the / directory: [wasmtime](https://github.com/bytecodealliance/wasmtime). Updates `wasmtime` from 36.0.3 to 36.0.5 - [Release notes](https://github.com/bytecodealliance/wasmtime/releases) - [Changelog](https://github.com/bytecodealliance/wasmtime/blob/v36.0.5/RELEASES.md) - [Commits](bytecodealliance/wasmtime@v36.0.3...v36.0.5) --- updated-dependencies: - dependency-name: wasmtime dependency-version: 36.0.5 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * docs(snowflake): correct v0.2.1 checksum (supabase#560) * fix(s3): add delimiter option support for CSV files (supabase#561) * fix(s3): add delimiter option support for CSV files * feat(s3): add csv delimiter table option --------- Co-authored-by: Bo Lu <[email protected]> * feat(clerk): Add CRUD endpoints to existing objects (supabase#541) * feat(clerk): Add CRUD endpoints to existing objects * chore: add pushdown * update docs * reflect available methods in Clerk column * fix example * upgrade clerk fdw version to 0.2.2 --------- Co-authored-by: Bo Lu <[email protected]> * chore(deps): bump bytes in the cargo group across 1 directory (supabase#568) Bumps the cargo group with 1 update in the / directory: [bytes](https://github.com/tokio-rs/bytes). Updates `bytes` from 1.10.1 to 1.11.1 - [Release notes](https://github.com/tokio-rs/bytes/releases) - [Changelog](https://github.com/tokio-rs/bytes/blob/master/CHANGELOG.md) - [Commits](tokio-rs/bytes@v1.10.1...v1.11.1) --- updated-dependencies: - dependency-name: bytes dependency-version: 1.11.1 dependency-type: direct:production dependency-group: cargo ... Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> * fix(clickhouse): implement re_scan for nested loop joins (supabase#546) * fix(clickhouse): implement re_scan for nested loop joins Fixes supabase#532 PostgreSQL uses Nested Loop joins when joining foreign tables. During a Nested Loop join, the inner (right) table is scanned multiple times - once for each row of the outer table. The FDW framework calls re_scan() to restart the inner scan for each outer row. The ClickHouse FDW was using the default no-op re_scan() implementation, which meant that after the first scan completed, subsequent rescans returned no data because: - is_scan_complete was still true - row_receiver channel was exhausted - No new streaming task was spawned This fix implements re_scan() to restart the async streaming: - Abort existing streaming task if running - Reset scan state flags - Reinitialize the bounded channel - Spawn new streaming task with the same SQL query - Fetch the first row to initialize the rescan Also adds integration tests for inner, left, and right joins. * code refactor and upgrade version --------- Co-authored-by: Bo Lu <[email protected]> * fix: serialize data instead of pointer in plan_foreign_modify (supabase#548) * fix: serialize data instead of pointer in plan_foreign_modify Fix PostgreSQL server crash when using prepared statements with foreign tables. PostgreSQL caches query plans after ~5-6 executions. The previous implementation stored a raw pointer to FdwModifyState in fdw_private, which became invalid after end_foreign_modify freed the state. Solution: - Add FdwModifyPrivate struct holding serializable reconstruction data - Serialize table OID, rowid info (not pointer) in plan_foreign_modify - Create fresh FdwModifyState in begin_foreign_modify for each execution Also includes stress test for issue supabase#482 that executes 15 parameterized INSERT operations to validate the fix. Fixes supabase#482 Related to supabase#237 * fix: serialize data instead of pointer in plan_foreign_modify Fixes PostgreSQL server crash when using prepared statements with foreign tables (issue supabase#482). The root cause was that PostgreSQL caches query plans after ~5-6 executions (generic plan optimization). The previous implementation stored a raw pointer to FdwModifyState in fdw_private, which became invalid after end_foreign_modify freed the state. Subsequent executions with cached plans dereferenced this stale pointer, causing a crash. Solution: - Add FdwModifyPrivate struct holding serializable reconstruction data - Serialize table OID, rowid info (not pointer) in plan_foreign_modify - Create fresh FdwModifyState in begin_foreign_modify for each execution This ensures each query execution gets a valid FDW instance and state, even when PostgreSQL reuses a cached query plan and skips planning. * test(clickhouse): add prepared statement stress test Validates fix for issue supabase#482 by executing 15 parameterized INSERT operations - enough to trigger PostgreSQL's generic plan caching. Before the fix, this would crash around iteration 7. * fix(clippy): inline format args in stress test Fix uninlined-format-args clippy warning by using inline variable in format string: format!("stress_{i}") instead of format!("stress_{}", i) * update Cargo.lock * format code --------- Co-authored-by: Bo Lu <[email protected]> Co-authored-by: Bo Lu <[email protected]> * bump edition to 2024 * clippy fixes * cargo fmt * fixes due to edition change * clippy fixes * shorten unsafe blocks * remove commented out code * run cargo fmt * merge from main * fix fmt issue --------- Signed-off-by: dependabot[bot] <[email protected]> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Bo Lu <[email protected]> Co-authored-by: Vaibhav <[email protected]> Co-authored-by: Bo Lu <[email protected]> Co-authored-by: Robert Soriano <[email protected]> Co-authored-by: JohnCari <[email protected]>
1 parent 8d542a9 commit 815f187

52 files changed

Lines changed: 1053 additions & 976 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ exclude = [
1010
resolver = "2"
1111

1212
[workspace.package]
13-
edition = "2021"
13+
edition = "2024"
1414
rust-version = "1.88.0"
1515
homepage = "https://github.com/supabase/wrappers"
1616
repository = "https://github.com/supabase/wrappers"

supabase-wrappers-macros/src/lib.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
extern crate proc_macro;
22
use proc_macro::TokenStream;
33
use proc_macro2::TokenStream as TokenStream2;
4-
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
5-
use syn::{parse_macro_input, punctuated::Punctuated, ItemStruct, Lit, MetaNameValue, Token};
4+
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
5+
use syn::{ItemStruct, Lit, MetaNameValue, Token, parse_macro_input, punctuated::Punctuated};
66

77
/// Create necessary handler, validator and meta functions for foreign data wrapper
88
///

supabase-wrappers/src/import_foreign_schema.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ struct FdwState<E: Into<ErrorReport>, W: ForeignDataWrapper<E>> {
2020
impl<E: Into<ErrorReport>, W: ForeignDataWrapper<E>> FdwState<E, W> {
2121
unsafe fn new(foreignserverid: Oid) -> Self {
2222
Self {
23-
instance: instance::create_fdw_instance_from_server_id(foreignserverid),
23+
instance: unsafe { instance::create_fdw_instance_from_server_id(foreignserverid) },
2424
_phantom: PhantomData,
2525
}
2626
}

supabase-wrappers/src/instance.rs

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ pub(super) unsafe fn create_fdw_instance_from_server_id<
2525
if raw.is_null() {
2626
return None;
2727
}
28-
let c_str = CStr::from_ptr(raw);
28+
let c_str = unsafe { CStr::from_ptr(raw) };
2929
let value = c_str
3030
.to_str()
3131
.map_err(|_| {
@@ -37,16 +37,18 @@ pub(super) unsafe fn create_fdw_instance_from_server_id<
3737
.to_string();
3838
Some(value)
3939
};
40-
let fserver = pg_sys::GetForeignServer(fserver_id);
41-
let server = ForeignServer {
42-
server_oid: fserver_id,
43-
server_name: to_string((*fserver).servername).unwrap(),
44-
server_type: to_string((*fserver).servertype),
45-
server_version: to_string((*fserver).serverversion),
46-
options: options_to_hashmap((*fserver).options).report_unwrap(),
47-
};
48-
let wrapper = W::new(server);
49-
wrapper.report_unwrap()
40+
unsafe {
41+
let fserver = pg_sys::GetForeignServer(fserver_id);
42+
let server = ForeignServer {
43+
server_oid: fserver_id,
44+
server_name: to_string((*fserver).servername).unwrap(),
45+
server_type: to_string((*fserver).servertype),
46+
server_version: to_string((*fserver).serverversion),
47+
options: options_to_hashmap((*fserver).options).report_unwrap(),
48+
};
49+
let wrapper = W::new(server);
50+
wrapper.report_unwrap()
51+
}
5052
}
5153

5254
// create a fdw instance from a foreign table id
@@ -56,6 +58,8 @@ pub(super) unsafe fn create_fdw_instance_from_table_id<
5658
>(
5759
ftable_id: pg_sys::Oid,
5860
) -> W {
59-
let ftable = pg_sys::GetForeignTable(ftable_id);
60-
create_fdw_instance_from_server_id((*ftable).serverid)
61+
unsafe {
62+
let ftable = pg_sys::GetForeignTable(ftable_id);
63+
create_fdw_instance_from_server_id((*ftable).serverid)
64+
}
6165
}

supabase-wrappers/src/interface.rs

Lines changed: 88 additions & 84 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,15 @@
11
//! Provides interface types and trait to develop Postgres foreign data wrapper
22
//!
33
4-
use crate::instance::ForeignServer;
54
use crate::FdwRoutine;
5+
use crate::instance::ForeignServer;
66
use pgrx::pg_sys::panic::ErrorReport;
77
use pgrx::prelude::{Date, Interval, Time, Timestamp, TimestampWithTimeZone};
88
use pgrx::{
9+
AllocatedByRust, AnyNumeric, FromDatum, IntoDatum, JsonB, PgBuiltInOids, PgOid,
910
datum::Uuid,
1011
fcinfo,
11-
pg_sys::{self, bytea, BuiltinOid, Datum, Expr, ExprState, Oid},
12-
AllocatedByRust, AnyNumeric, FromDatum, IntoDatum, JsonB, PgBuiltInOids, PgOid,
12+
pg_sys::{self, BuiltinOid, Datum, Expr, ExprState, Oid, bytea},
1313
};
1414
use std::collections::HashMap;
1515
use std::ffi::CStr;
@@ -285,90 +285,94 @@ impl FromDatum for Cell {
285285
where
286286
Self: Sized,
287287
{
288-
let oid = PgOid::from(typoid);
289-
match oid {
290-
PgOid::BuiltIn(PgBuiltInOids::BOOLOID) => {
291-
bool::from_datum(datum, is_null).map(Cell::Bool)
292-
}
293-
PgOid::BuiltIn(PgBuiltInOids::CHAROID) => i8::from_datum(datum, is_null).map(Cell::I8),
294-
PgOid::BuiltIn(PgBuiltInOids::INT2OID) => {
295-
i16::from_datum(datum, is_null).map(Cell::I16)
296-
}
297-
PgOid::BuiltIn(PgBuiltInOids::FLOAT4OID) => {
298-
f32::from_datum(datum, is_null).map(Cell::F32)
299-
}
300-
PgOid::BuiltIn(PgBuiltInOids::INT4OID) => {
301-
i32::from_datum(datum, is_null).map(Cell::I32)
302-
}
303-
PgOid::BuiltIn(PgBuiltInOids::FLOAT8OID) => {
304-
f64::from_datum(datum, is_null).map(Cell::F64)
305-
}
306-
PgOid::BuiltIn(PgBuiltInOids::INT8OID) => {
307-
i64::from_datum(datum, is_null).map(Cell::I64)
308-
}
309-
PgOid::BuiltIn(PgBuiltInOids::NUMERICOID) => {
310-
AnyNumeric::from_datum(datum, is_null).map(Cell::Numeric)
311-
}
312-
PgOid::BuiltIn(PgBuiltInOids::TEXTOID) => {
313-
String::from_datum(datum, is_null).map(Cell::String)
314-
}
315-
PgOid::BuiltIn(PgBuiltInOids::DATEOID) => {
316-
Date::from_datum(datum, is_null).map(Cell::Date)
317-
}
318-
PgOid::BuiltIn(PgBuiltInOids::TIMEOID) => {
319-
Time::from_datum(datum, is_null).map(Cell::Time)
320-
}
321-
PgOid::BuiltIn(PgBuiltInOids::TIMESTAMPOID) => {
322-
Timestamp::from_datum(datum, is_null).map(Cell::Timestamp)
323-
}
324-
PgOid::BuiltIn(PgBuiltInOids::TIMESTAMPTZOID) => {
325-
TimestampWithTimeZone::from_datum(datum, is_null).map(Cell::Timestamptz)
326-
}
327-
PgOid::BuiltIn(PgBuiltInOids::INTERVALOID) => {
328-
Interval::from_datum(datum, is_null).map(Cell::Interval)
329-
}
330-
PgOid::BuiltIn(PgBuiltInOids::JSONBOID) => {
331-
JsonB::from_datum(datum, is_null).map(Cell::Json)
332-
}
333-
PgOid::BuiltIn(PgBuiltInOids::BYTEAOID) => {
334-
if is_null {
335-
None
336-
} else {
337-
Some(Cell::Bytea(datum.cast_mut_ptr::<bytea>()))
288+
unsafe {
289+
let oid = PgOid::from(typoid);
290+
match oid {
291+
PgOid::BuiltIn(PgBuiltInOids::BOOLOID) => {
292+
bool::from_datum(datum, is_null).map(Cell::Bool)
338293
}
339-
}
340-
PgOid::BuiltIn(PgBuiltInOids::UUIDOID) => {
341-
Uuid::from_datum(datum, is_null).map(Cell::Uuid)
342-
}
343-
PgOid::BuiltIn(PgBuiltInOids::BOOLARRAYOID) => {
344-
Vec::<Option<bool>>::from_datum(datum, false).map(Cell::BoolArray)
345-
}
346-
PgOid::BuiltIn(PgBuiltInOids::INT2ARRAYOID) => {
347-
Vec::<Option<i16>>::from_datum(datum, false).map(Cell::I16Array)
348-
}
349-
PgOid::BuiltIn(PgBuiltInOids::INT4ARRAYOID) => {
350-
Vec::<Option<i32>>::from_datum(datum, false).map(Cell::I32Array)
351-
}
352-
PgOid::BuiltIn(PgBuiltInOids::INT8ARRAYOID) => {
353-
Vec::<Option<i64>>::from_datum(datum, false).map(Cell::I64Array)
354-
}
355-
PgOid::BuiltIn(PgBuiltInOids::FLOAT4ARRAYOID) => {
356-
Vec::<Option<f32>>::from_datum(datum, false).map(Cell::F32Array)
357-
}
358-
PgOid::BuiltIn(PgBuiltInOids::FLOAT8ARRAYOID) => {
359-
Vec::<Option<f64>>::from_datum(datum, false).map(Cell::F64Array)
360-
}
361-
PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID) => {
362-
Vec::<Option<String>>::from_datum(datum, false).map(Cell::StringArray)
363-
}
364-
PgOid::Custom(_) => {
365-
if is_null {
366-
None
367-
} else {
368-
Some(Cell::Bytea(datum.cast_mut_ptr::<bytea>()))
294+
PgOid::BuiltIn(PgBuiltInOids::CHAROID) => {
295+
i8::from_datum(datum, is_null).map(Cell::I8)
296+
}
297+
PgOid::BuiltIn(PgBuiltInOids::INT2OID) => {
298+
i16::from_datum(datum, is_null).map(Cell::I16)
299+
}
300+
PgOid::BuiltIn(PgBuiltInOids::FLOAT4OID) => {
301+
f32::from_datum(datum, is_null).map(Cell::F32)
302+
}
303+
PgOid::BuiltIn(PgBuiltInOids::INT4OID) => {
304+
i32::from_datum(datum, is_null).map(Cell::I32)
305+
}
306+
PgOid::BuiltIn(PgBuiltInOids::FLOAT8OID) => {
307+
f64::from_datum(datum, is_null).map(Cell::F64)
308+
}
309+
PgOid::BuiltIn(PgBuiltInOids::INT8OID) => {
310+
i64::from_datum(datum, is_null).map(Cell::I64)
311+
}
312+
PgOid::BuiltIn(PgBuiltInOids::NUMERICOID) => {
313+
AnyNumeric::from_datum(datum, is_null).map(Cell::Numeric)
314+
}
315+
PgOid::BuiltIn(PgBuiltInOids::TEXTOID) => {
316+
String::from_datum(datum, is_null).map(Cell::String)
317+
}
318+
PgOid::BuiltIn(PgBuiltInOids::DATEOID) => {
319+
Date::from_datum(datum, is_null).map(Cell::Date)
320+
}
321+
PgOid::BuiltIn(PgBuiltInOids::TIMEOID) => {
322+
Time::from_datum(datum, is_null).map(Cell::Time)
323+
}
324+
PgOid::BuiltIn(PgBuiltInOids::TIMESTAMPOID) => {
325+
Timestamp::from_datum(datum, is_null).map(Cell::Timestamp)
326+
}
327+
PgOid::BuiltIn(PgBuiltInOids::TIMESTAMPTZOID) => {
328+
TimestampWithTimeZone::from_datum(datum, is_null).map(Cell::Timestamptz)
329+
}
330+
PgOid::BuiltIn(PgBuiltInOids::INTERVALOID) => {
331+
Interval::from_datum(datum, is_null).map(Cell::Interval)
332+
}
333+
PgOid::BuiltIn(PgBuiltInOids::JSONBOID) => {
334+
JsonB::from_datum(datum, is_null).map(Cell::Json)
335+
}
336+
PgOid::BuiltIn(PgBuiltInOids::BYTEAOID) => {
337+
if is_null {
338+
None
339+
} else {
340+
Some(Cell::Bytea(datum.cast_mut_ptr::<bytea>()))
341+
}
342+
}
343+
PgOid::BuiltIn(PgBuiltInOids::UUIDOID) => {
344+
Uuid::from_datum(datum, is_null).map(Cell::Uuid)
345+
}
346+
PgOid::BuiltIn(PgBuiltInOids::BOOLARRAYOID) => {
347+
Vec::<Option<bool>>::from_datum(datum, false).map(Cell::BoolArray)
348+
}
349+
PgOid::BuiltIn(PgBuiltInOids::INT2ARRAYOID) => {
350+
Vec::<Option<i16>>::from_datum(datum, false).map(Cell::I16Array)
351+
}
352+
PgOid::BuiltIn(PgBuiltInOids::INT4ARRAYOID) => {
353+
Vec::<Option<i32>>::from_datum(datum, false).map(Cell::I32Array)
354+
}
355+
PgOid::BuiltIn(PgBuiltInOids::INT8ARRAYOID) => {
356+
Vec::<Option<i64>>::from_datum(datum, false).map(Cell::I64Array)
357+
}
358+
PgOid::BuiltIn(PgBuiltInOids::FLOAT4ARRAYOID) => {
359+
Vec::<Option<f32>>::from_datum(datum, false).map(Cell::F32Array)
360+
}
361+
PgOid::BuiltIn(PgBuiltInOids::FLOAT8ARRAYOID) => {
362+
Vec::<Option<f64>>::from_datum(datum, false).map(Cell::F64Array)
363+
}
364+
PgOid::BuiltIn(PgBuiltInOids::TEXTARRAYOID) => {
365+
Vec::<Option<String>>::from_datum(datum, false).map(Cell::StringArray)
366+
}
367+
PgOid::Custom(_) => {
368+
if is_null {
369+
None
370+
} else {
371+
Some(Cell::Bytea(datum.cast_mut_ptr::<bytea>()))
372+
}
369373
}
374+
_ => None,
370375
}
371-
_ => None,
372376
}
373377
}
374378
}

supabase-wrappers/src/lib.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -307,8 +307,8 @@ pub mod prelude {
307307
pub use tokio::runtime::Runtime;
308308
}
309309

310-
use pgrx::prelude::*;
311310
use pgrx::AllocatedByPostgres;
311+
use pgrx::prelude::*;
312312

313313
mod import_foreign_schema;
314314
mod instance;

supabase-wrappers/src/limit.rs

Lines changed: 36 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,49 +1,53 @@
11
use crate::interface::Limit;
2-
use pgrx::{is_a, pg_sys, FromDatum};
2+
use pgrx::{FromDatum, is_a, pg_sys};
33

44
// extract limit
55
pub(crate) unsafe fn extract_limit(
66
root: *mut pg_sys::PlannerInfo,
77
_baserel: *mut pg_sys::RelOptInfo,
88
_baserel_id: pg_sys::Oid,
99
) -> Option<Limit> {
10-
let parse = (*root).parse;
10+
unsafe {
11+
let parse = (*root).parse;
1112

12-
// don't push down LIMIT if the query has a GROUP BY clause or aggregates
13-
if !(*parse).groupClause.is_null() || (*parse).hasAggs {
14-
return None;
15-
}
16-
17-
// only push down constant LIMITs that are not NULL
18-
let limit_count = (*parse).limitCount as *mut pg_sys::Const;
19-
if limit_count.is_null() || !is_a(limit_count as *mut pg_sys::Node, pg_sys::NodeTag::T_Const) {
20-
return None;
21-
}
13+
// don't push down LIMIT if the query has a GROUP BY clause or aggregates
14+
if !(*parse).groupClause.is_null() || (*parse).hasAggs {
15+
return None;
16+
}
2217

23-
let mut limit = Limit::default();
18+
// only push down constant LIMITs that are not NULL
19+
let limit_count = (*parse).limitCount as *mut pg_sys::Const;
20+
if limit_count.is_null()
21+
|| !is_a(limit_count as *mut pg_sys::Node, pg_sys::NodeTag::T_Const)
22+
{
23+
return None;
24+
}
2425

25-
if let Some(count) = i64::from_polymorphic_datum(
26-
(*limit_count).constvalue,
27-
(*limit_count).constisnull,
28-
(*limit_count).consttype,
29-
) {
30-
limit.count = count;
31-
} else {
32-
return None;
33-
}
26+
let mut limit = Limit::default();
3427

35-
// only consider OFFSETS that are non-NULL constants
36-
let limit_offset = (*parse).limitOffset as *mut pg_sys::Const;
37-
if !limit_offset.is_null() && is_a(limit_offset as *mut pg_sys::Node, pg_sys::NodeTag::T_Const)
38-
{
39-
if let Some(offset) = i64::from_polymorphic_datum(
40-
(*limit_offset).constvalue,
41-
(*limit_offset).constisnull,
42-
(*limit_offset).consttype,
28+
if let Some(count) = i64::from_polymorphic_datum(
29+
(*limit_count).constvalue,
30+
(*limit_count).constisnull,
31+
(*limit_count).consttype,
4332
) {
33+
limit.count = count;
34+
} else {
35+
return None;
36+
}
37+
38+
// only consider OFFSETS that are non-NULL constants
39+
let limit_offset = (*parse).limitOffset as *mut pg_sys::Const;
40+
if !limit_offset.is_null()
41+
&& is_a(limit_offset as *mut pg_sys::Node, pg_sys::NodeTag::T_Const)
42+
&& let Some(offset) = i64::from_polymorphic_datum(
43+
(*limit_offset).constvalue,
44+
(*limit_offset).constisnull,
45+
(*limit_offset).consttype,
46+
)
47+
{
4448
limit.offset = offset;
4549
}
46-
}
4750

48-
Some(limit)
51+
Some(limit)
52+
}
4953
}

0 commit comments

Comments
 (0)