v0.3.3-pre.006
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/README.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# ksp-store-postgres-lib
|
||||
|
||||
@@ -83,7 +83,7 @@ ksp_store_schema_migrations
|
||||
|
||||
Le moteur vérifie version, nom et checksum SHA-256, sérialise les runners par advisory transaction lock et refuse une history divergente ou plus récente que le runtime.
|
||||
|
||||
V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit.
|
||||
V001 possède désormais le schéma physique `RawTransaction` et son contrat de compatibilité. Les lectures exactes sont acquises depuis `pre.004`; `pre.005` ajoute les écritures atomiques transaction + observation, l'idempotence réelle et la classification de conflit. `pre.006` ajoute la navigation keyset déterministe sur l'index `(slot, signature)` et son cursor opaque lié à la requête.
|
||||
|
||||
## Health et erreurs
|
||||
|
||||
@@ -134,6 +134,19 @@ Un tombstone `Purged` compatible produit `SkippedPurged/NotRecorded` en mode nor
|
||||
|
||||
Les erreurs physiques d'écriture sont réduites à `WriteFailed`; aucune erreur serveur, SQLSTATE, query ou valeur de bind n'est conservée.
|
||||
|
||||
## Pagination RAW `0.3.3-pre.006`
|
||||
|
||||
`list_raw_transactions` parcourt uniquement les références canoniques récupérables, en excluant les tombstones `Purged`. L'ordre total est :
|
||||
|
||||
```text
|
||||
Ascending = (slot ASC, signature ASC)
|
||||
Descending = (slot DESC, signature DESC)
|
||||
```
|
||||
|
||||
La continuation est une keyset stricte `>` / `<`, jamais un `OFFSET`. Le cursor backend V1 fait exactement 109 octets et transporte `KSPT`, version 1, le dernier slot, la dernière signature et un digest SHA-256 lié au réseau, à la direction et aux bornes de slots. Un cursor rejoué sous une autre query est `QueryInvalid`.
|
||||
|
||||
`RawPageLimit` n'est pas réduit par une policy KSP : PostgreSQL utilise `LIMIT requested + 1`, avec comme seule borne physique `requested <= i64::MAX - 1`. Une valeur supérieure produit `PageLimitUnsupported` sans clamp. La pagination ne promet aucun snapshot inter-pages.
|
||||
|
||||
## Hors périmètre actuel
|
||||
|
||||
La crate ne contient encore :
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!-- file: crates/ksp-store-postgres-lib/USAGE.md -->
|
||||
<!-- version: 4 -->
|
||||
<!-- version: 5 -->
|
||||
|
||||
# Utilisation de ksp-store-postgres-lib
|
||||
|
||||
@@ -129,6 +129,8 @@ match error.kind() {
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PageLimitUnsupported => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound => {}
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer => {}
|
||||
@@ -188,12 +190,25 @@ Pour une transaction purgée, le mode normal retourne `SkippedPurged/NotRecorded
|
||||
|
||||
Les références réseau-scopées sont toujours validées avant `pool.get()`.
|
||||
|
||||
## 9. Ce que cette crate ne permet pas encore
|
||||
## 9. Paginer les transactions RAW
|
||||
|
||||
Depuis `0.3.3-pre.006`, le backend fournit une navigation keyset déterministe :
|
||||
|
||||
```rust
|
||||
let page = backend.list_raw_transactions(&query).await;
|
||||
```
|
||||
|
||||
La query utilise les bornes inclusives de `RawSlotRange`, la direction demandée et le `RawPageLimit` exact. Les lignes `Purged` sont exclues. L'ordre est `(slot, signature)` dans les deux directions et la continuation utilise un cursor opaque de 109 octets lié au réseau, à la direction et aux bornes de la query.
|
||||
|
||||
Le cursor peut être transmis uniquement à une query ayant le même binding. Un changement de réseau/range/direction ou des bytes hostiles produit `QueryInvalid` avant acquisition du pool. Le backend n'utilise aucun `OFFSET` et ne fournit pas de snapshot transactionnel entre deux pages.
|
||||
|
||||
La seule limite physique est liée au `LIMIT + 1` PostgreSQL : `requested <= i64::MAX - 1`. Au-delà, `PageLimitUnsupported` est retourné ; la demande n'est jamais ramenée à 100, 500, 1000 ou une autre policy de worker.
|
||||
|
||||
## 10. Ce que cette crate ne permet pas encore
|
||||
|
||||
La tranche ne fournit pas encore :
|
||||
|
||||
```text
|
||||
list_raw_transactions / cursor
|
||||
transitions de rétention
|
||||
implémentations complètes des six traits RawTransaction*
|
||||
capabilities RawAccount*
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/error.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
/// Stable KSP error code reserved for PostgreSQL retention transitions that require unsupported physical compaction.
|
||||
pub const ERROR_CODE_POSTGRES_RETENTION_COMPACTION_UNSUPPORTED: ksp_store_api::ErrorCode =
|
||||
@@ -23,8 +23,12 @@ pub enum PostgresBackendErrorKind {
|
||||
DataInvalid,
|
||||
/// PostgreSQL migration/bootstrap execution failed without exposing server text or SQL.
|
||||
MigrationFailed,
|
||||
/// The requested RAW page size cannot be represented by PostgreSQL LIMIT plus the continuation probe row.
|
||||
PageLimitUnsupported,
|
||||
/// Applied PostgreSQL migration history diverges from the embedded immutable KSP history.
|
||||
MigrationMismatch,
|
||||
/// A bounded backend-private RAW query or opaque cursor is invalid for the requested navigation context.
|
||||
QueryInvalid,
|
||||
/// A PostgreSQL RAW read statement failed without exposing server text, SQL or bind values.
|
||||
ReadFailed,
|
||||
/// A RAW write requires an existing canonical reference that is not durable.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/lib.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -15,7 +15,8 @@
|
||||
//! backend-private RAW transaction/observation/retention read mapping.
|
||||
//! `0.3.3-pre.005` adds atomic canonical/observation writes, real idempotence
|
||||
//! checks and safe conflict classification without exposing PostgreSQL rows or
|
||||
//! SQL through the public bridge.
|
||||
//! SQL through the public bridge. `0.3.3-pre.006` adds deterministic keyset
|
||||
//! pagination with a fixed opaque cursor bound to network, range and direction.
|
||||
//!
|
||||
//! This crate depends on `ksp-store-api` and never on `ksp-store-lib`. The
|
||||
//! common facade consumes only this crate's narrow backend bridge and never
|
||||
@@ -54,6 +55,14 @@ pub(crate) use self::health::probe_health;
|
||||
pub(crate) use self::migration::bootstrap;
|
||||
/// Current embedded migration version consumed by the private health probe.
|
||||
pub(crate) use self::migration::current_migration_version;
|
||||
/// Private decoded RAW cursor representation consumed by tests and the physical RAW module.
|
||||
pub(crate) use self::raw_transaction::cursor::RawTransactionDecodedCursor;
|
||||
/// Private RAW transaction cursor decoder consumed by the physical RAW module.
|
||||
pub(crate) use self::raw_transaction::cursor::decode_raw_transaction_cursor;
|
||||
/// Private RAW transaction cursor encoder consumed by the physical RAW module.
|
||||
pub(crate) use self::raw_transaction::cursor::encode_raw_transaction_cursor;
|
||||
/// Private physical page-limit converter consumed by the physical RAW module.
|
||||
pub(crate) use self::raw_transaction::cursor::raw_transaction_physical_page_limit;
|
||||
/// Private RAW transaction reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_transaction::get_raw_transaction;
|
||||
/// Private RAW transaction observation reader consumed by the physical backend runtime.
|
||||
@@ -62,6 +71,8 @@ pub(crate) use self::raw_transaction::get_raw_transaction_observation;
|
||||
pub(crate) use self::raw_transaction::get_raw_transaction_retention_state;
|
||||
/// Private RAW transaction tombstone reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_transaction::get_raw_transaction_tombstone;
|
||||
/// Private RAW transaction list reader consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_transaction::list_raw_transactions;
|
||||
/// Private atomic RAW transaction acquisition writer consumed by the physical backend runtime.
|
||||
pub(crate) use self::raw_transaction::persist_raw_transaction_acquisition;
|
||||
/// Private additional RAW transaction observation writer consumed by the physical backend runtime.
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_transaction.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
pub(crate) mod cursor;
|
||||
|
||||
const GET_ARCHIVE_PAYLOAD_SQL: &str = "SELECT payload FROM ksp_raw_transaction_archive_payloads WHERE signature = $1";
|
||||
const GET_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1";
|
||||
@@ -8,12 +10,19 @@ const GET_TOMBSTONE_SQL: &str = "SELECT signature, slot::text AS slot_text, bloc
|
||||
const GET_TRANSACTION_SQL: &str = "SELECT transaction_row.signature, transaction_row.slot::text AS slot_text, transaction_row.block_time_unix_millis, transaction_row.format_id, transaction_row.format_version, transaction_row.content_hash, transaction_row.payload, transaction_row.retention_state, archive_row.payload AS archive_payload FROM ksp_raw_transactions AS transaction_row LEFT JOIN ksp_raw_transaction_archive_payloads AS archive_row ON archive_row.signature = transaction_row.signature WHERE transaction_row.signature = $1";
|
||||
const INSERT_OBSERVATION_SQL: &str = "INSERT INTO ksp_raw_transaction_observations (observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14) ON CONFLICT (observation_key) DO NOTHING RETURNING observation_key";
|
||||
const INSERT_TRANSACTION_SQL: &str = "INSERT INTO ksp_raw_transactions (signature, slot, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state) VALUES ($1, $2::TEXT::NUMERIC, $3, $4, $5, $6, $7, 'full') ON CONFLICT (signature) DO NOTHING RETURNING signature";
|
||||
const LIST_TRANSACTIONS_ASC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) > ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot ASC, signature ASC LIMIT $5";
|
||||
const LIST_TRANSACTIONS_DESC_SQL: &str = "SELECT signature, slot::text AS slot_text FROM ksp_raw_transactions WHERE retention_state <> 'purged' AND ($1::TEXT IS NULL OR slot >= $1::TEXT::NUMERIC) AND ($2::TEXT IS NULL OR slot <= $2::TEXT::NUMERIC) AND ($3::TEXT IS NULL OR (slot, signature) < ($3::TEXT::NUMERIC, $4::BYTEA)) ORDER BY slot DESC, signature DESC LIMIT $5";
|
||||
const LOCK_OBSERVATION_SQL: &str = "SELECT observation_key, transaction_signature, provider, protocol, acquisition_method, origin, received_at_unix_millis, capture_session_id, commitment, endpoint_id, filter_id, observed_at_unix_millis, source_payload_hash, source_payload_size_bytes FROM ksp_raw_transaction_observations WHERE observation_key = $1 FOR UPDATE";
|
||||
const LOCK_TRANSACTION_SQL: &str = "SELECT signature, slot::text AS slot_text, block_time_unix_millis, format_id, format_version, content_hash, payload, retention_state, NULL::BYTEA AS archive_payload FROM ksp_raw_transactions WHERE signature = $1 FOR UPDATE";
|
||||
const LOCK_TRANSACTION_STATE_SQL: &str = "SELECT retention_state FROM ksp_raw_transactions WHERE signature = $1 FOR UPDATE";
|
||||
const REHYDRATE_TRANSACTION_SQL: &str =
|
||||
"UPDATE ksp_raw_transactions SET block_time_unix_millis = $2, payload = $3, retention_state = 'full' WHERE signature = $1";
|
||||
|
||||
struct RawListDbRow {
|
||||
signature: std::vec::Vec<u8>,
|
||||
slot_text: std::string::String,
|
||||
}
|
||||
|
||||
struct RawObservationDbRow {
|
||||
acquisition_method: std::string::String,
|
||||
capture_session_id: std::option::Option<std::string::String>,
|
||||
@@ -94,6 +103,82 @@ pub(crate) async fn get_raw_transaction(
|
||||
return decode_raw_transaction_row(network, physical);
|
||||
}
|
||||
|
||||
/// Lists deterministic canonical RAW transaction references using PostgreSQL keyset pagination.
|
||||
pub(crate) async fn list_raw_transactions(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
query: &ksp_store_api::RawTransactionQuery,
|
||||
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>, crate::PostgresBackendError> {
|
||||
if query.network() != network {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WrongNetwork, "raw_transaction_list_network"));
|
||||
}
|
||||
let (requested_usize, sql_limit) = match crate::raw_transaction_physical_page_limit(query.page().limit().get()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let decoded_cursor = match query.page().cursor() {
|
||||
std::option::Option::Some(value) => match crate::decode_raw_transaction_cursor(query, value) {
|
||||
std::result::Result::Ok(decoded) => std::option::Option::Some(decoded),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
},
|
||||
std::option::Option::None => std::option::Option::None,
|
||||
};
|
||||
let client_result = pool.get().await;
|
||||
let client = match client_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(crate::map_pool_error(error)),
|
||||
};
|
||||
let slots = query.slots();
|
||||
let start_text = slots.start_inclusive().map(|value| return value.to_string());
|
||||
let end_text = slots.end_inclusive().map(|value| return value.to_string());
|
||||
let cursor_slot_text = decoded_cursor.as_ref().map(|value| return value.last_slot.to_string());
|
||||
let cursor_signature = decoded_cursor.as_ref().map(|value| return value.last_signature.to_vec());
|
||||
let sql = match query.direction() {
|
||||
ksp_store_api::RawSortDirection::Ascending => LIST_TRANSACTIONS_ASC_SQL,
|
||||
ksp_store_api::RawSortDirection::Descending => LIST_TRANSACTIONS_DESC_SQL,
|
||||
_ => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, "raw_transaction_list_direction"));
|
||||
},
|
||||
};
|
||||
let rows_result = client.query(sql, &[&start_text, &end_text, &cursor_slot_text, &cursor_signature, &sql_limit]).await;
|
||||
let rows = match rows_result {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::ReadFailed, "raw_transaction_list_query"));
|
||||
},
|
||||
};
|
||||
let mut decoded = std::vec::Vec::with_capacity(rows.len());
|
||||
for row in rows {
|
||||
let physical = match raw_list_db_row(&row) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let item = match decode_raw_list_row(network, physical) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
decoded.push(item);
|
||||
}
|
||||
let has_more = decoded.len() > requested_usize;
|
||||
if has_more {
|
||||
decoded.truncate(requested_usize);
|
||||
}
|
||||
let next_cursor = if has_more {
|
||||
let last = match decoded.last() {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => return std::result::Result::Err(data_invalid("raw_transaction_list_page")),
|
||||
};
|
||||
match crate::encode_raw_transaction_cursor(query, last.0, &last.1.signature()) {
|
||||
std::result::Result::Ok(value) => std::option::Option::Some(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
}
|
||||
} else {
|
||||
std::option::Option::None
|
||||
};
|
||||
let items = decoded.into_iter().map(|value| return value.1).collect();
|
||||
return std::result::Result::Ok(ksp_store_api::RawPage::new(items, next_cursor));
|
||||
}
|
||||
|
||||
/// Reads one RAW transaction observation from the physical PostgreSQL backend.
|
||||
pub(crate) async fn get_raw_transaction_observation(
|
||||
pool: &deadpool_postgres::Pool,
|
||||
@@ -655,6 +740,18 @@ fn write_failed(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::WriteFailed, phase);
|
||||
}
|
||||
|
||||
fn raw_list_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawListDbRow, crate::PostgresBackendError> {
|
||||
let signature = match row.try_get::<_, std::vec::Vec<u8>>("signature") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_list_decode")),
|
||||
};
|
||||
let slot_text = match row.try_get::<_, std::string::String>("slot_text") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(data_invalid("raw_transaction_list_decode")),
|
||||
};
|
||||
return std::result::Result::Ok(RawListDbRow { signature, slot_text });
|
||||
}
|
||||
|
||||
fn raw_transaction_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawTransactionDbRow, crate::PostgresBackendError> {
|
||||
let signature = match row.try_get::<_, std::vec::Vec<u8>>("signature") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
@@ -772,6 +869,22 @@ fn raw_tombstone_db_row(row: &tokio_postgres::Row) -> std::result::Result<RawTom
|
||||
});
|
||||
}
|
||||
|
||||
fn decode_raw_list_row(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
row: RawListDbRow,
|
||||
) -> std::result::Result<(u64, ksp_store_api::RawTransactionReference), crate::PostgresBackendError> {
|
||||
let signature = match fixed_bytes::<64>(row.signature) {
|
||||
std::result::Result::Ok(value) => ksp_store_api::RawTransactionSignature::new(value),
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let slot = match decode_u64_decimal(row.slot_text.as_str(), "raw_transaction_list_slot") {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let reference = ksp_store_api::RawTransactionReference::new(network.clone(), signature);
|
||||
return std::result::Result::Ok((slot, reference));
|
||||
}
|
||||
|
||||
fn decode_raw_transaction_row(
|
||||
network: &ksp_store_api::RawNetworkId,
|
||||
row: RawTransactionDbRow,
|
||||
|
||||
184
crates/ksp-store-postgres-lib/src/raw_transaction/cursor.rs
Normal file
184
crates/ksp-store-postgres-lib/src/raw_transaction/cursor.rs
Normal file
@@ -0,0 +1,184 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/raw_transaction/cursor.rs
|
||||
// version: 1
|
||||
|
||||
use sha2::Digest; // rust-rules: trait-import
|
||||
|
||||
const CURSOR_BYTES: usize = 109;
|
||||
const CURSOR_DIGEST_OFFSET: usize = 77;
|
||||
const CURSOR_DOMAIN: &[u8] = b"KSP/raw-transaction-cursor/v1";
|
||||
const CURSOR_MAGIC: &[u8; 4] = b"KSPT";
|
||||
const CURSOR_VERSION: u8 = 1;
|
||||
const MAX_POSTGRES_PAGE_LIMIT: u64 = 9_223_372_036_854_775_806;
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
/// Decoded backend-private continuation key extracted from one validated RAW cursor.
|
||||
pub(crate) struct RawTransactionDecodedCursor {
|
||||
/// Last canonical transaction signature returned by the previous page.
|
||||
pub(crate) last_signature: [u8; 64],
|
||||
/// Last canonical transaction slot returned by the previous page.
|
||||
pub(crate) last_slot: u64,
|
||||
}
|
||||
|
||||
/// Decodes and validates one backend-private RAW transaction cursor against its query binding.
|
||||
pub(crate) fn decode_raw_transaction_cursor(
|
||||
query: &ksp_store_api::RawTransactionQuery,
|
||||
cursor: &ksp_store_api::RawPageCursor,
|
||||
) -> std::result::Result<RawTransactionDecodedCursor, crate::PostgresBackendError> {
|
||||
let bytes = cursor.as_bytes();
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_size"));
|
||||
}
|
||||
if bytes.get(0..4) != std::option::Option::Some(CURSOR_MAGIC.as_ref()) {
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_magic"));
|
||||
}
|
||||
if bytes.get(4).copied() != std::option::Option::Some(CURSOR_VERSION) {
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_version"));
|
||||
}
|
||||
let slot_bytes = match bytes.get(5..13) {
|
||||
std::option::Option::Some(value) => match <[u8; 8]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_slot")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_slot")),
|
||||
};
|
||||
let last_slot = u64::from_be_bytes(slot_bytes);
|
||||
let last_signature = match bytes.get(13..CURSOR_DIGEST_OFFSET) {
|
||||
std::option::Option::Some(value) => match <[u8; 64]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_signature")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_signature")),
|
||||
};
|
||||
let range_result = validate_slot_in_range(query.slots(), last_slot);
|
||||
if let std::result::Result::Err(error) = range_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let expected_digest = match binding_digest(query, last_slot, &last_signature) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let stored_digest = match bytes.get(CURSOR_DIGEST_OFFSET..CURSOR_BYTES) {
|
||||
std::option::Option::Some(value) => match <[u8; 32]>::try_from(value) {
|
||||
std::result::Result::Ok(decoded) => decoded,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_digest")),
|
||||
},
|
||||
std::option::Option::None => return std::result::Result::Err(query_invalid("raw_cursor_digest")),
|
||||
};
|
||||
if stored_digest != expected_digest {
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_binding"));
|
||||
}
|
||||
return std::result::Result::Ok(RawTransactionDecodedCursor { last_signature, last_slot });
|
||||
}
|
||||
|
||||
/// Encodes one backend-private RAW transaction cursor bound to the supplied query context.
|
||||
pub(crate) fn encode_raw_transaction_cursor(
|
||||
query: &ksp_store_api::RawTransactionQuery,
|
||||
last_slot: u64,
|
||||
last_signature: &ksp_store_api::RawTransactionSignature,
|
||||
) -> std::result::Result<ksp_store_api::RawPageCursor, crate::PostgresBackendError> {
|
||||
let range_result = validate_slot_in_range(query.slots(), last_slot);
|
||||
if let std::result::Result::Err(error) = range_result {
|
||||
return std::result::Result::Err(error);
|
||||
}
|
||||
let digest = match binding_digest(query, last_slot, last_signature.as_bytes()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => return std::result::Result::Err(error),
|
||||
};
|
||||
let mut bytes = std::vec::Vec::with_capacity(CURSOR_BYTES);
|
||||
bytes.extend_from_slice(CURSOR_MAGIC);
|
||||
bytes.push(CURSOR_VERSION);
|
||||
bytes.extend_from_slice(&last_slot.to_be_bytes());
|
||||
bytes.extend_from_slice(last_signature.as_bytes());
|
||||
bytes.extend_from_slice(&digest);
|
||||
if bytes.len() != CURSOR_BYTES {
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_encode_size"));
|
||||
}
|
||||
return match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => std::result::Result::Ok(value),
|
||||
std::result::Result::Err(_) => std::result::Result::Err(query_invalid("raw_cursor_encode")),
|
||||
};
|
||||
}
|
||||
|
||||
/// Converts one caller page size into the exact PostgreSQL LIMIT+1 representation.
|
||||
pub(crate) fn raw_transaction_physical_page_limit(requested: u64) -> std::result::Result<(usize, i64), crate::PostgresBackendError> {
|
||||
if requested > MAX_POSTGRES_PAGE_LIMIT {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit"));
|
||||
}
|
||||
let requested_usize = match usize::try_from(requested) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit"));
|
||||
},
|
||||
};
|
||||
let requested_plus_one = match requested.checked_add(1) {
|
||||
std::option::Option::Some(value) => value,
|
||||
std::option::Option::None => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit"));
|
||||
},
|
||||
};
|
||||
let sql_limit = match i64::try_from(requested_plus_one) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => {
|
||||
return std::result::Result::Err(crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::PageLimitUnsupported, "raw_page_limit"));
|
||||
},
|
||||
};
|
||||
return std::result::Result::Ok((requested_usize, sql_limit));
|
||||
}
|
||||
|
||||
fn binding_digest(
|
||||
query: &ksp_store_api::RawTransactionQuery,
|
||||
last_slot: u64,
|
||||
last_signature: &[u8; 64],
|
||||
) -> std::result::Result<[u8; 32], crate::PostgresBackendError> {
|
||||
let direction = match query.direction() {
|
||||
ksp_store_api::RawSortDirection::Ascending => 0_u8,
|
||||
ksp_store_api::RawSortDirection::Descending => 1_u8,
|
||||
_ => return std::result::Result::Err(query_invalid("raw_cursor_direction")),
|
||||
};
|
||||
let network_len = match u64::try_from(query.network().as_str().len()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(_) => return std::result::Result::Err(query_invalid("raw_cursor_network")),
|
||||
};
|
||||
let mut hasher = sha2::Sha256::new();
|
||||
hasher.update(CURSOR_DOMAIN);
|
||||
hasher.update(network_len.to_be_bytes());
|
||||
hasher.update(query.network().as_str().as_bytes());
|
||||
hasher.update([direction]);
|
||||
hash_optional_slot(&mut hasher, query.slots().start_inclusive());
|
||||
hash_optional_slot(&mut hasher, query.slots().end_inclusive());
|
||||
hasher.update(last_slot.to_be_bytes());
|
||||
hasher.update(last_signature);
|
||||
let finalized = hasher.finalize();
|
||||
let mut digest = [0_u8; 32];
|
||||
digest.copy_from_slice(finalized.as_ref());
|
||||
return std::result::Result::Ok(digest);
|
||||
}
|
||||
|
||||
fn hash_optional_slot(hasher: &mut sha2::Sha256, value: std::option::Option<u64>) {
|
||||
match value {
|
||||
std::option::Option::Some(slot) => {
|
||||
hasher.update([1_u8]);
|
||||
hasher.update(slot.to_be_bytes());
|
||||
},
|
||||
std::option::Option::None => hasher.update([0_u8]),
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
fn validate_slot_in_range(range: ksp_store_api::RawSlotRange, slot: u64) -> std::result::Result<(), crate::PostgresBackendError> {
|
||||
if let std::option::Option::Some(start) = range.start_inclusive()
|
||||
&& slot < start
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_slot_range"));
|
||||
}
|
||||
if let std::option::Option::Some(end) = range.end_inclusive()
|
||||
&& slot > end
|
||||
{
|
||||
return std::result::Result::Err(query_invalid("raw_cursor_slot_range"));
|
||||
}
|
||||
return std::result::Result::Ok(());
|
||||
}
|
||||
|
||||
fn query_invalid(phase: &'static str) -> crate::PostgresBackendError {
|
||||
return crate::PostgresBackendError::new(crate::PostgresBackendErrorKind::QueryInvalid, phase);
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/src/runtime.rs
|
||||
// version: 7
|
||||
// version: 8
|
||||
|
||||
const APPLICATION_NAME: &str = "ksp-store";
|
||||
const MAX_CONNECTION_URI_BYTES: usize = 4_096;
|
||||
@@ -340,6 +340,14 @@ impl PostgresBackend {
|
||||
return crate::get_raw_transaction(&self.pool, &self.network, reference).await;
|
||||
}
|
||||
|
||||
/// Lists deterministic canonical RAW transaction references with a backend-owned opaque continuation cursor.
|
||||
pub async fn list_raw_transactions(
|
||||
&self,
|
||||
query: &ksp_store_api::RawTransactionQuery,
|
||||
) -> std::result::Result<ksp_store_api::RawPage<ksp_store_api::RawTransactionReference>, crate::PostgresBackendError> {
|
||||
return crate::list_raw_transactions(&self.pool, &self.network, query).await;
|
||||
}
|
||||
|
||||
/// Reads one persisted RAW transaction observation by producer-owned idempotence key.
|
||||
pub async fn get_raw_transaction_observation(
|
||||
&self,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/dependency_boundary.rs
|
||||
// version: 10
|
||||
// version: 11
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -168,9 +168,6 @@ fn pre_005_raw_write_sql_is_atomic_idempotent_and_keeps_later_scope_closed() {
|
||||
}
|
||||
for forbidden in [
|
||||
"DELETE FROM",
|
||||
"list_raw_transactions",
|
||||
"RawPage",
|
||||
"RawCursor",
|
||||
"transition_raw_transaction_retention",
|
||||
"impl ksp_store_api::RawTransactionWrite",
|
||||
"impl ksp_store_api::RawTransactionObservationWrite",
|
||||
@@ -179,3 +176,48 @@ fn pre_005_raw_write_sql_is_atomic_idempotent_and_keeps_later_scope_closed() {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_pagination_is_keyset_cursor_bound_and_policy_free() {
|
||||
let raw = include_str!("../src/raw_transaction.rs");
|
||||
let cursor = include_str!("../src/raw_transaction/cursor.rs");
|
||||
let index = include_str!("../migrations/v001_raw_transaction/indexes/001_ix_ksp_raw_transactions_slot_signature.sql");
|
||||
for required in [
|
||||
"LIST_TRANSACTIONS_ASC_SQL",
|
||||
"LIST_TRANSACTIONS_DESC_SQL",
|
||||
"retention_state <> 'purged'",
|
||||
"(slot, signature) >",
|
||||
"(slot, signature) <",
|
||||
"ORDER BY slot ASC, signature ASC",
|
||||
"ORDER BY slot DESC, signature DESC",
|
||||
"LIMIT $5",
|
||||
"list_raw_transactions",
|
||||
] {
|
||||
assert!(raw.contains(required), "missing pre.006 keyset pagination contract: {required}");
|
||||
}
|
||||
for required in [
|
||||
"CURSOR_BYTES: usize = 109",
|
||||
"CURSOR_MAGIC",
|
||||
"b\"KSPT\"",
|
||||
"CURSOR_VERSION: u8 = 1",
|
||||
"KSP/raw-transaction-cursor/v1",
|
||||
"sha2::Sha256",
|
||||
"query.network().as_str()",
|
||||
"query.direction()",
|
||||
"query.slots().start_inclusive()",
|
||||
"query.slots().end_inclusive()",
|
||||
"last_slot",
|
||||
"last_signature",
|
||||
"PageLimitUnsupported",
|
||||
"9_223_372_036_854_775_806",
|
||||
] {
|
||||
assert!(cursor.contains(required), "missing pre.006 cursor/binding contract: {required}");
|
||||
}
|
||||
assert!(index.contains("ON ksp_raw_transactions (slot, signature)"));
|
||||
assert!(index.contains("WHERE retention_state <> 'purged'"));
|
||||
for forbidden in [" OFFSET ", "limit.min(", "clamp(", "500", "1000", "DELETE FROM", "transition_raw_transaction_retention"] {
|
||||
assert!(!raw.contains(forbidden), "pre.006 contains forbidden pagination/policy/later-scope material: {forbidden}");
|
||||
assert!(!cursor.contains(forbidden), "pre.006 cursor contains forbidden pagination/policy/later-scope material: {forbidden}");
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/hardening_completeness.rs
|
||||
// version: 5
|
||||
// version: 6
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -168,7 +168,13 @@ fn pre_009_backend_error_bridge_cannot_retain_external_error_or_secret_text() {
|
||||
let runtime = include_str!("../src/runtime.rs");
|
||||
assert!(runtime.contains("deadpool_postgres::PoolError::Backend(_)"));
|
||||
assert!(!runtime.contains("deadpool_postgres::PoolError::Backend(error)"));
|
||||
for source in [runtime, include_str!("../src/migration.rs"), include_str!("../src/health.rs"), include_str!("../src/raw_transaction.rs")] {
|
||||
for source in [
|
||||
runtime,
|
||||
include_str!("../src/migration.rs"),
|
||||
include_str!("../src/health.rs"),
|
||||
include_str!("../src/raw_transaction.rs"),
|
||||
include_str!("../src/raw_transaction/cursor.rs"),
|
||||
] {
|
||||
for forbidden in ["format!(\"{error", "format!(\"{error:?", "error = ?", "error = %"] {
|
||||
assert!(!source.contains(forbidden), "backend source renders external error material: {forbidden}");
|
||||
}
|
||||
@@ -191,6 +197,7 @@ fn pre_009_backend_has_no_env_bypass_or_direct_store_trait_implementation() {
|
||||
include_str!("../src/lib.rs"),
|
||||
include_str!("../src/migration.rs"),
|
||||
include_str!("../src/raw_transaction.rs"),
|
||||
include_str!("../src/raw_transaction/cursor.rs"),
|
||||
include_str!("../src/runtime.rs"),
|
||||
include_str!("../src/schema.rs")
|
||||
);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/tests/public_api.rs
|
||||
// version: 6
|
||||
// version: 7
|
||||
|
||||
#![warn(missing_docs)]
|
||||
#![deny(unreachable_pub)]
|
||||
@@ -44,6 +44,8 @@ fn pre_005_backend_error_projection_is_safe_and_static() {
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::HealthFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::MigrationMismatch,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::PageLimitUnsupported,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::QueryInvalid,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ReadFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::ReferenceNotFound,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::SchemaNewer,
|
||||
@@ -52,7 +54,7 @@ fn pre_005_backend_error_projection_is_safe_and_static() {
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::WriteFailed,
|
||||
ksp_store_postgres_lib::PostgresBackendErrorKind::WrongNetwork,
|
||||
];
|
||||
assert_eq!(kinds.len(), 15);
|
||||
assert_eq!(kinds.len(), 17);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -87,3 +89,9 @@ fn pre_005_raw_write_bridge_uses_only_backend_independent_models_and_outcomes()
|
||||
let _observation = ksp_store_postgres_lib::PostgresBackend::record_raw_transaction_observation;
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_raw_list_bridge_uses_backend_independent_query_page_and_reference_models() {
|
||||
let _list = ksp_store_postgres_lib::PostgresBackend::list_raw_transactions;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// file: crates/ksp-store-postgres-lib/unit_tests/raw_transaction.rs
|
||||
// version: 2
|
||||
// version: 3
|
||||
|
||||
fn network() -> ksp_store_api::RawNetworkId {
|
||||
return match ksp_store_api::RawNetworkId::new("devnet") {
|
||||
@@ -353,3 +353,108 @@ fn pre_005_observation_origin_encoding_is_exact_and_static() {
|
||||
assert_eq!(super::encode_origin(ksp_store_api::RawAcquisitionOrigin::Replay), std::result::Result::Ok("replay"));
|
||||
return;
|
||||
}
|
||||
|
||||
fn page_query(
|
||||
network_id: &str,
|
||||
start: std::option::Option<u64>,
|
||||
end: std::option::Option<u64>,
|
||||
direction: ksp_store_api::RawSortDirection,
|
||||
cursor: std::option::Option<ksp_store_api::RawPageCursor>,
|
||||
) -> ksp_store_api::RawTransactionQuery {
|
||||
let network = match ksp_store_api::RawNetworkId::new(network_id) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid page-query network rejected: {error:?}"),
|
||||
};
|
||||
let range = match ksp_store_api::RawSlotRange::new(start, end) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid page-query range rejected: {error:?}"),
|
||||
};
|
||||
let limit = match ksp_store_api::RawPageLimit::new(2) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid page limit rejected: {error:?}"),
|
||||
};
|
||||
let page = match cursor {
|
||||
std::option::Option::Some(value) => ksp_store_api::RawPageRequest::after(limit, value),
|
||||
std::option::Option::None => ksp_store_api::RawPageRequest::first(limit),
|
||||
};
|
||||
return ksp_store_api::RawTransactionQuery::new(network, range, direction, page);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_cursor_v1_round_trip_is_fixed_109_bytes_and_exact() {
|
||||
let query = page_query("devnet", std::option::Option::Some(10), std::option::Option::Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
||||
let signature = ksp_store_api::RawTransactionSignature::new([7_u8; 64]);
|
||||
let cursor = match crate::encode_raw_transaction_cursor(&query, 15, &signature) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(cursor.as_bytes().len(), 109);
|
||||
assert_eq!(cursor.as_bytes().get(0..4), std::option::Option::Some(b"KSPT".as_ref()));
|
||||
assert_eq!(cursor.as_bytes().get(4).copied(), std::option::Option::Some(1));
|
||||
let decoded = match crate::decode_raw_transaction_cursor(&query, &cursor) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid cursor decoding rejected: {error:?}"),
|
||||
};
|
||||
assert_eq!(decoded.last_slot, 15);
|
||||
assert_eq!(decoded.last_signature, [7_u8; 64]);
|
||||
assert_eq!(std::format!("{cursor:?}"), "RawPageCursor { len: 109 }");
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_cursor_v1_rejects_replay_across_network_direction_and_range() {
|
||||
let query = page_query("devnet", std::option::Option::Some(10), std::option::Option::Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
||||
let signature = ksp_store_api::RawTransactionSignature::new([9_u8; 64]);
|
||||
let cursor = match crate::encode_raw_transaction_cursor(&query, 15, &signature) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
let other_network = page_query("mainnet-beta", Some(10), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
||||
let other_direction = page_query("devnet", Some(10), Some(20), ksp_store_api::RawSortDirection::Descending, None);
|
||||
let other_range = page_query("devnet", Some(11), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
||||
for candidate in [&other_network, &other_direction, &other_range] {
|
||||
let error = crate::decode_raw_transaction_cursor(candidate, &cursor).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
||||
}
|
||||
let outside_range = page_query("devnet", Some(16), Some(20), ksp_store_api::RawSortDirection::Ascending, None);
|
||||
let error = crate::decode_raw_transaction_cursor(&outside_range, &cursor).err();
|
||||
assert_eq!(error.map(|value| return value.phase()), Some("raw_cursor_slot_range"));
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_cursor_v1_hostile_size_magic_version_and_digest_are_rejected() {
|
||||
let query = page_query("devnet", None, None, ksp_store_api::RawSortDirection::Ascending, None);
|
||||
for size in [1_usize, 108, 109, 110, 4_096] {
|
||||
let raw = match ksp_store_api::RawPageCursor::try_new(vec![0_u8; size].into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("bounded hostile cursor rejected by API before backend test: {error:?}"),
|
||||
};
|
||||
assert!(crate::decode_raw_transaction_cursor(&query, &raw).is_err());
|
||||
}
|
||||
let signature = ksp_store_api::RawTransactionSignature::new([3_u8; 64]);
|
||||
let valid = match crate::encode_raw_transaction_cursor(&query, 42, &signature) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("valid cursor encoding rejected: {error:?}"),
|
||||
};
|
||||
for index in [0_usize, 4, 108] {
|
||||
let mut bytes = valid.as_bytes().to_vec();
|
||||
bytes[index] ^= 0xff;
|
||||
let hostile = match ksp_store_api::RawPageCursor::try_new(bytes.into_boxed_slice()) {
|
||||
std::result::Result::Ok(value) => value,
|
||||
std::result::Result::Err(error) => panic!("fixed-size hostile cursor rejected by API before backend test: {error:?}"),
|
||||
};
|
||||
let error = crate::decode_raw_transaction_cursor(&query, &hostile).err();
|
||||
assert_eq!(error.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::QueryInvalid));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pre_006_page_limit_exposes_only_the_real_postgres_limit_plus_one_boundary() {
|
||||
let maximum = crate::raw_transaction_physical_page_limit(9_223_372_036_854_775_806);
|
||||
assert_eq!(maximum, Ok((9_223_372_036_854_775_806_usize, i64::MAX)));
|
||||
let rejected = crate::raw_transaction_physical_page_limit(9_223_372_036_854_775_807).err();
|
||||
assert_eq!(rejected.map(|value| return value.kind()), Some(crate::PostgresBackendErrorKind::PageLimitUnsupported));
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user