Some checks failed
go: build dev binaries / cleanup (push) Successful in 4s
Codespell / Check for spelling errors (push) Failing after 3s
docker: build dev containers / build-rust-binaries (amd64, x86_64-unknown-linux-musl) (push) Failing after 3s
docker: build dev containers / build-dev-containers (push) Has been skipped
docker: build dev containers / build-rust-binaries (arm64, true, aarch64-unknown-linux-musl) (push) Failing after 3s
End to End / FUSE Mount (push) Failing after 2s
EC Integration Tests / EC Integration Tests (push) Failing after 30s
FUSE DLM Integration Tests / FUSE DLM Integration Tests (push) Failing after 3s
FUSE Volume Server Failover Tests / FUSE Volume Server Failover (push) Failing after 3s
FUSE Integration Tests / FUSE Integration Testing (push) Failing after 2s
FUSE P2P Peer Chunk Sharing Integration Tests / FUSE P2P Peer Chunk Sharing (push) Failing after 3s
go: build binary / Go Vet (push) Failing after 2s
go: build binary / Go Vet 32-bit (push) Failing after 3s
go: build binary / Build (push) Failing after 4s
go: build binary / Build cross-platform (freebsd/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/amd64) (push) Failing after 3s
go: build binary / Build cross-platform (darwin/arm64) (push) Failing after 3s
go: build binary / Build cross-platform (windows/arm64) (push) Failing after 3s
go: build binary / Test (push) Failing after 2s
go: build binary / Test 32-bit (push) Failing after 3s
helm: lint and test charts / lint-test (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (11) (push) Failing after 3s
Java Client Integration Tests / Java Integration Tests (17) (push) Failing after 3s
go: build dev binaries / build_dev_linux_windows (amd64, linux) (push) Failing after 2s
go: build dev binaries / build_dev_linux_windows (amd64, windows) (push) Failing after 3s
go: build dev binaries / build_dev_darwin (amd64, darwin) (push) Has been skipped
go: build dev binaries / build_dev_darwin (arm64, darwin) (push) Has been skipped
Java Client Unit Tests / Java Unit Tests (11) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (17) (push) Failing after 2s
Java Client Unit Tests / Java Unit Tests (21) (push) Failing after 3s
Java Client Unit Tests / Java Unit Tests (8) (push) Failing after 3s
Kafka Quick Test (Load Test with Schema Registry) / Kafka Client Load Test (Quick) (push) Failing after 3s
Kafka Gateway Tests / Kafka Unit Tests (unit-tests-1) (push) Failing after 11s
Kafka Gateway Tests / Kafka Integration Tests (Critical) (integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka End-to-End Tests (with SMQ) (e2e-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Consumer Group Tests (Highly Isolated) (consumer-group-1) (push) Failing after 2s
Kafka Gateway Tests / Kafka Client Compatibility (with SMQ) (client-compat-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka SMQ Integration Tests (Full Stack) (smq-integration-1) (push) Failing after 3s
Kafka Gateway Tests / Kafka Protocol Tests (Isolated) (protocol-1) (push) Failing after 3s
KMS Tests / KMS Provider Integration Tests (push) Failing after 2s
KMS Tests / S3 KMS End-to-End Tests (push) Failing after 3s
Master Cold Start Tests / Master Cold Start Tests (push) Failing after 4s
Metadata Subscribe Integration Tests / Metadata Subscribe Integration Tests (push) Failing after 3s
Multi-Master Tests / Multi-Master Failover Tests (push) Failing after 3s
Performance / CPU and Heap Profile (push) Failing after 2s
Performance / S3 Read/Write Benchmark (rust volume) (push) Failing after 3s
pjdfstest POSIX Compliance / pjdfstest (push) Failing after 2s
Plugin Worker Integration Tests / Plugin Worker: erasure_coding (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: vacuum (push) Failing after 3s
Rust Volume Server Tests / Rust Integration Tests (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 1) (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 1) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 2) (push) Failing after 3s
Rust Volume Server Tests / Go Tests with Rust Volume (grpc - Shard 3) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Distributed Tests (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Performance Tests (push) Failing after 3s
Performance / Throughput Benchmark (go volume) (push) Failing after 3s
Performance / Throughput Benchmark (rust volume) (push) Failing after 2s
Performance / S3 Read/Write Benchmark (go volume) (push) Failing after 3s
Plugin Worker Integration Tests / Plugin Worker: volume_balance (push) Failing after 3s
PostgreSQL Gateway Tests / PostgreSQL Basic Tests (push) Failing after 3s
Rust Volume Server Tests / Rust Unit Tests (push) Failing after 2s
Rust Volume Server Tests / Go Tests with Rust Volume (http - Shard 3) (push) Failing after 3s
Rust Plugin Worker Tests / Rust Plugin Worker Build and Unit Tests (push) Failing after 3s
rust: build dev volume server binaries / cleanup (push) Successful in 3s
S3 ETag and ACL Tests / S3 ETag + ACL Integration Tests (push) Failing after 3s
S3 IAM Integration Tests / IAM Unit Tests (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (advanced) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (basic) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (group) (push) Failing after 3s
S3 IAM Integration Tests / S3 IAM Integration Tests (policy-enforcement) (push) Failing after 2s
S3 IAM Integration Tests / S3 IAM Integration Tests (sts) (push) Failing after 4s
S3 Keycloak Integration Tests / S3 Keycloak Integration Tests (push) Failing after 3s
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.11) (push) Failing after 3s
S3 PyArrow Parquet Tests / Go Unit Tests (Implicit Directory) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.12) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-amd64, x86_64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-darwin (darwin-arm64, aarch64-apple-darwin) (push) Has been cancelled
rust: build dev volume server binaries / build-rust-volume-dev-linux (linux-amd64, x86_64-unknown-linux-gnu) (push) Has been cancelled
S3 PyArrow Parquet Tests / PyArrow Parquet Tests (Python 3.9) (push) Has been cancelled
S3 Proxy Signature Tests / S3 Proxy Signature Verification Tests (push) Has been cancelled
S3 SDK V2 Route Disambiguation Tests / S3 SDK V2 Routing Tests (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (quick) (push) Has been cancelled
S3 SSE Tests / S3 SSE Compatibility Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Metadata Persistence Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Copy Operations Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Multipart Upload Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Performance Test (push) Has been cancelled
S3 SSE Tests / S3 Volume Encryption Test (push) Has been cancelled
S3 SSE Tests / S3 SSE Integration Tests (comprehensive) (push) Has been cancelled
Spark Integration Tests / Spark Integration Tests (push) Failing after 2s
Telemetry Integration Tests / telemetry-integration-test (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 1) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 1) (push) Failing after 3s
Ceph S3 tests / Basic S3 tests (KV store) (push) Has been cancelled
Ceph S3 tests / S3 Versioning & Object Lock tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Unit Tests (push) Failing after 3s
Ceph S3 tests / S3 CORS tests (push) Has been cancelled
Ceph S3 tests / SeaweedFS Custom S3 Copy tests (push) Has been cancelled
Ceph S3 tests / Basic S3 tests (SQL store) (push) Has been cancelled
test s3 over https using aws-cli / awscli-tests (push) Has been cancelled
TLS Rotation Integration Tests / TLS Rotation Integration Tests (push) Has been cancelled
Vacuum Integration Tests / Vacuum Integration Tests (push) Has been cancelled
S3 Policy Integration Tests / S3 Policy Variables Integration Tests (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (advanced-policy) (push) Failing after 3s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (basic-policy) (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Enforcement Integration Tests (contextual-policy) (push) Failing after 4s
S3 Policy Integration Tests / Trusted Proxy Detection Tests (push) Failing after 2s
S3 Policy Integration Tests / S3 Policy Shell Integration Tests (push) Failing after 3s
Samba on FUSE Integration / samba-integration (push) Failing after 2s
SFTP Integration Tests / SFTP Integration Testing (push) Failing after 3s
terraform: validate and test modules / fmt, validate, plan-level tests (push) Failing after 3s
terraform: validate and test modules / local cluster smoke test (real weed) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 3) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (grpc - Shard 2) (push) Failing after 3s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 2) (push) Failing after 2s
Volume Server Integration Tests / Volume Server Integration Tests (http - Shard 3) (push) Failing after 5s
mount: windows conformance / WinFsp conformance (push) Has been cancelled
mount: windows / Mount on Windows (push) Has been cancelled
505 lines
17 KiB
Rust
505 lines
17 KiB
Rust
//! Drives the sort handler against a live namespace.
|
|
//!
|
|
//! Skipped unless WEED_LANCE_NAMESPACE names one, like the other integration
|
|
//! tests here: the job rewrites every fragment of a real table and commits, and
|
|
//! the commit is the half worth testing.
|
|
|
|
use anyhow::Result;
|
|
use seaweed_worker_core::JobHandler;
|
|
use seaweed_worker_core::pb::{
|
|
ConfigValue, ExecuteJobRequest, JobSpec, RunDetectionRequest, config_value::Kind,
|
|
};
|
|
use weed_lance_worker::catalog::NamespaceClient;
|
|
use weed_lance_worker::jobs::sort::{JOB_TYPE, SortHandler};
|
|
|
|
mod common;
|
|
use common::{Recorder, fallback, namespace_url};
|
|
|
|
/// One live gateway and one shared catalog, and `list_all_tables` sweeps
|
|
/// everything, so these tests take a lock the way the compaction ones do.
|
|
static GATEWAY: tokio::sync::Mutex<()> = tokio::sync::Mutex::const_new(());
|
|
|
|
fn config(values: &[(&str, ConfigValue)]) -> std::collections::HashMap<String, ConfigValue> {
|
|
values
|
|
.iter()
|
|
.map(|(name, value)| (name.to_string(), value.clone()))
|
|
.collect()
|
|
}
|
|
|
|
fn int(value: i64) -> ConfigValue {
|
|
ConfigValue {
|
|
kind: Some(Kind::Int64Value(value)),
|
|
}
|
|
}
|
|
|
|
fn text(value: &str) -> ConfigValue {
|
|
ConfigValue {
|
|
kind: Some(Kind::StringValue(value.to_string())),
|
|
}
|
|
}
|
|
|
|
/// Declares a table and writes `batches` appends whose ids descend, so the rows
|
|
/// land in an order no sort would produce. `declared` is written into the
|
|
/// dataset's own configuration, which is where a table says how it wants to be
|
|
/// sorted.
|
|
async fn seed_unsorted_table(
|
|
url: &str,
|
|
name: &str,
|
|
batches: i64,
|
|
rows_each: i64,
|
|
declared: Option<&str>,
|
|
) -> Result<String> {
|
|
use arrow_array::{Int64Array, RecordBatch, RecordBatchIterator};
|
|
use arrow_schema::{DataType, Field, Schema};
|
|
use lance::dataset::{Dataset, WriteMode, WriteParams};
|
|
use lance::io::{ObjectStoreParams, StorageOptionsAccessor};
|
|
use std::sync::Arc;
|
|
|
|
let http = reqwest::Client::new();
|
|
for parent in ["vec", "vec$ml"] {
|
|
http.post(format!("{url}/v1/namespace/{parent}/create"))
|
|
.json(&serde_json::json!({"mode": "EXIST_OK"}))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
}
|
|
let encoded = format!("vec$ml${name}");
|
|
http.post(format!("{url}/v1/table/{encoded}/declare"))
|
|
.json(&serde_json::json!({}))
|
|
.send()
|
|
.await?
|
|
.error_for_status()?;
|
|
|
|
let client = NamespaceClient::new(url.to_string());
|
|
let id = vec!["vec".to_string(), "ml".to_string(), name.to_string()];
|
|
let description = client.describe_table(&id).await?;
|
|
let mut options = description.storage_options.clone();
|
|
options.extend(fallback());
|
|
|
|
let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)]));
|
|
let total = batches * rows_each;
|
|
let mut dataset = None;
|
|
for batch_index in 0..batches {
|
|
// Ids descend across and within the batches, so the physical order is
|
|
// the reverse of the order the table declares.
|
|
let ids: Vec<i64> = (0..rows_each)
|
|
.map(|row| total - 1 - (batch_index * rows_each + row))
|
|
.collect();
|
|
let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(Int64Array::from(ids))])?;
|
|
let params = WriteParams {
|
|
mode: if batch_index == 0 {
|
|
WriteMode::Overwrite
|
|
} else {
|
|
WriteMode::Append
|
|
},
|
|
store_params: Some(ObjectStoreParams {
|
|
storage_options_accessor: Some(Arc::new(
|
|
StorageOptionsAccessor::with_static_options(options.clone()),
|
|
)),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
dataset = Some(
|
|
Dataset::write(
|
|
RecordBatchIterator::new(vec![Ok(batch)], schema.clone()),
|
|
description.location.as_str(),
|
|
Some(params),
|
|
)
|
|
.await?,
|
|
);
|
|
}
|
|
|
|
if let Some(declared) = declared {
|
|
let mut dataset = dataset.expect("at least one batch was written");
|
|
dataset
|
|
.update_config([("seaweedfs.sort.fields", declared)])
|
|
.await?;
|
|
}
|
|
Ok(encoded)
|
|
}
|
|
|
|
/// Reads the ids back in the order they are stored, which is what a sort is
|
|
/// supposed to change.
|
|
async fn stored_ids(url: &str, name: &str) -> Result<Vec<i64>> {
|
|
use arrow_array::Int64Array;
|
|
use futures::TryStreamExt;
|
|
|
|
let client = NamespaceClient::new(url.to_string());
|
|
let id = vec!["vec".to_string(), "ml".to_string(), name.to_string()];
|
|
let table = weed_lance_worker::dataset::open(&client, &id, &fallback()).await?;
|
|
let stream = table.dataset.scan().try_into_stream().await?;
|
|
let batches: Vec<_> = stream.try_collect().await?;
|
|
let mut ids = Vec::new();
|
|
for batch in batches {
|
|
let column = batch
|
|
.column_by_name("id")
|
|
.expect("the seeded table has an id column")
|
|
.as_any()
|
|
.downcast_ref::<Int64Array>()
|
|
.expect("id is an Int64 column")
|
|
.clone();
|
|
ids.extend(column.values().iter().copied());
|
|
}
|
|
Ok(ids)
|
|
}
|
|
|
|
/// The whole loop: a table declaring an order its rows do not follow is
|
|
/// proposed, sorting it stores the rows in that order, and the marker the same
|
|
/// commit carries is what stops the next sweep proposing it again.
|
|
#[tokio::test]
|
|
async fn sorts_a_table_and_stops_proposing_it() {
|
|
let _gateway = GATEWAY.lock().await;
|
|
let Some(url) = namespace_url() else {
|
|
eprintln!("WEED_LANCE_NAMESPACE is unset, skipping");
|
|
return;
|
|
};
|
|
let encoded = seed_unsorted_table(&url, "sortme", 4, 250, Some("id asc"))
|
|
.await
|
|
.expect("seed an unsorted table");
|
|
|
|
let before = stored_ids(&url, "sortme").await.expect("read the ids back");
|
|
assert!(
|
|
before.windows(2).any(|pair| pair[0] > pair[1]),
|
|
"the seeded table was already sorted, so the test proves nothing"
|
|
);
|
|
|
|
let handler = SortHandler::new(url.clone()).with_fallback(fallback());
|
|
let recorder = Recorder::default();
|
|
let detection = RunDetectionRequest {
|
|
request_id: "detect-sort".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
worker_config_values: config(&[("min_unsorted_rows", int(1))]),
|
|
..Default::default()
|
|
};
|
|
handler
|
|
.detect(&detection, &recorder)
|
|
.await
|
|
.expect("detection failed");
|
|
|
|
let proposal = recorder
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|p| p.summary.contains(encoded.as_str()))
|
|
.cloned()
|
|
.expect("the unsorted table was not proposed");
|
|
assert!(
|
|
proposal.summary.contains("id asc"),
|
|
"the proposal does not say what order it would use: {}",
|
|
proposal.summary
|
|
);
|
|
|
|
let execute = ExecuteJobRequest {
|
|
request_id: "execute-sort".to_string(),
|
|
job: Some(JobSpec {
|
|
job_id: "job-sort".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
parameters: proposal.parameters.clone(),
|
|
..Default::default()
|
|
}),
|
|
worker_config_values: config(&[
|
|
("memory_budget_mb", int(64)),
|
|
("max_rows_per_file", int(1024)),
|
|
]),
|
|
..Default::default()
|
|
};
|
|
handler
|
|
.execute(&execute, &recorder)
|
|
.await
|
|
.expect("sorting failed");
|
|
|
|
let completed = recorder.completed.lock().unwrap().clone();
|
|
let result = completed.last().expect("no completion reported");
|
|
assert!(
|
|
result.success,
|
|
"the sort reported failure: {}",
|
|
result.error_message
|
|
);
|
|
|
|
let after = stored_ids(&url, "sortme")
|
|
.await
|
|
.expect("read the sorted ids back");
|
|
assert_eq!(after.len(), before.len(), "the sort lost or invented rows");
|
|
assert!(
|
|
after.windows(2).all(|pair| pair[0] <= pair[1]),
|
|
"the rows are not stored in ascending id order"
|
|
);
|
|
|
|
// The marker rode in the same commit as the data, so a second sweep finds
|
|
// the table up to date without anything else having written it.
|
|
let second = Recorder::default();
|
|
handler
|
|
.detect(&detection, &second)
|
|
.await
|
|
.expect("second detection failed");
|
|
assert!(
|
|
!second
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p.summary.contains(encoded.as_str())),
|
|
"a table that was just sorted must not be proposed again"
|
|
);
|
|
eprintln!("sort result: {}", result.result.as_ref().unwrap().summary);
|
|
}
|
|
|
|
/// A table that declares no order, with a worker configuring none either, is
|
|
/// not something to rewrite on a guess.
|
|
#[tokio::test]
|
|
async fn leaves_a_table_that_declares_no_order_alone() {
|
|
let _gateway = GATEWAY.lock().await;
|
|
let Some(url) = namespace_url() else {
|
|
eprintln!("WEED_LANCE_NAMESPACE is unset, skipping");
|
|
return;
|
|
};
|
|
let encoded = seed_unsorted_table(&url, "unsorted", 2, 16, None)
|
|
.await
|
|
.expect("seed a table with no declared order");
|
|
|
|
let handler = SortHandler::new(url.clone()).with_fallback(fallback());
|
|
let recorder = Recorder::default();
|
|
handler
|
|
.detect(
|
|
&RunDetectionRequest {
|
|
request_id: "detect-none".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
worker_config_values: config(&[
|
|
("min_unsorted_rows", int(1)),
|
|
("sort_fields", text("")),
|
|
]),
|
|
..Default::default()
|
|
},
|
|
&recorder,
|
|
)
|
|
.await
|
|
.expect("detection failed");
|
|
|
|
assert!(
|
|
!recorder
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p.summary.contains(encoded.as_str())),
|
|
"a table with no order named anywhere must not be proposed"
|
|
);
|
|
}
|
|
|
|
/// The case a row-count threshold cannot see: the table replaced by a write of
|
|
/// its own, growing by far less than `min_unsorted_rows`. Every row moved, so
|
|
/// the table is unsorted, and detection has to say so however small the delta.
|
|
#[tokio::test]
|
|
async fn proposes_a_replacement_that_barely_changes_the_row_count() {
|
|
let _gateway = GATEWAY.lock().await;
|
|
let Some(url) = namespace_url() else {
|
|
eprintln!("WEED_LANCE_NAMESPACE is unset, skipping");
|
|
return;
|
|
};
|
|
let encoded = seed_unsorted_table(&url, "replaceme", 4, 250, Some("id asc"))
|
|
.await
|
|
.expect("seed an unsorted table");
|
|
|
|
let handler = SortHandler::new(url.clone()).with_fallback(fallback());
|
|
// A threshold no realistic append would cross, so nothing but the identity
|
|
// of the data can be what proposes this table.
|
|
let detection = RunDetectionRequest {
|
|
request_id: "detect-replace".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
worker_config_values: config(&[("min_unsorted_rows", int(1_000_000))]),
|
|
..Default::default()
|
|
};
|
|
|
|
let first = Recorder::default();
|
|
handler.detect(&detection, &first).await.expect("detection");
|
|
let proposal = first
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|p| p.summary.contains(encoded.as_str()))
|
|
.cloned()
|
|
.expect("a never-sorted table was not proposed");
|
|
|
|
handler
|
|
.execute(
|
|
&ExecuteJobRequest {
|
|
request_id: "execute-replace".to_string(),
|
|
job: Some(JobSpec {
|
|
job_id: "job-replace".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
parameters: proposal.parameters.clone(),
|
|
..Default::default()
|
|
}),
|
|
worker_config_values: config(&[("max_rows_per_file", int(1024))]),
|
|
..Default::default()
|
|
},
|
|
&first,
|
|
)
|
|
.await
|
|
.expect("sorting failed");
|
|
|
|
// Sorted, and left alone by the next sweep.
|
|
let after_sort = Recorder::default();
|
|
handler
|
|
.detect(&detection, &after_sort)
|
|
.await
|
|
.expect("detection");
|
|
assert!(
|
|
!after_sort
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p.summary.contains(encoded.as_str())),
|
|
"a table that was just sorted must not be proposed again"
|
|
);
|
|
|
|
// Now replace the data: 1002 rows where there were 1000, shuffled again.
|
|
seed_unsorted_table(&url, "replaceme", 3, 334, Some("id asc"))
|
|
.await
|
|
.expect("replace the table's data");
|
|
|
|
let after_replace = Recorder::default();
|
|
handler
|
|
.detect(&detection, &after_replace)
|
|
.await
|
|
.expect("detection");
|
|
assert!(
|
|
after_replace
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.any(|p| p.summary.contains(encoded.as_str())),
|
|
"a replaced table must be proposed even when the row count barely moved"
|
|
);
|
|
}
|
|
|
|
/// Deletions must not hide appended rows. This is the arithmetic against a real
|
|
/// manifest rather than a hand-built one: the sorted fragments keep their files
|
|
/// and lose most of their rows, a smaller batch is appended after them, and the
|
|
/// table has to be proposed *because rows were appended* — not because it looks
|
|
/// replaced, and not left alone because it now holds fewer rows than when it
|
|
/// was sorted.
|
|
#[tokio::test]
|
|
async fn deletions_do_not_hide_appended_rows() {
|
|
let _gateway = GATEWAY.lock().await;
|
|
let Some(url) = namespace_url() else {
|
|
eprintln!("WEED_LANCE_NAMESPACE is unset, skipping");
|
|
return;
|
|
};
|
|
let encoded = seed_unsorted_table(&url, "deleteme", 4, 250, Some("id asc"))
|
|
.await
|
|
.expect("seed an unsorted table");
|
|
|
|
let handler = SortHandler::new(url.clone()).with_fallback(fallback());
|
|
let detection = RunDetectionRequest {
|
|
request_id: "detect-delete".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
worker_config_values: config(&[("min_unsorted_rows", int(100))]),
|
|
..Default::default()
|
|
};
|
|
|
|
let recorder = Recorder::default();
|
|
handler
|
|
.detect(&detection, &recorder)
|
|
.await
|
|
.expect("detection");
|
|
let proposal = recorder
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|p| p.summary.contains(encoded.as_str()))
|
|
.cloned()
|
|
.expect("a never-sorted table was not proposed");
|
|
handler
|
|
.execute(
|
|
&ExecuteJobRequest {
|
|
request_id: "execute-delete".to_string(),
|
|
job: Some(JobSpec {
|
|
job_id: "job-delete".to_string(),
|
|
job_type: JOB_TYPE.to_string(),
|
|
parameters: proposal.parameters.clone(),
|
|
..Default::default()
|
|
}),
|
|
worker_config_values: config(&[("max_rows_per_file", int(1024))]),
|
|
..Default::default()
|
|
},
|
|
&recorder,
|
|
)
|
|
.await
|
|
.expect("sorting failed");
|
|
|
|
// Delete 900 of the 1000 sorted rows, then append 200 unsorted ones. The
|
|
// table is smaller than when it was sorted, so net growth reads as zero.
|
|
let client = NamespaceClient::new(url.clone());
|
|
let id = vec!["vec".to_string(), "ml".to_string(), "deleteme".to_string()];
|
|
let mut table = weed_lance_worker::dataset::open(&client, &id, &fallback())
|
|
.await
|
|
.expect("reopen the sorted table");
|
|
table
|
|
.dataset
|
|
.delete("id < 900")
|
|
.await
|
|
.expect("delete most of the sorted rows");
|
|
append_ids(&url, "deleteme", 5_000, 200)
|
|
.await
|
|
.expect("append rows after the sorted ones");
|
|
|
|
let after = Recorder::default();
|
|
handler.detect(&detection, &after).await.expect("detection");
|
|
let proposal = after
|
|
.proposals
|
|
.lock()
|
|
.unwrap()
|
|
.iter()
|
|
.find(|p| p.summary.contains(encoded.as_str()))
|
|
.cloned()
|
|
.expect("a table with 200 unsorted rows appended was not proposed");
|
|
assert!(
|
|
proposal.detail.contains("200 rows appended"),
|
|
"expected the appended rows to be counted, got {:?}",
|
|
proposal.detail
|
|
);
|
|
}
|
|
|
|
/// Appends one batch of ascending ids to an existing table.
|
|
async fn append_ids(url: &str, name: &str, first_id: i64, rows: i64) -> Result<()> {
|
|
use arrow_array::{Int64Array, RecordBatch, RecordBatchIterator};
|
|
use arrow_schema::{DataType, Field, Schema};
|
|
use lance::dataset::{Dataset, WriteMode, WriteParams};
|
|
use lance::io::{ObjectStoreParams, StorageOptionsAccessor};
|
|
use std::sync::Arc;
|
|
|
|
let client = NamespaceClient::new(url.to_string());
|
|
let id = vec!["vec".to_string(), "ml".to_string(), name.to_string()];
|
|
let description = client.describe_table(&id).await?;
|
|
let mut options = description.storage_options.clone();
|
|
options.extend(fallback());
|
|
|
|
let schema = Arc::new(Schema::new(vec![Field::new("id", DataType::Int64, false)]));
|
|
let ids: Vec<i64> = (0..rows).map(|row| first_id + row).collect();
|
|
let batch = RecordBatch::try_new(schema.clone(), vec![Arc::new(Int64Array::from(ids))])?;
|
|
let params = WriteParams {
|
|
mode: WriteMode::Append,
|
|
store_params: Some(ObjectStoreParams {
|
|
storage_options_accessor: Some(Arc::new(StorageOptionsAccessor::with_static_options(
|
|
options,
|
|
))),
|
|
..Default::default()
|
|
}),
|
|
..Default::default()
|
|
};
|
|
Dataset::write(
|
|
RecordBatchIterator::new(vec![Ok(batch)], schema.clone()),
|
|
description.location.as_str(),
|
|
Some(params),
|
|
)
|
|
.await?;
|
|
Ok(())
|
|
}
|