|
22 | 22 | mod common; |
23 | 23 |
|
24 | 24 | use arrow_array::{Array, BinaryArray, Int32Array, RecordBatch, StringArray}; |
25 | | -use common::{create_sql_context, create_test_env, exec}; |
| 25 | +use common::{assert_sql_error, create_sql_context, create_test_env, exec}; |
26 | 26 | use paimon::catalog::Identifier; |
27 | 27 | use paimon::spec::{BlobDescriptor, BlobViewStruct}; |
28 | 28 | use paimon::table::BranchManager; |
@@ -577,6 +577,66 @@ async fn test_blob_resolve_descriptor_with_offset() { |
577 | 577 | assert_eq!(rows[0], (1, "Partial".into(), Some(b"PAYLOAD".to_vec()))); |
578 | 578 | } |
579 | 579 |
|
| 580 | +#[tokio::test] |
| 581 | +async fn test_blob_resolve_unknown_length_descriptor() { |
| 582 | + let (tmp, sql_context) = setup(BLOB_TABLE_DDL).await; |
| 583 | + |
| 584 | + let source_data = b"HEADER_PAYLOAD_TRAILER"; |
| 585 | + let source_path = tmp.path().join("blob_unknown_length.bin"); |
| 586 | + std::fs::write(&source_path, source_data).unwrap(); |
| 587 | + |
| 588 | + let uri = format!("file://{}", source_path.display()); |
| 589 | + let full_hex = to_hex(&BlobDescriptor::new(uri.clone(), 0, -1).serialize()); |
| 590 | + let suffix_hex = to_hex(&BlobDescriptor::new(uri.clone(), 7, -1).serialize()); |
| 591 | + let eof_hex = |
| 592 | + to_hex(&BlobDescriptor::new(uri.clone(), source_data.len() as i64, -1).serialize()); |
| 593 | + let past_eof_hex = |
| 594 | + to_hex(&BlobDescriptor::new(uri, source_data.len() as i64 + 5, -1).serialize()); |
| 595 | + |
| 596 | + let sql = format!( |
| 597 | + "INSERT INTO paimon.test_db.t (id, name, picture) VALUES \ |
| 598 | + (1, 'Full', X'{full_hex}'), \ |
| 599 | + (2, 'Suffix', X'{suffix_hex}'), \ |
| 600 | + (3, 'Eof', X'{eof_hex}'), \ |
| 601 | + (4, 'PastEof', X'{past_eof_hex}'), \ |
| 602 | + (5, 'Raw', X'524157'), \ |
| 603 | + (6, 'Null', NULL)" |
| 604 | + ); |
| 605 | + exec(&sql_context, &sql).await; |
| 606 | + |
| 607 | + let rows = query_id_name_picture( |
| 608 | + &sql_context, |
| 609 | + "SELECT id, name, picture FROM paimon.test_db.t ORDER BY id", |
| 610 | + ) |
| 611 | + .await; |
| 612 | + assert_eq!( |
| 613 | + rows, |
| 614 | + vec![ |
| 615 | + (1, "Full".into(), Some(source_data.to_vec())), |
| 616 | + (2, "Suffix".into(), Some(b"PAYLOAD_TRAILER".to_vec())), |
| 617 | + (3, "Eof".into(), Some(Vec::new())), |
| 618 | + (4, "PastEof".into(), Some(Vec::new())), |
| 619 | + (5, "Raw".into(), Some(b"RAW".to_vec())), |
| 620 | + (6, "Null".into(), None), |
| 621 | + ] |
| 622 | + ); |
| 623 | +} |
| 624 | + |
| 625 | +#[tokio::test] |
| 626 | +async fn test_blob_descriptor_short_read_returns_error() { |
| 627 | + let (tmp, sql_context) = setup(BLOB_TABLE_DDL).await; |
| 628 | + |
| 629 | + let source_path = tmp.path().join("blob_short_read.bin"); |
| 630 | + std::fs::write(&source_path, b"short").unwrap(); |
| 631 | + let uri = format!("file://{}", source_path.display()); |
| 632 | + let desc_hex = to_hex(&BlobDescriptor::new(uri, 0, 6).serialize()); |
| 633 | + let sql = format!( |
| 634 | + "INSERT INTO paimon.test_db.t (id, name, picture) VALUES (1, 'Short', X'{desc_hex}')" |
| 635 | + ); |
| 636 | + |
| 637 | + assert_sql_error(&sql_context, &sql, "Failed to read BlobDescriptor").await; |
| 638 | +} |
| 639 | + |
580 | 640 | /// Blob files roll independently when `blob.target-file-size` is small. |
581 | 641 | #[tokio::test] |
582 | 642 | async fn test_blob_rolling() { |
@@ -709,6 +769,123 @@ async fn test_blob_descriptor_field_resolve_descriptor_value() { |
709 | 769 | ); |
710 | 770 | } |
711 | 771 |
|
| 772 | +#[tokio::test] |
| 773 | +async fn test_blob_descriptor_field_resolve_unknown_length_descriptor() { |
| 774 | + let (tmp, sql_context) = setup( |
| 775 | + "CREATE TABLE paimon.test_db.t (\ |
| 776 | + id INT, \ |
| 777 | + name STRING, \ |
| 778 | + picture BLOB \ |
| 779 | + ) WITH (\ |
| 780 | + 'data-evolution.enabled' = 'true', \ |
| 781 | + 'row-tracking.enabled' = 'true', \ |
| 782 | + 'blob-descriptor-field' = 'picture'\ |
| 783 | + )", |
| 784 | + ) |
| 785 | + .await; |
| 786 | + |
| 787 | + let source_data = b"HEADER_PAYLOAD_TRAILER"; |
| 788 | + let source_path = tmp.path().join("descriptor_unknown_length.bin"); |
| 789 | + std::fs::write(&source_path, source_data).unwrap(); |
| 790 | + |
| 791 | + let uri = format!("file://{}", source_path.display()); |
| 792 | + let bounded_hex = to_hex(&BlobDescriptor::new(uri.clone(), 0, 6).serialize()); |
| 793 | + let suffix_hex = to_hex(&BlobDescriptor::new(uri.clone(), 7, -1).serialize()); |
| 794 | + let eof_hex = |
| 795 | + to_hex(&BlobDescriptor::new(uri.clone(), source_data.len() as i64, -1).serialize()); |
| 796 | + let past_eof_hex = |
| 797 | + to_hex(&BlobDescriptor::new(uri, source_data.len() as i64 + 5, -1).serialize()); |
| 798 | + let sql = format!( |
| 799 | + "INSERT INTO paimon.test_db.t (id, name, picture) VALUES \ |
| 800 | + (1, 'Bounded', X'{bounded_hex}'), \ |
| 801 | + (2, 'Suffix', X'{suffix_hex}'), \ |
| 802 | + (3, 'Eof', X'{eof_hex}'), \ |
| 803 | + (4, 'PastEof', X'{past_eof_hex}'), \ |
| 804 | + (5, 'Raw', X'524157'), \ |
| 805 | + (6, 'Null', NULL)" |
| 806 | + ); |
| 807 | + exec(&sql_context, &sql).await; |
| 808 | + |
| 809 | + let rows = query_id_name_picture( |
| 810 | + &sql_context, |
| 811 | + "SELECT id, name, picture FROM paimon.test_db.t ORDER BY id", |
| 812 | + ) |
| 813 | + .await; |
| 814 | + assert_eq!( |
| 815 | + rows, |
| 816 | + vec![ |
| 817 | + (1, "Bounded".into(), Some(b"HEADER".to_vec())), |
| 818 | + (2, "Suffix".into(), Some(b"PAYLOAD_TRAILER".to_vec())), |
| 819 | + (3, "Eof".into(), Some(Vec::new())), |
| 820 | + (4, "PastEof".into(), Some(Vec::new())), |
| 821 | + (5, "Raw".into(), Some(b"RAW".to_vec())), |
| 822 | + (6, "Null".into(), None), |
| 823 | + ] |
| 824 | + ); |
| 825 | +} |
| 826 | + |
| 827 | +#[tokio::test] |
| 828 | +async fn test_blob_descriptor_field_rejects_invalid_length() { |
| 829 | + let (tmp, sql_context) = setup( |
| 830 | + "CREATE TABLE paimon.test_db.t (\ |
| 831 | + id INT, \ |
| 832 | + name STRING, \ |
| 833 | + picture BLOB \ |
| 834 | + ) WITH (\ |
| 835 | + 'data-evolution.enabled' = 'true', \ |
| 836 | + 'row-tracking.enabled' = 'true', \ |
| 837 | + 'blob-descriptor-field' = 'picture'\ |
| 838 | + )", |
| 839 | + ) |
| 840 | + .await; |
| 841 | + |
| 842 | + let uri = format!("file://{}", tmp.path().join("unused.bin").display()); |
| 843 | + let desc_hex = to_hex(&BlobDescriptor::new(uri, 0, -2).serialize()); |
| 844 | + let sql = format!( |
| 845 | + "INSERT INTO paimon.test_db.t (id, name, picture) VALUES (1, 'Invalid', X'{desc_hex}')" |
| 846 | + ); |
| 847 | + exec(&sql_context, &sql).await; |
| 848 | + |
| 849 | + assert_sql_error( |
| 850 | + &sql_context, |
| 851 | + "SELECT id, name, picture FROM paimon.test_db.t", |
| 852 | + "length must be -1 or non-negative", |
| 853 | + ) |
| 854 | + .await; |
| 855 | +} |
| 856 | + |
| 857 | +#[tokio::test] |
| 858 | +async fn test_blob_descriptor_field_short_read_returns_error() { |
| 859 | + let (tmp, sql_context) = setup( |
| 860 | + "CREATE TABLE paimon.test_db.t (\ |
| 861 | + id INT, \ |
| 862 | + name STRING, \ |
| 863 | + picture BLOB \ |
| 864 | + ) WITH (\ |
| 865 | + 'data-evolution.enabled' = 'true', \ |
| 866 | + 'row-tracking.enabled' = 'true', \ |
| 867 | + 'blob-descriptor-field' = 'picture'\ |
| 868 | + )", |
| 869 | + ) |
| 870 | + .await; |
| 871 | + |
| 872 | + let source_path = tmp.path().join("descriptor_short_read.bin"); |
| 873 | + std::fs::write(&source_path, b"short").unwrap(); |
| 874 | + let uri = format!("file://{}", source_path.display()); |
| 875 | + let desc_hex = to_hex(&BlobDescriptor::new(uri, 0, 6).serialize()); |
| 876 | + let sql = format!( |
| 877 | + "INSERT INTO paimon.test_db.t (id, name, picture) VALUES (1, 'Short', X'{desc_hex}')" |
| 878 | + ); |
| 879 | + exec(&sql_context, &sql).await; |
| 880 | + |
| 881 | + assert_sql_error( |
| 882 | + &sql_context, |
| 883 | + "SELECT id, name, picture FROM paimon.test_db.t", |
| 884 | + "Failed to read BlobDescriptor", |
| 885 | + ) |
| 886 | + .await; |
| 887 | +} |
| 888 | + |
712 | 889 | #[tokio::test] |
713 | 890 | async fn test_blob_descriptor_filter_before_resolve_skips_filtered_bad_descriptor() { |
714 | 891 | let (tmp, sql_context) = setup( |
|
0 commit comments