diff --git a/datafusion/physical-plan/src/aggregates/group_values/column.rs b/datafusion/physical-plan/src/aggregates/group_values/column.rs index 28f35b2bded2e..3cd59390971e6 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/column.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/column.rs @@ -16,7 +16,8 @@ // under the License. use crate::aggregates::group_values::group_column::{ - ByteGroupValueBuilder, GroupColumn, PrimitiveGroupValueBuilder, + ByteGroupValueBuilder, ByteViewGroupValueBuilder, BytesOutputType, GroupColumn, + PrimitiveGroupValueBuilder, }; use crate::aggregates::group_values::GroupValues; use ahash::RandomState; @@ -26,13 +27,13 @@ use arrow::datatypes::{ Int8Type, UInt16Type, UInt32Type, UInt64Type, UInt8Type, }; use arrow::record_batch::RecordBatch; +use arrow_array::types::{BinaryViewType, StringViewType}; use arrow_array::{Array, ArrayRef}; use arrow_schema::{DataType, Schema, SchemaRef}; use datafusion_common::hash_utils::create_hashes; use datafusion_common::{not_impl_err, DataFusionError, Result}; use datafusion_execution::memory_pool::proxy::{RawTableAllocExt, VecAllocExt}; use datafusion_expr::EmitTo; -use datafusion_physical_expr::binary_map::OutputType; use hashbrown::raw::RawTable; @@ -119,6 +120,8 @@ impl GroupValuesColumn { | DataType::LargeBinary | DataType::Date32 | DataType::Date64 + | DataType::Utf8View + | DataType::BinaryView ) } } @@ -169,19 +172,29 @@ impl GroupValues for GroupValuesColumn { &DataType::Date32 => instantiate_primitive!(v, nullable, Date32Type), &DataType::Date64 => instantiate_primitive!(v, nullable, Date64Type), &DataType::Utf8 => { - let b = ByteGroupValueBuilder::::new(OutputType::Utf8); + let b = ByteGroupValueBuilder::::new(BytesOutputType::Utf8); v.push(Box::new(b) as _) } &DataType::LargeUtf8 => { - let b = ByteGroupValueBuilder::::new(OutputType::Utf8); + let b = ByteGroupValueBuilder::::new(BytesOutputType::Utf8); v.push(Box::new(b) as _) } &DataType::Binary => { - let b = ByteGroupValueBuilder::::new(OutputType::Binary); + let b = + ByteGroupValueBuilder::::new(BytesOutputType::Binary); v.push(Box::new(b) as _) } &DataType::LargeBinary => { - let b = ByteGroupValueBuilder::::new(OutputType::Binary); + let b = + ByteGroupValueBuilder::::new(BytesOutputType::Binary); + v.push(Box::new(b) as _) + } + &DataType::Utf8View => { + let b = ByteViewGroupValueBuilder::::new(); + v.push(Box::new(b) as _) + } + &DataType::BinaryView => { + let b = ByteViewGroupValueBuilder::::new(); v.push(Box::new(b) as _) } dt => { diff --git a/datafusion/physical-plan/src/aggregates/group_values/group_column.rs b/datafusion/physical-plan/src/aggregates/group_values/group_column.rs index 5d00f300e960c..e5333e92d877a 100644 --- a/datafusion/physical-plan/src/aggregates/group_values/group_column.rs +++ b/datafusion/physical-plan/src/aggregates/group_values/group_column.rs @@ -15,7 +15,9 @@ // specific language governing permissions and limitations // under the License. +use arrow::array::make_view; use arrow::array::BufferBuilder; +use arrow::array::ByteView; use arrow::array::GenericBinaryArray; use arrow::array::GenericStringArray; use arrow::array::OffsetSizeTrait; @@ -24,13 +26,19 @@ use arrow::array::{Array, ArrayRef, ArrowPrimitiveType, AsArray}; use arrow::buffer::OffsetBuffer; use arrow::buffer::ScalarBuffer; use arrow::datatypes::ByteArrayType; +use arrow::datatypes::ByteViewType; use arrow::datatypes::DataType; use arrow::datatypes::GenericBinaryType; +use arrow_array::{GenericByteViewArray, StringViewArray}; +use arrow_buffer::Buffer; use datafusion_common::utils::proxy::VecAllocExt; +use std::marker::PhantomData; use crate::aggregates::group_values::null_builder::MaybeNullBufferBuilder; use arrow_array::types::GenericStringType; -use datafusion_physical_expr_common::binary_map::{OutputType, INITIAL_BUFFER_CAPACITY}; +use datafusion_physical_expr_common::binary_map::INITIAL_BUFFER_CAPACITY; +use std::mem; +use std::str::from_utf8; use std::sync::Arc; use std::vec; @@ -60,6 +68,13 @@ pub trait GroupColumn: Send + Sync { fn take_n(&mut self, n: usize) -> ArrayRef; } +/// Should the builder output binary or UTF8? +#[derive(Debug, PartialEq, Clone, Copy)] +pub(crate) enum BytesOutputType { + Binary, + Utf8, +} + /// An implementation of [`GroupColumn`] for primitive values /// /// Optimized to skip null buffer construction if the input is known to be non nullable @@ -167,7 +182,7 @@ pub struct ByteGroupValueBuilder where O: OffsetSizeTrait, { - output_type: OutputType, + bytes_output_type: BytesOutputType, buffer: BufferBuilder, /// Offsets into `buffer` for each distinct value. These offsets as used /// directly to create the final `GenericBinaryArray`. The `i`th string is @@ -182,9 +197,9 @@ impl ByteGroupValueBuilder where O: OffsetSizeTrait, { - pub fn new(output_type: OutputType) -> Self { + pub fn new(bytes_output_type: BytesOutputType) -> Self { Self { - output_type, + bytes_output_type, buffer: BufferBuilder::new(INITIAL_BUFFER_CAPACITY), offsets: vec![O::default()], nulls: MaybeNullBufferBuilder::new(), @@ -238,43 +253,41 @@ where { fn equal_to(&self, lhs_row: usize, column: &ArrayRef, rhs_row: usize) -> bool { // Sanity array type - match self.output_type { - OutputType::Binary => { + match self.bytes_output_type { + BytesOutputType::Binary => { debug_assert!(matches!( column.data_type(), DataType::Binary | DataType::LargeBinary )); self.equal_to_inner::>(lhs_row, column, rhs_row) } - OutputType::Utf8 => { + BytesOutputType::Utf8 => { debug_assert!(matches!( column.data_type(), DataType::Utf8 | DataType::LargeUtf8 )); self.equal_to_inner::>(lhs_row, column, rhs_row) } - _ => unreachable!("View types should use `ArrowBytesViewMap`"), } } fn append_val(&mut self, column: &ArrayRef, row: usize) { // Sanity array type - match self.output_type { - OutputType::Binary => { + match self.bytes_output_type { + BytesOutputType::Binary => { debug_assert!(matches!( column.data_type(), DataType::Binary | DataType::LargeBinary )); self.append_val_inner::>(column, row) } - OutputType::Utf8 => { + BytesOutputType::Utf8 => { debug_assert!(matches!( column.data_type(), DataType::Utf8 | DataType::LargeUtf8 )); self.append_val_inner::>(column, row) } - _ => unreachable!("View types should use `ArrowBytesViewMap`"), }; } @@ -290,7 +303,7 @@ where fn build(self: Box) -> ArrayRef { let Self { - output_type, + bytes_output_type: output_type, mut buffer, offsets, nulls, @@ -303,13 +316,13 @@ where let offsets = unsafe { OffsetBuffer::new_unchecked(ScalarBuffer::from(offsets)) }; let values = buffer.finish(); match output_type { - OutputType::Binary => { + BytesOutputType::Binary => { // SAFETY: the offsets were constructed correctly Arc::new(unsafe { GenericBinaryArray::new_unchecked(offsets, values, null_buffer) }) } - OutputType::Utf8 => { + BytesOutputType::Utf8 => { // SAFETY: // 1. the offsets were constructed safely // @@ -320,7 +333,6 @@ where GenericStringArray::new_unchecked(offsets, values, null_buffer) }) } - _ => unreachable!("View types should use `ArrowBytesViewMap`"), } } @@ -353,14 +365,14 @@ where let values = self.buffer.finish(); self.buffer = remaining_buffer; - match self.output_type { - OutputType::Binary => { + match self.bytes_output_type { + BytesOutputType::Binary => { // SAFETY: the offsets were constructed correctly Arc::new(unsafe { GenericBinaryArray::new_unchecked(offsets, values, null_buffer) }) } - OutputType::Utf8 => { + BytesOutputType::Utf8 => { // SAFETY: // 1. the offsets were constructed safely // @@ -371,11 +383,320 @@ where GenericStringArray::new_unchecked(offsets, values, null_buffer) }) } - _ => unreachable!("View types should use `ArrowBytesViewMap`"), } } } +/// An implementation of [`GroupColumn`] for binary view and utf8 view types. +/// +/// Stores a collection of binary view or utf8 view group values in a buffer +/// whose structure is similar to `GenericByteViewArray`, and we can get benefits: +/// +/// 1. Efficient comparison of incoming rows to existing rows +/// 2. Efficient construction of the final output array +/// 3. Efficient to perform `take_n` comparing to use `GenericByteViewBuilder` +pub struct ByteViewGroupValueBuilder { + /// The views of string values + /// + /// If string len <= 12, the view's format will be: + /// string(12B) | len(4B) + /// + /// If string len > 12, its format will be: + /// offset(4B) | buffer_index(4B) | prefix(4B) | len(4B) + views: Vec, + + /// The progressing block + /// + /// New values will be inserted into it until its capacity + /// is not enough(detail can see `max_block_size`). + in_progress: Vec, + + /// The completed blocks + completed: Vec, + + /// The max size of `in_progress` + /// + /// `in_progress` will be flushed into `completed`, and create new `in_progress` + /// when found its remaining capacity(`max_block_size` - `len(in_progress)`), + /// is no enough to store the appended value. + max_block_size: usize, + + /// Nulls + nulls: MaybeNullBufferBuilder, + + /// phantom data so the type requires + _phantom: PhantomData, +} + +impl ByteViewGroupValueBuilder { + pub fn new() -> Self { + Self { + views: vec![], + in_progress: Vec::with_capacity(INITIAL_BUFFER_CAPACITY), + completed: vec![], + max_block_size: INITIAL_BUFFER_CAPACITY, + nulls: MaybeNullBufferBuilder::new(), + _phantom: PhantomData {}, + } + } + + /// Set the max block size + #[cfg(test)] + fn with_max_block_size(mut self, max_block_size: usize) -> Self { + self.max_block_size = max_block_size; + self + } + + fn append_val_inner(&mut self, array: &ArrayRef, row: usize) { + let arr = array.as_byte_view::(); + + // If a null row, set and return + if arr.is_null(row) { + self.nulls.append(true); + self.views.push(0); + return; + } + + // Not null case + self.nulls.append(false); + let value: &[u8] = arr.value(row).as_ref(); + + let value_len = value.len(); + let view = if value_len <= 12 { + make_view(value, 0, 0) + } else { + // Ensure big enough block to hold the value firstly + self.ensure_in_progress_big_enough(value_len); + + // Append value + let buffer_index = self.completed.len(); + let offset = self.in_progress.len(); + self.in_progress.extend_from_slice(value); + + make_view(value, buffer_index as u32, offset as u32) + }; + + // Append view + self.views.push(view); + } + + fn ensure_in_progress_big_enough(&mut self, value_len: usize) { + debug_assert!(value_len > 12); + let require_cap = self.in_progress.len() + value_len; + + // If current block isn't big enough, flush it and create a new in progress block + if require_cap > self.max_block_size { + self.finish_in_progress() + } + } + + /// Finishes the current in progress block and appends it to self.completed + fn finish_in_progress(&mut self) { + let flushed_block = mem::replace( + &mut self.in_progress, + Vec::with_capacity(self.max_block_size), + ); + let buffer = Buffer::from_vec(flushed_block); + self.completed.push(buffer); + } + + + fn equal_to_inner(&self, lhs_row: usize, array: &ArrayRef, rhs_row: usize) -> bool { + let array = array.as_byte_view::(); + + // Check if nulls equal firstly + let exist_null = self.nulls.is_null(lhs_row); + let input_null = array.is_null(rhs_row); + if let Some(result) = nulls_equal_to(exist_null, input_null) { + return result; + } + + // Otherwise, we need to check their values + let exist_view = self.views[lhs_row]; + let exist_view_len = exist_view as u32; + + let input_view = array.views()[rhs_row]; + let input_view_len = input_view as u32; + + // The check logic + // - Check len equality + // - If inlined, check inlined value + // - If non-inlined, check prefix and then check value in buffer + // when needed + if exist_view_len != input_view_len { + return false; + } + + if exist_view_len <= 12 { + let exist_inline = unsafe { + GenericByteViewArray::::inline_value( + &exist_view, + exist_view_len as usize, + ) + }; + let input_inline = unsafe { + GenericByteViewArray::::inline_value( + &input_view, + input_view_len as usize, + ) + }; + exist_inline == input_inline + } else { + let exist_prefix = + unsafe { GenericByteViewArray::::inline_value(&exist_view, 4) }; + let input_prefix = + unsafe { GenericByteViewArray::::inline_value(&input_view, 4) }; + + if exist_prefix != input_prefix { + return false; + } + + let exist_full = { + let byte_view = ByteView::from(exist_view); + self.value( + byte_view.buffer_index as usize, + byte_view.offset as usize, + byte_view.length as usize, + ) + }; + let input_full: &[u8] = unsafe { array.value_unchecked(rhs_row).as_ref() }; + exist_full == input_full + } + } + + fn value(&self, buffer_index: usize, offset: usize, length: usize) -> &[u8] { + debug_assert!(buffer_index <= self.completed.len()); + + if buffer_index < self.completed.len() { + let block = &self.completed[buffer_index]; + &block[offset..offset + length] + } else { + &self.in_progress[offset..offset + length] + } + } + + fn inner_take_n(&mut self, n: usize) -> ArrayRef { + let views = self.views.drain(0..n).collect::>(); + + let num_buffers_in_first_n = if let Some(idx) = higest_buffer_index(&views) { + let idx = idx as usize; + // if the views we are returning contain currently in progress + // buffer, finalize it + if idx > self.completed.len() { + self.finish_in_progress(); + } + idx+1 + } else { + 0 + }; + + let buffers = self.completed[0..num_buffers_in_first_n].to_vec(); + let nulls = self.nulls.take_n(n); + + // TODO: remove any buffers no longer referenced + if let Some(idx) = higest_buffer_index(&self.views) { + //if idx as usize <= self.completed.len() { + // self.completed.drain() + //} + // update existing views to reflect removed buffers + } + + + // safety + // all input values came from arrays of the correct type (so were valid utf8 if string) + // all views were created correctly + let arr = unsafe { + GenericByteViewArray::::new_unchecked( + views.into(), + buffers, + nulls, + ) + }; + Arc::new(arr) + } +} + +/// returns the highest buffer index referred to in a set og u128 views +/// assumes that all buffer indexes are monotonically increasing +/// +/// Returns `None` if no buffers are referenced (e.g. all views are inlined) +fn higest_buffer_index(views: &[u128]) -> Option { + println!("Checking views"); + for v in views.iter() { + let view_len = (*v as u32) as usize; + if view_len < 12 { + let prefix = unsafe { StringViewArray::inline_value(v, view_len) }; + println!("len: {view_len} inline: {}", from_utf8(prefix).unwrap()) + } + else { + println!( + "len: {view_len} val: {:?}", + ByteView::from(*v) + ); + } + } + views.iter().rev().find_map(|view| { + let view_len = *view as u32; + if view_len < 12 { + None // inline view + } else { + let view = ByteView::from(*view); + Some(view.buffer_index) + } + }) +} + +impl GroupColumn for ByteViewGroupValueBuilder { + fn equal_to(&self, lhs_row: usize, array: &ArrayRef, rhs_row: usize) -> bool { + self.equal_to_inner(lhs_row, array, rhs_row) + } + + fn append_val(&mut self, array: &ArrayRef, row: usize) { + self.append_val_inner(array, row) + } + + fn len(&self) -> usize { + self.views.len() + } + + fn size(&self) -> usize { + self.views.capacity() * std::mem::size_of::() + + self.in_progress.len() + + self.completed.iter().map(|b| b.len()).sum::() + + self.nulls.allocated_size() + } + + fn build(self: Box) -> ArrayRef { + let Self { + views, + in_progress, + mut completed, + max_block_size: _, + nulls, + _phantom, + } = *self; + + // complete the in progress view + completed.push(in_progress.into()); + + // safety + // all input values came from arrays of the correct type (so were valid utf8 if string) + // all views were created correctly + let arr = unsafe { + GenericByteViewArray::::new_unchecked( + views.into(), + completed, + nulls.build(), + ) + }; + Arc::new(arr) + } + + fn take_n(&mut self, n: usize) -> ArrayRef { + self.inner_take_n(n) + } +} + /// Determines if the nullability of the existing and new input array can be used /// to short-circuit the comparison of the two values. /// @@ -392,20 +713,17 @@ fn nulls_equal_to(lhs_null: bool, rhs_null: bool) -> Option { #[cfg(test)] mod tests { + use super::*; use std::sync::Arc; use arrow::datatypes::Int64Type; - use arrow_array::{ArrayRef, Int64Array, StringArray}; + use arrow_array::types::StringViewType; + use arrow_array::{ArrayRef, Int64Array, StringArray, StringViewArray}; use arrow_buffer::{BooleanBufferBuilder, NullBuffer}; - use datafusion_physical_expr::binary_map::OutputType; - - use crate::aggregates::group_values::group_column::PrimitiveGroupValueBuilder; - - use super::{ByteGroupValueBuilder, GroupColumn}; #[test] fn test_take_n() { - let mut builder = ByteGroupValueBuilder::::new(OutputType::Utf8); + let mut builder = ByteGroupValueBuilder::::new(BytesOutputType::Utf8); let array = Arc::new(StringArray::from(vec![Some("a"), None])) as ArrayRef; // a, null, null builder.append_val(&array, 0); @@ -532,7 +850,7 @@ mod tests { // - exist not null, input not null; values equal // Define PrimitiveGroupValueBuilder - let mut builder = ByteGroupValueBuilder::::new(OutputType::Utf8); + let mut builder = ByteGroupValueBuilder::::new(BytesOutputType::Utf8); let builder_array = Arc::new(StringArray::from(vec![ None, None, @@ -579,4 +897,153 @@ mod tests { assert!(!builder.equal_to(4, &input_array, 4)); assert!(builder.equal_to(5, &input_array, 5)); } + + #[test] + fn test_byte_view_append_val() { + let mut builder = + ByteViewGroupValueBuilder::::new().with_max_block_size(60); + let builder_array = StringViewArray::from(vec![ + Some("this string is quite long"), // in buffer 0 + Some("foo"), + None, + Some("bar"), + Some("this string is also quite long"), // buffer 0 + Some("this string is quite long"), // buffer 1 + Some("bar"), + ]); + let builder_array: ArrayRef = Arc::new(builder_array); + for row in 0..builder_array.len() { + builder.append_val(&builder_array, row); + } + + let output = Box::new(builder).build(); + // should be 2 output buffers to hold all the data + assert_eq!( + output.as_string_view().data_buffers().len(), + 2, + ); + assert_eq!(&output, &builder_array) + } + + #[test] + fn test_byte_view_equal_to() { + // Will cover such cases: + // - exist null, input not null + // - exist null, input null; values not equal + // - exist null, input null; values equal + // - exist not null, input null + // - exist not null, input not null; values not equal + // - exist not null, input not null; values equal + + let mut builder = ByteViewGroupValueBuilder::::new(); + let builder_array = Arc::new(StringViewArray::from(vec![ + None, + None, + None, + Some("foo"), + Some("bar"), + Some("this string is quite long"), + Some("baz"), + ])) as ArrayRef; + builder.append_val(&builder_array, 0); + builder.append_val(&builder_array, 1); + builder.append_val(&builder_array, 2); + builder.append_val(&builder_array, 3); + builder.append_val(&builder_array, 4); + builder.append_val(&builder_array, 5); + builder.append_val(&builder_array, 6); + + // Define input array + let (views, buffer, _nulls) = StringViewArray::from(vec![ + Some("foo"), + Some("bar"), // set to null + Some("this string is quite long"), // set to null + None, + None, + Some("foo"), + Some("baz"), + ]) + .into_parts(); + + // explicitly build a boolean buffer where one of the null values also happens to match + let mut boolean_buffer_builder = BooleanBufferBuilder::new(6); + boolean_buffer_builder.append(true); + boolean_buffer_builder.append(false); // this sets Some("bar") to null above + boolean_buffer_builder.append(false); // this sets Some("thisstringisquitelong") to null above + boolean_buffer_builder.append(false); + boolean_buffer_builder.append(false); + boolean_buffer_builder.append(true); + boolean_buffer_builder.append(true); + let nulls = NullBuffer::new(boolean_buffer_builder.finish()); + let input_array = + Arc::new(StringViewArray::new(views, buffer, Some(nulls))) as ArrayRef; + + // Check + assert!(!builder.equal_to(0, &input_array, 0)); + assert!(builder.equal_to(1, &input_array, 1)); + assert!(builder.equal_to(2, &input_array, 2)); + assert!(!builder.equal_to(3, &input_array, 3)); + assert!(!builder.equal_to(4, &input_array, 4)); + assert!(!builder.equal_to(5, &input_array, 5)); + assert!(builder.equal_to(6, &input_array, 6)); + } + + + #[test] + fn test_byte_view_take_n() { + let mut builder = + ByteViewGroupValueBuilder::::new().with_max_block_size(60); + let input_array = StringViewArray::from(vec![ + Some("this string is quite long"), // in buffer 0 + Some("foo"), + Some("bar"), + Some("this string is also quite long"), // buffer 0 + None, + Some("this string is quite long"), // buffer 1 + None, + Some("another string that is is quite long"), // buffer 1 + Some("bar"), + ]); + let input_array: ArrayRef = Arc::new(input_array); + for row in 0..input_array.len() { + builder.append_val(&input_array, row); + } + + // should be 2 completed, one in progress buffer to hold all output + assert_eq!(builder.completed.len(), 2); + assert!(builder.in_progress.len() > 0); + + let first_4 = builder.take_n(4); + println!("{}", arrow::util::pretty::pretty_format_columns("first_4", &[first_4.clone()]).unwrap()); + assert_eq!(&first_4, &input_array.slice(0, 4)); + + // Add some new data after the first n + let input_array = StringViewArray::from(vec![ + Some("short"), + None, + Some("Some new data to add that is long"), // in buffer 0 + Some("short again"), + ]); + let input_array: ArrayRef = Arc::new(input_array); + for row in 0..input_array.len() { + builder.append_val(&input_array, row); + } + + let result = Box::new(builder).build(); + let expected: ArrayRef = Arc::new(StringViewArray::from(vec![ + // last rows of the original input + None, + Some("this string is quite long"), + None, + Some("another string that is is quite long"), + Some("bar"), + // the subsequent input + Some("short"), + None, + Some("Some new data to add that is long"), // in buffer 0 + Some("short again"), + ])); + assert_eq!(&result, &expected); + } + } diff --git a/datafusion/sqllogictest/test_files/group_by.slt b/datafusion/sqllogictest/test_files/group_by.slt index a80a0891e9770..4807342c5a1a9 100644 --- a/datafusion/sqllogictest/test_files/group_by.slt +++ b/datafusion/sqllogictest/test_files/group_by.slt @@ -5207,4 +5207,67 @@ NULL NULL 2 NULL a 2 statement ok -drop table t; +drop table t + + +# test multi group by int + utf8view +statement ok +create table source as values +-- use some strings that are larger than 12 characters as that goes through a different path +(1, 'a'), +(1, 'a'), +(2, 'thisstringislongerthan12'), +(2, 'thisstring'), +(3, 'abc'), +(3, 'cba'), +(2, 'thisstring'), +(null, null), +(null, 'a'), +(null, null), +(null, 'a'), +(2, 'thisstringisalsolongerthan12'), +(2, 'thisstringislongerthan12'), +(1, 'null') +; + +statement ok +create view t as select column1 as a, arrow_cast(column2, 'Utf8View') as b from source; + +query ITI +select a, b, count(*) from t group by a, b order by a, b; +---- +1 a 2 +1 null 1 +2 thisstring 2 +2 thisstringisalsolongerthan12 1 +2 thisstringislongerthan12 2 +3 abc 1 +3 cba 1 +NULL a 2 +NULL NULL 2 + +statement ok +drop view t + +# test with binary view +statement ok +create view t as select column1 as a, arrow_cast(column2, 'BinaryView') as b from source; + +query I?I +select a, b, count(*) from t group by a, b order by a, b; +---- +1 61 2 +1 6e756c6c 1 +2 74686973737472696e67 2 +2 74686973737472696e676973616c736f6c6f6e6765727468616e3132 1 +2 74686973737472696e6769736c6f6e6765727468616e3132 2 +3 616263 1 +3 636261 1 +NULL 61 2 +NULL NULL 2 + +statement ok +drop view t + +statement ok +drop table source;