snapshot of https://github.com/seaweedfs/seaweedfs.git (master, 2026-09-16; blobs > 131072B omitted)
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
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
This commit is contained in:
206
weed/mq/schema/flat_schema_utils.go
Normal file
206
weed/mq/schema/flat_schema_utils.go
Normal file
@@ -0,0 +1,206 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
// SplitFlatSchemaToKeyValue takes a flat RecordType and key column names,
|
||||
// returns separate key and value RecordTypes
|
||||
func SplitFlatSchemaToKeyValue(flatSchema *schema_pb.RecordType, keyColumns []string) (*schema_pb.RecordType, *schema_pb.RecordType, error) {
|
||||
if flatSchema == nil {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
// Create maps for fast lookup
|
||||
keyColumnSet := make(map[string]bool)
|
||||
for _, col := range keyColumns {
|
||||
keyColumnSet[col] = true
|
||||
}
|
||||
|
||||
var keyFields []*schema_pb.Field
|
||||
var valueFields []*schema_pb.Field
|
||||
|
||||
// Split fields based on key columns
|
||||
for _, field := range flatSchema.Fields {
|
||||
if keyColumnSet[field.Name] {
|
||||
// Create key field with reindexed field index
|
||||
keyField := &schema_pb.Field{
|
||||
Name: field.Name,
|
||||
FieldIndex: int32(len(keyFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
keyFields = append(keyFields, keyField)
|
||||
} else {
|
||||
// Create value field with reindexed field index
|
||||
valueField := &schema_pb.Field{
|
||||
Name: field.Name,
|
||||
FieldIndex: int32(len(valueFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
valueFields = append(valueFields, valueField)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that all key columns were found
|
||||
if len(keyFields) != len(keyColumns) {
|
||||
missingCols := []string{}
|
||||
for _, col := range keyColumns {
|
||||
found := false
|
||||
for _, field := range keyFields {
|
||||
if field.Name == col {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
missingCols = append(missingCols, col)
|
||||
}
|
||||
}
|
||||
if len(missingCols) > 0 {
|
||||
return nil, nil, fmt.Errorf("key columns not found in schema: %v", missingCols)
|
||||
}
|
||||
}
|
||||
|
||||
var keyRecordType *schema_pb.RecordType
|
||||
if len(keyFields) > 0 {
|
||||
keyRecordType = &schema_pb.RecordType{Fields: keyFields}
|
||||
}
|
||||
|
||||
var valueRecordType *schema_pb.RecordType
|
||||
if len(valueFields) > 0 {
|
||||
valueRecordType = &schema_pb.RecordType{Fields: valueFields}
|
||||
}
|
||||
|
||||
return keyRecordType, valueRecordType, nil
|
||||
}
|
||||
|
||||
// CombineFlatSchemaFromKeyValue creates a flat RecordType by combining key and value schemas
|
||||
// Key fields are placed first, then value fields
|
||||
func CombineFlatSchemaFromKeyValue(keySchema *schema_pb.RecordType, valueSchema *schema_pb.RecordType) (*schema_pb.RecordType, []string) {
|
||||
var combinedFields []*schema_pb.Field
|
||||
var keyColumns []string
|
||||
|
||||
// Add key fields first
|
||||
if keySchema != nil {
|
||||
for _, field := range keySchema.Fields {
|
||||
combinedField := &schema_pb.Field{
|
||||
Name: field.Name,
|
||||
FieldIndex: int32(len(combinedFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
combinedFields = append(combinedFields, combinedField)
|
||||
keyColumns = append(keyColumns, field.Name)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value fields
|
||||
if valueSchema != nil {
|
||||
for _, field := range valueSchema.Fields {
|
||||
// Check for name conflicts
|
||||
fieldName := field.Name
|
||||
for _, keyCol := range keyColumns {
|
||||
if fieldName == keyCol {
|
||||
// This shouldn't happen in well-formed schemas, but handle gracefully
|
||||
fieldName = "value_" + fieldName
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
combinedField := &schema_pb.Field{
|
||||
Name: fieldName,
|
||||
FieldIndex: int32(len(combinedFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
combinedFields = append(combinedFields, combinedField)
|
||||
}
|
||||
}
|
||||
|
||||
if len(combinedFields) == 0 {
|
||||
return nil, keyColumns
|
||||
}
|
||||
|
||||
return &schema_pb.RecordType{Fields: combinedFields}, keyColumns
|
||||
}
|
||||
|
||||
// ExtractKeyColumnsFromCombinedSchema tries to infer key columns from a combined schema
|
||||
// that was created using CreateCombinedRecordType (with key_ prefixes)
|
||||
func ExtractKeyColumnsFromCombinedSchema(combinedSchema *schema_pb.RecordType) (flatSchema *schema_pb.RecordType, keyColumns []string) {
|
||||
if combinedSchema == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var flatFields []*schema_pb.Field
|
||||
var keyColumns_ []string
|
||||
|
||||
for _, field := range combinedSchema.Fields {
|
||||
if strings.HasPrefix(field.Name, "key_") {
|
||||
// This is a key field - remove the prefix
|
||||
originalName := strings.TrimPrefix(field.Name, "key_")
|
||||
flatField := &schema_pb.Field{
|
||||
Name: originalName,
|
||||
FieldIndex: int32(len(flatFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
flatFields = append(flatFields, flatField)
|
||||
keyColumns_ = append(keyColumns_, originalName)
|
||||
} else {
|
||||
// This is a value field
|
||||
flatField := &schema_pb.Field{
|
||||
Name: field.Name,
|
||||
FieldIndex: int32(len(flatFields)),
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
flatFields = append(flatFields, flatField)
|
||||
}
|
||||
}
|
||||
|
||||
// Sort key columns to ensure deterministic order
|
||||
sort.Strings(keyColumns_)
|
||||
|
||||
if len(flatFields) == 0 {
|
||||
return nil, keyColumns_
|
||||
}
|
||||
|
||||
return &schema_pb.RecordType{Fields: flatFields}, keyColumns_
|
||||
}
|
||||
|
||||
// ValidateKeyColumns checks that all key columns exist in the schema
|
||||
func ValidateKeyColumns(schema *schema_pb.RecordType, keyColumns []string) error {
|
||||
if schema == nil || len(keyColumns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
fieldNames := make(map[string]bool)
|
||||
for _, field := range schema.Fields {
|
||||
fieldNames[field.Name] = true
|
||||
}
|
||||
|
||||
var missingColumns []string
|
||||
for _, keyCol := range keyColumns {
|
||||
if !fieldNames[keyCol] {
|
||||
missingColumns = append(missingColumns, keyCol)
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingColumns) > 0 {
|
||||
return fmt.Errorf("key columns not found in schema: %v", missingColumns)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
265
weed/mq/schema/flat_schema_utils_test.go
Normal file
265
weed/mq/schema/flat_schema_utils_test.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func TestSplitFlatSchemaToKeyValue(t *testing.T) {
|
||||
// Create a test flat schema
|
||||
flatSchema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{
|
||||
Name: "user_id",
|
||||
FieldIndex: 0,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
{
|
||||
Name: "session_id",
|
||||
FieldIndex: 1,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
{
|
||||
Name: "event_type",
|
||||
FieldIndex: 2,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
{
|
||||
Name: "timestamp",
|
||||
FieldIndex: 3,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
keyColumns := []string{"user_id", "session_id"}
|
||||
|
||||
keySchema, valueSchema, err := SplitFlatSchemaToKeyValue(flatSchema, keyColumns)
|
||||
if err != nil {
|
||||
t.Fatalf("SplitFlatSchemaToKeyValue failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify key schema
|
||||
if keySchema == nil {
|
||||
t.Fatal("Expected key schema, got nil")
|
||||
}
|
||||
if len(keySchema.Fields) != 2 {
|
||||
t.Errorf("Expected 2 key fields, got %d", len(keySchema.Fields))
|
||||
}
|
||||
if keySchema.Fields[0].Name != "user_id" || keySchema.Fields[1].Name != "session_id" {
|
||||
t.Errorf("Key field names incorrect: %v", []string{keySchema.Fields[0].Name, keySchema.Fields[1].Name})
|
||||
}
|
||||
|
||||
// Verify value schema
|
||||
if valueSchema == nil {
|
||||
t.Fatal("Expected value schema, got nil")
|
||||
}
|
||||
if len(valueSchema.Fields) != 2 {
|
||||
t.Errorf("Expected 2 value fields, got %d", len(valueSchema.Fields))
|
||||
}
|
||||
if valueSchema.Fields[0].Name != "event_type" || valueSchema.Fields[1].Name != "timestamp" {
|
||||
t.Errorf("Value field names incorrect: %v", []string{valueSchema.Fields[0].Name, valueSchema.Fields[1].Name})
|
||||
}
|
||||
|
||||
// Verify field indices are reindexed
|
||||
for i, field := range keySchema.Fields {
|
||||
if field.FieldIndex != int32(i) {
|
||||
t.Errorf("Key field %s has incorrect index %d, expected %d", field.Name, field.FieldIndex, i)
|
||||
}
|
||||
}
|
||||
for i, field := range valueSchema.Fields {
|
||||
if field.FieldIndex != int32(i) {
|
||||
t.Errorf("Value field %s has incorrect index %d, expected %d", field.Name, field.FieldIndex, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSplitFlatSchemaToKeyValueMissingColumns(t *testing.T) {
|
||||
flatSchema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{Name: "field1", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}},
|
||||
},
|
||||
}
|
||||
|
||||
keyColumns := []string{"field1", "missing_field"}
|
||||
|
||||
_, _, err := SplitFlatSchemaToKeyValue(flatSchema, keyColumns)
|
||||
if err == nil {
|
||||
t.Error("Expected error for missing key column, got nil")
|
||||
}
|
||||
if !contains(err.Error(), "missing_field") {
|
||||
t.Errorf("Error should mention missing_field: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCombineFlatSchemaFromKeyValue(t *testing.T) {
|
||||
keySchema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{
|
||||
Name: "user_id",
|
||||
FieldIndex: 0,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
{
|
||||
Name: "session_id",
|
||||
FieldIndex: 1,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
valueSchema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{
|
||||
Name: "event_type",
|
||||
FieldIndex: 0,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
{
|
||||
Name: "timestamp",
|
||||
FieldIndex: 1,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flatSchema, keyColumns := CombineFlatSchemaFromKeyValue(keySchema, valueSchema)
|
||||
|
||||
// Verify combined schema
|
||||
if flatSchema == nil {
|
||||
t.Fatal("Expected flat schema, got nil")
|
||||
}
|
||||
if len(flatSchema.Fields) != 4 {
|
||||
t.Errorf("Expected 4 fields, got %d", len(flatSchema.Fields))
|
||||
}
|
||||
|
||||
// Verify key columns
|
||||
expectedKeyColumns := []string{"user_id", "session_id"}
|
||||
if !reflect.DeepEqual(keyColumns, expectedKeyColumns) {
|
||||
t.Errorf("Expected key columns %v, got %v", expectedKeyColumns, keyColumns)
|
||||
}
|
||||
|
||||
// Verify field order (key fields first)
|
||||
expectedNames := []string{"user_id", "session_id", "event_type", "timestamp"}
|
||||
actualNames := make([]string, len(flatSchema.Fields))
|
||||
for i, field := range flatSchema.Fields {
|
||||
actualNames[i] = field.Name
|
||||
}
|
||||
if !reflect.DeepEqual(actualNames, expectedNames) {
|
||||
t.Errorf("Expected field names %v, got %v", expectedNames, actualNames)
|
||||
}
|
||||
|
||||
// Verify field indices are sequential
|
||||
for i, field := range flatSchema.Fields {
|
||||
if field.FieldIndex != int32(i) {
|
||||
t.Errorf("Field %s has incorrect index %d, expected %d", field.Name, field.FieldIndex, i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestExtractKeyColumnsFromCombinedSchema(t *testing.T) {
|
||||
// Create a combined schema with key_ prefixes (as created by CreateCombinedRecordType)
|
||||
combinedSchema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{
|
||||
Name: "key_user_id",
|
||||
FieldIndex: 0,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
{
|
||||
Name: "key_session_id",
|
||||
FieldIndex: 1,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
{
|
||||
Name: "event_type",
|
||||
FieldIndex: 2,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}},
|
||||
},
|
||||
{
|
||||
Name: "timestamp",
|
||||
FieldIndex: 3,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
flatSchema, keyColumns := ExtractKeyColumnsFromCombinedSchema(combinedSchema)
|
||||
|
||||
// Verify flat schema
|
||||
if flatSchema == nil {
|
||||
t.Fatal("Expected flat schema, got nil")
|
||||
}
|
||||
if len(flatSchema.Fields) != 4 {
|
||||
t.Errorf("Expected 4 fields, got %d", len(flatSchema.Fields))
|
||||
}
|
||||
|
||||
// Verify key columns (should be sorted)
|
||||
expectedKeyColumns := []string{"session_id", "user_id"}
|
||||
if !reflect.DeepEqual(keyColumns, expectedKeyColumns) {
|
||||
t.Errorf("Expected key columns %v, got %v", expectedKeyColumns, keyColumns)
|
||||
}
|
||||
|
||||
// Verify field names (key_ prefixes removed)
|
||||
expectedNames := []string{"user_id", "session_id", "event_type", "timestamp"}
|
||||
actualNames := make([]string, len(flatSchema.Fields))
|
||||
for i, field := range flatSchema.Fields {
|
||||
actualNames[i] = field.Name
|
||||
}
|
||||
if !reflect.DeepEqual(actualNames, expectedNames) {
|
||||
t.Errorf("Expected field names %v, got %v", expectedNames, actualNames)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateKeyColumns(t *testing.T) {
|
||||
schema := &schema_pb.RecordType{
|
||||
Fields: []*schema_pb.Field{
|
||||
{Name: "field1", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}},
|
||||
{Name: "field2", Type: &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}},
|
||||
},
|
||||
}
|
||||
|
||||
// Valid key columns
|
||||
err := ValidateKeyColumns(schema, []string{"field1"})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for valid key columns, got: %v", err)
|
||||
}
|
||||
|
||||
// Invalid key columns
|
||||
err = ValidateKeyColumns(schema, []string{"field1", "missing_field"})
|
||||
if err == nil {
|
||||
t.Error("Expected error for invalid key columns, got nil")
|
||||
}
|
||||
|
||||
// Nil schema should not error
|
||||
err = ValidateKeyColumns(nil, []string{"any_field"})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for nil schema, got: %v", err)
|
||||
}
|
||||
|
||||
// Empty key columns should not error
|
||||
err = ValidateKeyColumns(schema, []string{})
|
||||
if err != nil {
|
||||
t.Errorf("Expected no error for empty key columns, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to check if string contains substring
|
||||
func contains(str, substr string) bool {
|
||||
return len(str) >= len(substr) &&
|
||||
(len(substr) == 0 || str[len(str)-len(substr):] == substr ||
|
||||
str[:len(substr)] == substr ||
|
||||
len(str) > len(substr) && (str[len(str)-len(substr)-1:len(str)-len(substr)] == " " || str[len(str)-len(substr)-1] == ' ') && str[len(str)-len(substr):] == substr ||
|
||||
findInString(str, substr))
|
||||
}
|
||||
|
||||
func findInString(str, substr string) bool {
|
||||
for i := 0; i <= len(str)-len(substr); i++ {
|
||||
if str[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
58
weed/mq/schema/schema.go
Normal file
58
weed/mq/schema/schema.go
Normal file
@@ -0,0 +1,58 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
type Schema struct {
|
||||
Namespace string
|
||||
Name string
|
||||
RevisionId uint32
|
||||
RecordType *schema_pb.RecordType
|
||||
fieldMap map[string]*schema_pb.Field
|
||||
}
|
||||
|
||||
func NewSchema(namespace string, name string, recordType *schema_pb.RecordType) *Schema {
|
||||
fieldMap := make(map[string]*schema_pb.Field)
|
||||
for _, field := range recordType.Fields {
|
||||
fieldMap[field.Name] = field
|
||||
}
|
||||
return &Schema{
|
||||
Namespace: namespace,
|
||||
Name: name,
|
||||
RecordType: recordType,
|
||||
fieldMap: fieldMap,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Schema) GetField(name string) (*schema_pb.Field, bool) {
|
||||
field, ok := s.fieldMap[name]
|
||||
return field, ok
|
||||
}
|
||||
|
||||
func TypeToString(t *schema_pb.Type) string {
|
||||
switch t.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
switch t.GetScalarType() {
|
||||
case schema_pb.ScalarType_BOOL:
|
||||
return "bool"
|
||||
case schema_pb.ScalarType_INT32:
|
||||
return "int32"
|
||||
case schema_pb.ScalarType_INT64:
|
||||
return "int64"
|
||||
case schema_pb.ScalarType_FLOAT:
|
||||
return "float"
|
||||
case schema_pb.ScalarType_DOUBLE:
|
||||
return "double"
|
||||
case schema_pb.ScalarType_BYTES:
|
||||
return "bytes"
|
||||
case schema_pb.ScalarType_STRING:
|
||||
return "string"
|
||||
}
|
||||
case *schema_pb.Type_ListType:
|
||||
return "list"
|
||||
case *schema_pb.Type_RecordType:
|
||||
return "record"
|
||||
}
|
||||
return "unknown"
|
||||
}
|
||||
67
weed/mq/schema/schema_builder.go
Normal file
67
weed/mq/schema/schema_builder.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
var (
|
||||
// Basic scalar types
|
||||
TypeBoolean = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_BOOL}}
|
||||
TypeInt32 = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT32}}
|
||||
TypeInt64 = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_INT64}}
|
||||
TypeFloat = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_FLOAT}}
|
||||
TypeDouble = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DOUBLE}}
|
||||
TypeBytes = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_BYTES}}
|
||||
TypeString = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_STRING}}
|
||||
|
||||
// Parquet logical types
|
||||
TypeTimestamp = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_TIMESTAMP}}
|
||||
TypeDate = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DATE}}
|
||||
TypeDecimal = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_DECIMAL}}
|
||||
TypeTime = &schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_TIME}}
|
||||
)
|
||||
|
||||
type RecordTypeBuilder struct {
|
||||
recordType *schema_pb.RecordType
|
||||
}
|
||||
|
||||
// RecordTypeBegin creates a new RecordTypeBuilder, it should be followed by a series of WithField methods and RecordTypeEnd
|
||||
func RecordTypeBegin() *RecordTypeBuilder {
|
||||
return &RecordTypeBuilder{recordType: &schema_pb.RecordType{}}
|
||||
}
|
||||
|
||||
// RecordTypeEnd finishes the building of a RecordValue
|
||||
func (rtb *RecordTypeBuilder) RecordTypeEnd() *schema_pb.RecordType {
|
||||
// be consistent with parquet.node.go `func (g Group) Fields() []Field`
|
||||
sort.Slice(rtb.recordType.Fields, func(i, j int) bool {
|
||||
return rtb.recordType.Fields[i].Name < rtb.recordType.Fields[j].Name
|
||||
})
|
||||
return rtb.recordType
|
||||
}
|
||||
|
||||
// NewRecordTypeBuilder creates a new RecordTypeBuilder from an existing RecordType, it should be followed by a series of WithField methods and RecordTypeEnd
|
||||
func NewRecordTypeBuilder(recordType *schema_pb.RecordType) (rtb *RecordTypeBuilder) {
|
||||
return &RecordTypeBuilder{recordType: recordType}
|
||||
}
|
||||
|
||||
func (rtb *RecordTypeBuilder) WithField(name string, scalarType *schema_pb.Type) *RecordTypeBuilder {
|
||||
rtb.recordType.Fields = append(rtb.recordType.Fields, &schema_pb.Field{
|
||||
Name: name,
|
||||
Type: scalarType,
|
||||
})
|
||||
return rtb
|
||||
}
|
||||
|
||||
func (rtb *RecordTypeBuilder) WithRecordField(name string, recordType *schema_pb.RecordType) *RecordTypeBuilder {
|
||||
rtb.recordType.Fields = append(rtb.recordType.Fields, &schema_pb.Field{
|
||||
Name: name,
|
||||
Type: &schema_pb.Type{Kind: &schema_pb.Type_RecordType{RecordType: recordType}},
|
||||
})
|
||||
return rtb
|
||||
}
|
||||
|
||||
func ListOf(elementType *schema_pb.Type) *schema_pb.Type {
|
||||
return &schema_pb.Type{Kind: &schema_pb.Type_ListType{ListType: &schema_pb.ListType{ElementType: elementType}}}
|
||||
}
|
||||
86
weed/mq/schema/schema_test.go
Normal file
86
weed/mq/schema/schema_test.go
Normal file
@@ -0,0 +1,86 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
|
||||
"github.com/golang/protobuf/proto"
|
||||
. "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestEnumScalarType(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
enum ScalarType
|
||||
expected int32
|
||||
}{
|
||||
{"Boolean", ScalarType_BOOL, 0},
|
||||
{"Integer", ScalarType_INT32, 1},
|
||||
{"Long", ScalarType_INT64, 3},
|
||||
{"Float", ScalarType_FLOAT, 4},
|
||||
{"Double", ScalarType_DOUBLE, 5},
|
||||
{"Bytes", ScalarType_BYTES, 6},
|
||||
{"String", ScalarType_STRING, 7},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equal(t, tt.expected, int32(tt.enum))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestField(t *testing.T) {
|
||||
field := &Field{
|
||||
Name: "field_name",
|
||||
Type: &Type{Kind: &Type_ScalarType{ScalarType: ScalarType_INT32}},
|
||||
FieldIndex: 1,
|
||||
IsRepeated: false,
|
||||
}
|
||||
assert.NotNil(t, field)
|
||||
}
|
||||
|
||||
func TestRecordType(t *testing.T) {
|
||||
subRecord := &RecordType{
|
||||
Fields: []*Field{
|
||||
{
|
||||
Name: "field_1",
|
||||
Type: &Type{Kind: &Type_ScalarType{ScalarType: ScalarType_INT32}},
|
||||
FieldIndex: 1,
|
||||
IsRepeated: false,
|
||||
},
|
||||
{
|
||||
Name: "field_2",
|
||||
Type: &Type{Kind: &Type_ScalarType{ScalarType: ScalarType_STRING}},
|
||||
FieldIndex: 2,
|
||||
IsRepeated: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
record := &RecordType{
|
||||
Fields: []*Field{
|
||||
{
|
||||
Name: "field_key",
|
||||
Type: &Type{Kind: &Type_ScalarType{ScalarType: ScalarType_INT32}},
|
||||
FieldIndex: 1,
|
||||
IsRepeated: false,
|
||||
},
|
||||
{
|
||||
Name: "field_record",
|
||||
Type: &Type{Kind: &Type_RecordType{RecordType: subRecord}},
|
||||
FieldIndex: 2,
|
||||
IsRepeated: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// serialize record to protobuf text marshalling
|
||||
text := proto.MarshalTextString(record)
|
||||
println(text)
|
||||
|
||||
bytes, _ := json.Marshal(record)
|
||||
println(string(bytes))
|
||||
|
||||
assert.NotNil(t, record)
|
||||
}
|
||||
105
weed/mq/schema/struct_to_schema.go
Normal file
105
weed/mq/schema/struct_to_schema.go
Normal file
@@ -0,0 +1,105 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func StructToSchema(instance any) *schema_pb.RecordType {
|
||||
myType := reflect.TypeOf(instance)
|
||||
if myType.Kind() != reflect.Struct {
|
||||
return nil
|
||||
}
|
||||
st := reflectTypeToSchemaType(myType)
|
||||
return st.GetRecordType()
|
||||
}
|
||||
|
||||
// CreateCombinedRecordType creates a combined RecordType that includes fields from both key and value schemas
|
||||
// Key fields are prefixed with "key_" to distinguish them from value fields
|
||||
func CreateCombinedRecordType(keyRecordType *schema_pb.RecordType, valueRecordType *schema_pb.RecordType) *schema_pb.RecordType {
|
||||
var combinedFields []*schema_pb.Field
|
||||
|
||||
// Add key fields with "key_" prefix
|
||||
if keyRecordType != nil {
|
||||
for _, field := range keyRecordType.Fields {
|
||||
keyField := &schema_pb.Field{
|
||||
Name: "key_" + field.Name,
|
||||
FieldIndex: field.FieldIndex, // Will be reindexed later
|
||||
Type: field.Type,
|
||||
IsRepeated: field.IsRepeated,
|
||||
IsRequired: field.IsRequired,
|
||||
}
|
||||
combinedFields = append(combinedFields, keyField)
|
||||
}
|
||||
}
|
||||
|
||||
// Add value fields (no prefix)
|
||||
if valueRecordType != nil {
|
||||
for _, field := range valueRecordType.Fields {
|
||||
combinedFields = append(combinedFields, field)
|
||||
}
|
||||
}
|
||||
|
||||
// Reindex all fields to have sequential indices
|
||||
for i, field := range combinedFields {
|
||||
field.FieldIndex = int32(i)
|
||||
}
|
||||
|
||||
return &schema_pb.RecordType{
|
||||
Fields: combinedFields,
|
||||
}
|
||||
}
|
||||
|
||||
func reflectTypeToSchemaType(t reflect.Type) *schema_pb.Type {
|
||||
switch t.Kind() {
|
||||
case reflect.Bool:
|
||||
return TypeBoolean
|
||||
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32:
|
||||
return TypeInt32
|
||||
case reflect.Int64:
|
||||
return TypeInt64
|
||||
case reflect.Float32:
|
||||
return TypeFloat
|
||||
case reflect.Float64:
|
||||
return TypeDouble
|
||||
case reflect.String:
|
||||
return TypeString
|
||||
case reflect.Slice:
|
||||
switch t.Elem().Kind() {
|
||||
case reflect.Uint8:
|
||||
return TypeBytes
|
||||
default:
|
||||
if st := reflectTypeToSchemaType(t.Elem()); st != nil {
|
||||
return &schema_pb.Type{
|
||||
Kind: &schema_pb.Type_ListType{
|
||||
ListType: &schema_pb.ListType{
|
||||
ElementType: st,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
case reflect.Struct:
|
||||
recordType := &schema_pb.RecordType{}
|
||||
for i := 0; i < t.NumField(); i++ {
|
||||
field := t.Field(i)
|
||||
fieldType := field.Type
|
||||
fieldName := field.Name
|
||||
schemaField := reflectTypeToSchemaType(fieldType)
|
||||
if schemaField == nil {
|
||||
return nil
|
||||
}
|
||||
recordType.Fields = append(recordType.Fields, &schema_pb.Field{
|
||||
Name: fieldName,
|
||||
Type: schemaField,
|
||||
})
|
||||
}
|
||||
return &schema_pb.Type{
|
||||
Kind: &schema_pb.Type_RecordType{
|
||||
RecordType: recordType,
|
||||
},
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
120
weed/mq/schema/struct_to_schema_test.go
Normal file
120
weed/mq/schema/struct_to_schema_test.go
Normal file
@@ -0,0 +1,120 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestStructToSchema(t *testing.T) {
|
||||
type args struct {
|
||||
instance any
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *schema_pb.RecordType
|
||||
}{
|
||||
{
|
||||
name: "scalar type",
|
||||
args: args{
|
||||
instance: 1,
|
||||
},
|
||||
want: nil,
|
||||
},
|
||||
{
|
||||
name: "simple struct type",
|
||||
args: args{
|
||||
instance: struct {
|
||||
Field1 int
|
||||
Field2 string
|
||||
}{},
|
||||
},
|
||||
want: RecordTypeBegin().
|
||||
WithField("Field1", TypeInt32).
|
||||
WithField("Field2", TypeString).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
{
|
||||
name: "simple list",
|
||||
args: args{
|
||||
instance: struct {
|
||||
Field1 []int
|
||||
Field2 string
|
||||
}{},
|
||||
},
|
||||
want: RecordTypeBegin().
|
||||
WithField("Field1", ListOf(TypeInt32)).
|
||||
WithField("Field2", TypeString).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
{
|
||||
name: "simple []byte",
|
||||
args: args{
|
||||
instance: struct {
|
||||
Field2 []byte
|
||||
}{},
|
||||
},
|
||||
want: RecordTypeBegin().
|
||||
WithField("Field2", TypeBytes).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
{
|
||||
name: "nested simple structs",
|
||||
args: args{
|
||||
instance: struct {
|
||||
Field1 int
|
||||
Field2 struct {
|
||||
Field3 string
|
||||
Field4 int
|
||||
}
|
||||
}{},
|
||||
},
|
||||
want: RecordTypeBegin().
|
||||
WithField("Field1", TypeInt32).
|
||||
WithRecordField("Field2",
|
||||
RecordTypeBegin().
|
||||
WithField("Field3", TypeString).
|
||||
WithField("Field4", TypeInt32).
|
||||
RecordTypeEnd(),
|
||||
).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
{
|
||||
name: "nested struct type",
|
||||
args: args{
|
||||
instance: struct {
|
||||
Field1 int
|
||||
Field2 struct {
|
||||
Field3 string
|
||||
Field4 []int
|
||||
Field5 struct {
|
||||
Field6 string
|
||||
Field7 []byte
|
||||
}
|
||||
}
|
||||
}{},
|
||||
},
|
||||
want: RecordTypeBegin().
|
||||
WithField("Field1", TypeInt32).
|
||||
WithRecordField("Field2", RecordTypeBegin().
|
||||
WithField("Field3", TypeString).
|
||||
WithField("Field4", ListOf(TypeInt32)).
|
||||
WithRecordField("Field5",
|
||||
RecordTypeBegin().
|
||||
WithField("Field6", TypeString).
|
||||
WithField("Field7", TypeBytes).
|
||||
RecordTypeEnd(),
|
||||
).RecordTypeEnd(),
|
||||
).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
assert.Equalf(t, tt.want, StructToSchema(tt.args.instance), "StructToSchema(%v)", tt.args.instance)
|
||||
})
|
||||
}
|
||||
}
|
||||
59
weed/mq/schema/to_parquet_levels.go
Normal file
59
weed/mq/schema/to_parquet_levels.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
type ParquetLevels struct {
|
||||
startColumnIndex int
|
||||
endColumnIndex int
|
||||
definitionDepth int
|
||||
levels map[string]*ParquetLevels
|
||||
}
|
||||
|
||||
func ToParquetLevels(recordType *schema_pb.RecordType) (*ParquetLevels, error) {
|
||||
return toRecordTypeLevels(recordType, 0, 0)
|
||||
}
|
||||
|
||||
func toFieldTypeLevels(fieldType *schema_pb.Type, startColumnIndex, definitionDepth int) (*ParquetLevels, error) {
|
||||
switch fieldType.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
return toFieldTypeScalarLevels(fieldType.GetScalarType(), startColumnIndex, definitionDepth)
|
||||
case *schema_pb.Type_RecordType:
|
||||
return toRecordTypeLevels(fieldType.GetRecordType(), startColumnIndex, definitionDepth)
|
||||
case *schema_pb.Type_ListType:
|
||||
return toFieldTypeListLevels(fieldType.GetListType(), startColumnIndex, definitionDepth)
|
||||
}
|
||||
return nil, fmt.Errorf("unknown field type: %T", fieldType.Kind)
|
||||
}
|
||||
|
||||
func toFieldTypeListLevels(listType *schema_pb.ListType, startColumnIndex, definitionDepth int) (*ParquetLevels, error) {
|
||||
return toFieldTypeLevels(listType.ElementType, startColumnIndex, definitionDepth)
|
||||
}
|
||||
|
||||
func toFieldTypeScalarLevels(scalarType schema_pb.ScalarType, startColumnIndex, definitionDepth int) (*ParquetLevels, error) {
|
||||
return &ParquetLevels{
|
||||
startColumnIndex: startColumnIndex,
|
||||
endColumnIndex: startColumnIndex + 1,
|
||||
definitionDepth: definitionDepth,
|
||||
}, nil
|
||||
}
|
||||
func toRecordTypeLevels(recordType *schema_pb.RecordType, startColumnIndex, definitionDepth int) (*ParquetLevels, error) {
|
||||
recordTypeLevels := &ParquetLevels{
|
||||
startColumnIndex: startColumnIndex,
|
||||
definitionDepth: definitionDepth,
|
||||
levels: make(map[string]*ParquetLevels),
|
||||
}
|
||||
for _, field := range recordType.Fields {
|
||||
fieldTypeLevels, err := toFieldTypeLevels(field.Type, startColumnIndex, definitionDepth+1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recordTypeLevels.levels[field.Name] = fieldTypeLevels
|
||||
startColumnIndex = fieldTypeLevels.endColumnIndex
|
||||
}
|
||||
recordTypeLevels.endColumnIndex = startColumnIndex
|
||||
return recordTypeLevels, nil
|
||||
}
|
||||
103
weed/mq/schema/to_parquet_levels_test.go
Normal file
103
weed/mq/schema/to_parquet_levels_test.go
Normal file
@@ -0,0 +1,103 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestToParquetLevels(t *testing.T) {
|
||||
type args struct {
|
||||
recordType *schema_pb.RecordType
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
want *ParquetLevels
|
||||
}{
|
||||
{
|
||||
name: "nested type",
|
||||
args: args{
|
||||
RecordTypeBegin().
|
||||
WithField("ID", TypeInt64).
|
||||
WithField("CreatedAt", TypeInt64).
|
||||
WithRecordField("Person",
|
||||
RecordTypeBegin().
|
||||
WithField("zName", TypeString).
|
||||
WithField("emails", ListOf(TypeString)).
|
||||
RecordTypeEnd()).
|
||||
WithField("Company", TypeString).
|
||||
WithRecordField("Address",
|
||||
RecordTypeBegin().
|
||||
WithField("Street", TypeString).
|
||||
WithField("City", TypeString).
|
||||
RecordTypeEnd()).
|
||||
RecordTypeEnd(),
|
||||
},
|
||||
want: &ParquetLevels{
|
||||
startColumnIndex: 0,
|
||||
endColumnIndex: 7,
|
||||
definitionDepth: 0,
|
||||
levels: map[string]*ParquetLevels{
|
||||
"Address": {
|
||||
startColumnIndex: 0,
|
||||
endColumnIndex: 2,
|
||||
definitionDepth: 1,
|
||||
levels: map[string]*ParquetLevels{
|
||||
"City": {
|
||||
startColumnIndex: 0,
|
||||
endColumnIndex: 1,
|
||||
definitionDepth: 2,
|
||||
},
|
||||
"Street": {
|
||||
startColumnIndex: 1,
|
||||
endColumnIndex: 2,
|
||||
definitionDepth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
"Company": {
|
||||
startColumnIndex: 2,
|
||||
endColumnIndex: 3,
|
||||
definitionDepth: 1,
|
||||
},
|
||||
"CreatedAt": {
|
||||
startColumnIndex: 3,
|
||||
endColumnIndex: 4,
|
||||
definitionDepth: 1,
|
||||
},
|
||||
"ID": {
|
||||
startColumnIndex: 4,
|
||||
endColumnIndex: 5,
|
||||
definitionDepth: 1,
|
||||
},
|
||||
"Person": {
|
||||
startColumnIndex: 5,
|
||||
endColumnIndex: 7,
|
||||
definitionDepth: 1,
|
||||
levels: map[string]*ParquetLevels{
|
||||
"emails": {
|
||||
startColumnIndex: 5,
|
||||
endColumnIndex: 6,
|
||||
definitionDepth: 2,
|
||||
},
|
||||
"zName": {
|
||||
startColumnIndex: 6,
|
||||
endColumnIndex: 7,
|
||||
definitionDepth: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got, err := ToParquetLevels(tt.args.recordType)
|
||||
assert.Nil(t, err)
|
||||
assert.Equalf(t, tt.want, got, "ToParquetLevels(%v)", tt.args.recordType)
|
||||
})
|
||||
}
|
||||
}
|
||||
117
weed/mq/schema/to_parquet_schema.go
Normal file
117
weed/mq/schema/to_parquet_schema.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
parquet "github.com/parquet-go/parquet-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func ToParquetSchema(topicName string, recordType *schema_pb.RecordType) (*parquet.Schema, error) {
|
||||
rootNode, err := toParquetFieldTypeRecord(recordType)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert record type to parquet schema: %w", err)
|
||||
}
|
||||
|
||||
// Fields are sorted by name, so the value should be sorted also
|
||||
// the sorting is inside parquet.`func (g Group) Fields() []Field`
|
||||
return parquet.NewSchema(topicName, rootNode), nil
|
||||
}
|
||||
|
||||
func toParquetFieldType(fieldType *schema_pb.Type) (dataType parquet.Node, err error) {
|
||||
// This is the old function - now defaults to Optional for backward compatibility
|
||||
return toParquetFieldTypeWithRequirement(fieldType, false)
|
||||
}
|
||||
|
||||
func toParquetFieldTypeList(listType *schema_pb.ListType) (parquet.Node, error) {
|
||||
elementType, err := toParquetFieldType(listType.ElementType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parquet.Repeated(elementType), nil
|
||||
}
|
||||
|
||||
func toParquetFieldTypeScalar(scalarType schema_pb.ScalarType) (parquet.Node, error) {
|
||||
switch scalarType {
|
||||
case schema_pb.ScalarType_BOOL:
|
||||
return parquet.Leaf(parquet.BooleanType), nil
|
||||
case schema_pb.ScalarType_INT32:
|
||||
return parquet.Leaf(parquet.Int32Type), nil
|
||||
case schema_pb.ScalarType_INT64:
|
||||
return parquet.Leaf(parquet.Int64Type), nil
|
||||
case schema_pb.ScalarType_FLOAT:
|
||||
return parquet.Leaf(parquet.FloatType), nil
|
||||
case schema_pb.ScalarType_DOUBLE:
|
||||
return parquet.Leaf(parquet.DoubleType), nil
|
||||
case schema_pb.ScalarType_BYTES:
|
||||
return parquet.Leaf(parquet.ByteArrayType), nil
|
||||
case schema_pb.ScalarType_STRING:
|
||||
return parquet.Leaf(parquet.ByteArrayType), nil
|
||||
// Parquet logical types - map to their physical storage types
|
||||
case schema_pb.ScalarType_TIMESTAMP:
|
||||
// Stored as INT64 (microseconds since Unix epoch)
|
||||
return parquet.Leaf(parquet.Int64Type), nil
|
||||
case schema_pb.ScalarType_DATE:
|
||||
// Stored as INT32 (days since Unix epoch)
|
||||
return parquet.Leaf(parquet.Int32Type), nil
|
||||
case schema_pb.ScalarType_DECIMAL:
|
||||
// Use maximum precision/scale to accommodate any decimal value
|
||||
// Per Parquet spec: precision ≤9→INT32, ≤18→INT64, >18→FixedLenByteArray
|
||||
// Using precision=38 (max for most systems), scale=18 for flexibility
|
||||
// Individual values can have smaller precision/scale, but schema supports maximum
|
||||
return parquet.Decimal(18, 38, parquet.FixedLenByteArrayType(16)), nil
|
||||
case schema_pb.ScalarType_TIME:
|
||||
// Stored as INT64 (microseconds since midnight)
|
||||
return parquet.Leaf(parquet.Int64Type), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown scalar type: %v", scalarType)
|
||||
}
|
||||
}
|
||||
func toParquetFieldTypeRecord(recordType *schema_pb.RecordType) (parquet.Node, error) {
|
||||
recordNode := parquet.Group{}
|
||||
for _, field := range recordType.Fields {
|
||||
parquetFieldType, err := toParquetFieldTypeWithRequirement(field.Type, field.IsRequired)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recordNode[field.Name] = parquetFieldType
|
||||
}
|
||||
return recordNode, nil
|
||||
}
|
||||
|
||||
// toParquetFieldTypeWithRequirement creates parquet field type respecting required/optional constraints
|
||||
func toParquetFieldTypeWithRequirement(fieldType *schema_pb.Type, isRequired bool) (dataType parquet.Node, err error) {
|
||||
switch fieldType.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
dataType, err = toParquetFieldTypeScalar(fieldType.GetScalarType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isRequired {
|
||||
// Required fields are NOT wrapped in Optional
|
||||
return dataType, nil
|
||||
} else {
|
||||
// Optional fields are wrapped in Optional
|
||||
return parquet.Optional(dataType), nil
|
||||
}
|
||||
case *schema_pb.Type_RecordType:
|
||||
dataType, err = toParquetFieldTypeRecord(fieldType.GetRecordType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if isRequired {
|
||||
return dataType, nil
|
||||
} else {
|
||||
return parquet.Optional(dataType), nil
|
||||
}
|
||||
case *schema_pb.Type_ListType:
|
||||
dataType, err = toParquetFieldTypeList(fieldType.GetListType())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Lists are typically optional by nature
|
||||
return dataType, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown field type: %T", fieldType.Kind)
|
||||
}
|
||||
}
|
||||
346
weed/mq/schema/to_parquet_value.go
Normal file
346
weed/mq/schema/to_parquet_value.go
Normal file
@@ -0,0 +1,346 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
|
||||
parquet "github.com/parquet-go/parquet-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func rowBuilderVisit(rowBuilder *parquet.RowBuilder, fieldType *schema_pb.Type, levels *ParquetLevels, fieldValue *schema_pb.Value) (err error) {
|
||||
switch fieldType.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
// If value is missing, write NULL at the correct column to keep rows aligned
|
||||
if fieldValue == nil || fieldValue.Kind == nil {
|
||||
rowBuilder.Add(levels.startColumnIndex, parquet.NullValue())
|
||||
return nil
|
||||
}
|
||||
var parquetValue parquet.Value
|
||||
parquetValue, err = toParquetValueForType(fieldType, fieldValue)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Safety check: prevent nil byte arrays from reaching parquet library
|
||||
if parquetValue.Kind() == parquet.ByteArray {
|
||||
byteData := parquetValue.ByteArray()
|
||||
if byteData == nil {
|
||||
parquetValue = parquet.ByteArrayValue([]byte{})
|
||||
}
|
||||
}
|
||||
|
||||
rowBuilder.Add(levels.startColumnIndex, parquetValue)
|
||||
case *schema_pb.Type_ListType:
|
||||
// Advance to list position even if value is missing
|
||||
rowBuilder.Next(levels.startColumnIndex)
|
||||
if fieldValue == nil || fieldValue.GetListValue() == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
elementType := fieldType.GetListType().ElementType
|
||||
for _, value := range fieldValue.GetListValue().Values {
|
||||
if err = rowBuilderVisit(rowBuilder, elementType, levels, value); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func AddRecordValue(rowBuilder *parquet.RowBuilder, recordType *schema_pb.RecordType, parquetLevels *ParquetLevels, recordValue *schema_pb.RecordValue) error {
|
||||
visitor := func(fieldType *schema_pb.Type, levels *ParquetLevels, fieldValue *schema_pb.Value) (err error) {
|
||||
return rowBuilderVisit(rowBuilder, fieldType, levels, fieldValue)
|
||||
}
|
||||
fieldType := &schema_pb.Type{Kind: &schema_pb.Type_RecordType{RecordType: recordType}}
|
||||
fieldValue := &schema_pb.Value{Kind: &schema_pb.Value_RecordValue{RecordValue: recordValue}}
|
||||
return doVisitValue(fieldType, parquetLevels, fieldValue, visitor)
|
||||
}
|
||||
|
||||
// typeValueVisitor is a function that is called for each value in a schema_pb.Value
|
||||
// Find the column index.
|
||||
// intended to be used in RowBuilder.Add(columnIndex, value)
|
||||
type typeValueVisitor func(fieldType *schema_pb.Type, levels *ParquetLevels, fieldValue *schema_pb.Value) (err error)
|
||||
|
||||
// endIndex is exclusive
|
||||
// same logic as RowBuilder.configure in row_builder.go
|
||||
func doVisitValue(fieldType *schema_pb.Type, levels *ParquetLevels, fieldValue *schema_pb.Value, visitor typeValueVisitor) (err error) {
|
||||
switch fieldType.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
return visitor(fieldType, levels, fieldValue)
|
||||
case *schema_pb.Type_ListType:
|
||||
return visitor(fieldType, levels, fieldValue)
|
||||
case *schema_pb.Type_RecordType:
|
||||
for _, field := range fieldType.GetRecordType().Fields {
|
||||
var fv *schema_pb.Value
|
||||
if fieldValue != nil && fieldValue.GetRecordValue() != nil {
|
||||
var found bool
|
||||
fv, found = fieldValue.GetRecordValue().Fields[field.Name]
|
||||
if !found {
|
||||
// pass nil so visitor can emit NULL for alignment
|
||||
fv = nil
|
||||
}
|
||||
}
|
||||
fieldLevels := levels.levels[field.Name]
|
||||
err = doVisitValue(field.Type, fieldLevels, fv, visitor)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func toParquetValue(value *schema_pb.Value) (parquet.Value, error) {
|
||||
// Safety check for nil value
|
||||
if value == nil || value.Kind == nil {
|
||||
return parquet.NullValue(), fmt.Errorf("nil value or nil value kind")
|
||||
}
|
||||
|
||||
switch value.Kind.(type) {
|
||||
case *schema_pb.Value_BoolValue:
|
||||
return parquet.BooleanValue(value.GetBoolValue()), nil
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.Int32Value(value.GetInt32Value()), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int64Value(value.GetInt64Value()), nil
|
||||
case *schema_pb.Value_FloatValue:
|
||||
return parquet.FloatValue(value.GetFloatValue()), nil
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.DoubleValue(value.GetDoubleValue()), nil
|
||||
case *schema_pb.Value_BytesValue:
|
||||
// Handle nil byte slices to prevent growslice panic in parquet-go
|
||||
byteData := value.GetBytesValue()
|
||||
if byteData == nil {
|
||||
byteData = []byte{} // Use empty slice instead of nil
|
||||
}
|
||||
return parquet.ByteArrayValue(byteData), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
// Convert string to bytes, ensuring we never pass nil
|
||||
stringData := value.GetStringValue()
|
||||
return parquet.ByteArrayValue([]byte(stringData)), nil
|
||||
// Parquet logical types with safe conversion (preventing commit 7a4aeec60 panic)
|
||||
case *schema_pb.Value_TimestampValue:
|
||||
timestampValue := value.GetTimestampValue()
|
||||
if timestampValue == nil {
|
||||
return parquet.NullValue(), nil
|
||||
}
|
||||
return parquet.Int64Value(timestampValue.TimestampMicros), nil
|
||||
case *schema_pb.Value_DateValue:
|
||||
dateValue := value.GetDateValue()
|
||||
if dateValue == nil {
|
||||
return parquet.NullValue(), nil
|
||||
}
|
||||
return parquet.Int32Value(dateValue.DaysSinceEpoch), nil
|
||||
case *schema_pb.Value_DecimalValue:
|
||||
decimalValue := value.GetDecimalValue()
|
||||
if decimalValue == nil || decimalValue.Value == nil || len(decimalValue.Value) == 0 {
|
||||
return parquet.NullValue(), nil
|
||||
}
|
||||
|
||||
// Validate input data - reject unreasonably large values instead of corrupting data
|
||||
if len(decimalValue.Value) > 64 {
|
||||
// Reject extremely large decimal values (>512 bits) as likely corrupted data
|
||||
// Better to fail fast than silently corrupt financial/scientific data
|
||||
return parquet.NullValue(), fmt.Errorf("decimal value too large: %d bytes (max 64)", len(decimalValue.Value))
|
||||
}
|
||||
|
||||
// Convert to FixedLenByteArray to match schema (DECIMAL with FixedLenByteArray physical type)
|
||||
// This accommodates any precision up to 38 digits (16 bytes = 128 bits)
|
||||
|
||||
// Pad or truncate to exactly 16 bytes for FixedLenByteArray
|
||||
fixedBytes := make([]byte, 16)
|
||||
if len(decimalValue.Value) <= 16 {
|
||||
// Right-align the value (big-endian)
|
||||
copy(fixedBytes[16-len(decimalValue.Value):], decimalValue.Value)
|
||||
} else {
|
||||
// Truncate if too large, taking the least significant bytes
|
||||
copy(fixedBytes, decimalValue.Value[len(decimalValue.Value)-16:])
|
||||
}
|
||||
|
||||
return parquet.FixedLenByteArrayValue(fixedBytes), nil
|
||||
case *schema_pb.Value_TimeValue:
|
||||
timeValue := value.GetTimeValue()
|
||||
if timeValue == nil {
|
||||
return parquet.NullValue(), nil
|
||||
}
|
||||
return parquet.Int64Value(timeValue.TimeMicros), nil
|
||||
default:
|
||||
return parquet.NullValue(), fmt.Errorf("unknown value type: %T", value.Kind)
|
||||
}
|
||||
}
|
||||
|
||||
// toParquetValueForType coerces a schema_pb.Value into a parquet.Value that matches the declared field type.
|
||||
func toParquetValueForType(fieldType *schema_pb.Type, value *schema_pb.Value) (parquet.Value, error) {
|
||||
switch t := fieldType.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
switch t.ScalarType {
|
||||
case schema_pb.ScalarType_BOOL:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_BoolValue:
|
||||
return parquet.BooleanValue(v.BoolValue), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if b, err := strconv.ParseBool(v.StringValue); err == nil {
|
||||
return parquet.BooleanValue(b), nil
|
||||
}
|
||||
return parquet.BooleanValue(false), nil
|
||||
default:
|
||||
return parquet.BooleanValue(false), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_INT32:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.Int32Value(v.Int32Value), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int32Value(int32(v.Int64Value)), nil
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.Int32Value(int32(v.DoubleValue)), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if i, err := strconv.ParseInt(v.StringValue, 10, 32); err == nil {
|
||||
return parquet.Int32Value(int32(i)), nil
|
||||
}
|
||||
return parquet.Int32Value(0), nil
|
||||
default:
|
||||
return parquet.Int32Value(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_INT64:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int64Value(v.Int64Value), nil
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.Int64Value(int64(v.Int32Value)), nil
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.Int64Value(int64(v.DoubleValue)), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if i, err := strconv.ParseInt(v.StringValue, 10, 64); err == nil {
|
||||
return parquet.Int64Value(i), nil
|
||||
}
|
||||
return parquet.Int64Value(0), nil
|
||||
default:
|
||||
return parquet.Int64Value(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_FLOAT:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_FloatValue:
|
||||
return parquet.FloatValue(v.FloatValue), nil
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.FloatValue(float32(v.DoubleValue)), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.FloatValue(float32(v.Int64Value)), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if f, err := strconv.ParseFloat(v.StringValue, 32); err == nil {
|
||||
return parquet.FloatValue(float32(f)), nil
|
||||
}
|
||||
return parquet.FloatValue(0), nil
|
||||
default:
|
||||
return parquet.FloatValue(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_DOUBLE:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.DoubleValue(v.DoubleValue), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.DoubleValue(float64(v.Int64Value)), nil
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.DoubleValue(float64(v.Int32Value)), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if f, err := strconv.ParseFloat(v.StringValue, 64); err == nil {
|
||||
return parquet.DoubleValue(f), nil
|
||||
}
|
||||
return parquet.DoubleValue(0), nil
|
||||
default:
|
||||
return parquet.DoubleValue(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_BYTES:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_BytesValue:
|
||||
b := v.BytesValue
|
||||
if b == nil {
|
||||
b = []byte{}
|
||||
}
|
||||
return parquet.ByteArrayValue(b), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
return parquet.ByteArrayValue([]byte(v.StringValue)), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.ByteArrayValue([]byte(strconv.FormatInt(v.Int64Value, 10))), nil
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.ByteArrayValue([]byte(strconv.FormatInt(int64(v.Int32Value), 10))), nil
|
||||
case *schema_pb.Value_DoubleValue:
|
||||
return parquet.ByteArrayValue([]byte(strconv.FormatFloat(v.DoubleValue, 'f', -1, 64))), nil
|
||||
case *schema_pb.Value_FloatValue:
|
||||
return parquet.ByteArrayValue([]byte(strconv.FormatFloat(float64(v.FloatValue), 'f', -1, 32))), nil
|
||||
case *schema_pb.Value_BoolValue:
|
||||
if v.BoolValue {
|
||||
return parquet.ByteArrayValue([]byte("true")), nil
|
||||
}
|
||||
return parquet.ByteArrayValue([]byte("false")), nil
|
||||
default:
|
||||
return parquet.ByteArrayValue([]byte{}), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_STRING:
|
||||
// Same as bytes but semantically string
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_StringValue:
|
||||
return parquet.ByteArrayValue([]byte(v.StringValue)), nil
|
||||
default:
|
||||
// Fallback through bytes coercion
|
||||
b, _ := toParquetValueForType(&schema_pb.Type{Kind: &schema_pb.Type_ScalarType{ScalarType: schema_pb.ScalarType_BYTES}}, value)
|
||||
return b, nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_TIMESTAMP:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int64Value(v.Int64Value), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if i, err := strconv.ParseInt(v.StringValue, 10, 64); err == nil {
|
||||
return parquet.Int64Value(i), nil
|
||||
}
|
||||
return parquet.Int64Value(0), nil
|
||||
default:
|
||||
return parquet.Int64Value(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_DATE:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_Int32Value:
|
||||
return parquet.Int32Value(v.Int32Value), nil
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int32Value(int32(v.Int64Value)), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if i, err := strconv.ParseInt(v.StringValue, 10, 32); err == nil {
|
||||
return parquet.Int32Value(int32(i)), nil
|
||||
}
|
||||
return parquet.Int32Value(0), nil
|
||||
default:
|
||||
return parquet.Int32Value(0), nil
|
||||
}
|
||||
|
||||
case schema_pb.ScalarType_DECIMAL:
|
||||
// Reuse existing conversion path (FixedLenByteArray 16)
|
||||
return toParquetValue(value)
|
||||
|
||||
case schema_pb.ScalarType_TIME:
|
||||
switch v := value.Kind.(type) {
|
||||
case *schema_pb.Value_Int64Value:
|
||||
return parquet.Int64Value(v.Int64Value), nil
|
||||
case *schema_pb.Value_StringValue:
|
||||
if i, err := strconv.ParseInt(v.StringValue, 10, 64); err == nil {
|
||||
return parquet.Int64Value(i), nil
|
||||
}
|
||||
return parquet.Int64Value(0), nil
|
||||
default:
|
||||
return parquet.Int64Value(0), nil
|
||||
}
|
||||
}
|
||||
}
|
||||
// Fallback to generic conversion
|
||||
return toParquetValue(value)
|
||||
}
|
||||
666
weed/mq/schema/to_parquet_value_test.go
Normal file
666
weed/mq/schema/to_parquet_value_test.go
Normal file
@@ -0,0 +1,666 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"math/big"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/parquet-go/parquet-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func TestToParquetValue_BasicTypes(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "BoolValue true",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_BoolValue{BoolValue: true},
|
||||
},
|
||||
expected: parquet.BooleanValue(true),
|
||||
},
|
||||
{
|
||||
name: "Int32Value",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_Int32Value{Int32Value: 42},
|
||||
},
|
||||
expected: parquet.Int32Value(42),
|
||||
},
|
||||
{
|
||||
name: "Int64Value",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_Int64Value{Int64Value: 12345678901234},
|
||||
},
|
||||
expected: parquet.Int64Value(12345678901234),
|
||||
},
|
||||
{
|
||||
name: "FloatValue",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_FloatValue{FloatValue: 3.14159},
|
||||
},
|
||||
expected: parquet.FloatValue(3.14159),
|
||||
},
|
||||
{
|
||||
name: "DoubleValue",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DoubleValue{DoubleValue: 2.718281828},
|
||||
},
|
||||
expected: parquet.DoubleValue(2.718281828),
|
||||
},
|
||||
{
|
||||
name: "BytesValue",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_BytesValue{BytesValue: []byte("hello world")},
|
||||
},
|
||||
expected: parquet.ByteArrayValue([]byte("hello world")),
|
||||
},
|
||||
{
|
||||
name: "BytesValue empty",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_BytesValue{BytesValue: []byte{}},
|
||||
},
|
||||
expected: parquet.ByteArrayValue([]byte{}),
|
||||
},
|
||||
{
|
||||
name: "StringValue",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_StringValue{StringValue: "test string"},
|
||||
},
|
||||
expected: parquet.ByteArrayValue([]byte("test string")),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParquetValue_TimestampValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid TimestampValue UTC",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: 1704067200000000, // 2024-01-01 00:00:00 UTC in microseconds
|
||||
IsUtc: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(1704067200000000),
|
||||
},
|
||||
{
|
||||
name: "Valid TimestampValue local",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: 1704067200000000,
|
||||
IsUtc: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(1704067200000000),
|
||||
},
|
||||
{
|
||||
name: "TimestampValue zero",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: 0,
|
||||
IsUtc: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(0),
|
||||
},
|
||||
{
|
||||
name: "TimestampValue negative (before epoch)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: -1000000, // 1 second before epoch
|
||||
IsUtc: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(-1000000),
|
||||
},
|
||||
{
|
||||
name: "TimestampValue nil pointer",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: nil,
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParquetValue_DateValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid DateValue (2024-01-01)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DateValue{
|
||||
DateValue: &schema_pb.DateValue{
|
||||
DaysSinceEpoch: 19723, // 2024-01-01 = 19723 days since epoch
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int32Value(19723),
|
||||
},
|
||||
{
|
||||
name: "DateValue epoch (1970-01-01)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DateValue{
|
||||
DateValue: &schema_pb.DateValue{
|
||||
DaysSinceEpoch: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int32Value(0),
|
||||
},
|
||||
{
|
||||
name: "DateValue before epoch",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DateValue{
|
||||
DateValue: &schema_pb.DateValue{
|
||||
DaysSinceEpoch: -365, // 1969-01-01
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int32Value(-365),
|
||||
},
|
||||
{
|
||||
name: "DateValue nil pointer",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DateValue{
|
||||
DateValue: nil,
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParquetValue_DecimalValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Small Decimal (precision <= 9) - positive",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(12345)), // 123.45 with scale 2
|
||||
Precision: 5,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(encodeBigIntToBytes(big.NewInt(12345))), // FixedLenByteArray conversion
|
||||
},
|
||||
{
|
||||
name: "Small Decimal (precision <= 9) - negative",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(-12345)),
|
||||
Precision: 5,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(encodeBigIntToBytes(big.NewInt(-12345))), // FixedLenByteArray conversion
|
||||
},
|
||||
{
|
||||
name: "Medium Decimal (9 < precision <= 18)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(123456789012345)),
|
||||
Precision: 15,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(encodeBigIntToBytes(big.NewInt(123456789012345))), // FixedLenByteArray conversion
|
||||
},
|
||||
{
|
||||
name: "Large Decimal (precision > 18)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}, // Large number as bytes
|
||||
Precision: 25,
|
||||
Scale: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray([]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF}), // FixedLenByteArray conversion
|
||||
},
|
||||
{
|
||||
name: "Decimal with zero precision",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(0)),
|
||||
Precision: 0,
|
||||
Scale: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(encodeBigIntToBytes(big.NewInt(0))), // Zero as FixedLenByteArray
|
||||
},
|
||||
{
|
||||
name: "Decimal nil pointer",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: nil,
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
},
|
||||
{
|
||||
name: "Decimal with nil Value bytes",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: nil, // This was the original panic cause
|
||||
Precision: 5,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
},
|
||||
{
|
||||
name: "Decimal with empty Value bytes",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: []byte{}, // Empty slice
|
||||
Precision: 5,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(), // Returns null for empty bytes
|
||||
},
|
||||
{
|
||||
name: "Decimal out of int32 range (stored as binary)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(999999999999)), // Too large for int32
|
||||
Precision: 5, // But precision says int32
|
||||
Scale: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(encodeBigIntToBytes(big.NewInt(999999999999))), // FixedLenByteArray
|
||||
},
|
||||
{
|
||||
name: "Decimal out of int64 range (stored as binary)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: func() []byte {
|
||||
// Create a number larger than int64 max
|
||||
bigNum := new(big.Int)
|
||||
bigNum.SetString("99999999999999999999999999999", 10)
|
||||
return encodeBigIntToBytes(bigNum)
|
||||
}(),
|
||||
Precision: 15, // Says int64 but value is too large
|
||||
Scale: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: createFixedLenByteArray(func() []byte {
|
||||
bigNum := new(big.Int)
|
||||
bigNum.SetString("99999999999999999999999999999", 10)
|
||||
return encodeBigIntToBytes(bigNum)
|
||||
}()), // Large number as FixedLenByteArray (truncated to 16 bytes)
|
||||
},
|
||||
{
|
||||
name: "Decimal extremely large value (should be rejected)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: make([]byte, 100), // 100 bytes > 64 byte limit
|
||||
Precision: 100,
|
||||
Scale: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
wantErr: true, // Should return error instead of corrupting data
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParquetValue_TimeValue(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Valid TimeValue (12:34:56.789)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimeValue{
|
||||
TimeValue: &schema_pb.TimeValue{
|
||||
TimeMicros: 45296789000, // 12:34:56.789 in microseconds since midnight
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(45296789000),
|
||||
},
|
||||
{
|
||||
name: "TimeValue midnight",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimeValue{
|
||||
TimeValue: &schema_pb.TimeValue{
|
||||
TimeMicros: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(0),
|
||||
},
|
||||
{
|
||||
name: "TimeValue end of day (23:59:59.999999)",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimeValue{
|
||||
TimeValue: &schema_pb.TimeValue{
|
||||
TimeMicros: 86399999999, // 23:59:59.999999
|
||||
},
|
||||
},
|
||||
},
|
||||
expected: parquet.Int64Value(86399999999),
|
||||
},
|
||||
{
|
||||
name: "TimeValue nil pointer",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimeValue{
|
||||
TimeValue: nil,
|
||||
},
|
||||
},
|
||||
expected: parquet.NullValue(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestToParquetValue_EdgeCases(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
value *schema_pb.Value
|
||||
expected parquet.Value
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
name: "Nil value",
|
||||
value: &schema_pb.Value{
|
||||
Kind: nil,
|
||||
},
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "Completely nil value",
|
||||
value: nil,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
name: "BytesValue with nil slice",
|
||||
value: &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_BytesValue{BytesValue: nil},
|
||||
},
|
||||
expected: parquet.ByteArrayValue([]byte{}), // Should convert nil to empty slice
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
result, err := toParquetValue(tt.value)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("toParquetValue() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if !tt.wantErr && !parquetValuesEqual(result, tt.expected) {
|
||||
t.Errorf("toParquetValue() = %v, want %v", result, tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Helper function to encode a big.Int to bytes using two's complement representation
|
||||
func encodeBigIntToBytes(n *big.Int) []byte {
|
||||
if n.Sign() == 0 {
|
||||
return []byte{0}
|
||||
}
|
||||
|
||||
// For positive numbers, just use Bytes()
|
||||
if n.Sign() > 0 {
|
||||
return n.Bytes()
|
||||
}
|
||||
|
||||
// For negative numbers, we need two's complement representation
|
||||
bitLen := n.BitLen()
|
||||
if bitLen%8 != 0 {
|
||||
bitLen += 8 - (bitLen % 8) // Round up to byte boundary
|
||||
}
|
||||
byteLen := bitLen / 8
|
||||
if byteLen == 0 {
|
||||
byteLen = 1
|
||||
}
|
||||
|
||||
// Calculate 2^(byteLen*8)
|
||||
modulus := new(big.Int).Lsh(big.NewInt(1), uint(byteLen*8))
|
||||
|
||||
// Convert negative to positive representation: n + 2^(byteLen*8)
|
||||
positive := new(big.Int).Add(n, modulus)
|
||||
|
||||
bytes := positive.Bytes()
|
||||
|
||||
// Pad with leading zeros if needed
|
||||
if len(bytes) < byteLen {
|
||||
padded := make([]byte, byteLen)
|
||||
copy(padded[byteLen-len(bytes):], bytes)
|
||||
return padded
|
||||
}
|
||||
|
||||
return bytes
|
||||
}
|
||||
|
||||
// Helper function to create a FixedLenByteArray(16) matching our conversion logic
|
||||
func createFixedLenByteArray(inputBytes []byte) parquet.Value {
|
||||
fixedBytes := make([]byte, 16)
|
||||
if len(inputBytes) <= 16 {
|
||||
// Right-align the value (big-endian) - same as our conversion logic
|
||||
copy(fixedBytes[16-len(inputBytes):], inputBytes)
|
||||
} else {
|
||||
// Truncate if too large, taking the least significant bytes
|
||||
copy(fixedBytes, inputBytes[len(inputBytes)-16:])
|
||||
}
|
||||
return parquet.FixedLenByteArrayValue(fixedBytes)
|
||||
}
|
||||
|
||||
// Helper function to compare parquet values
|
||||
func parquetValuesEqual(a, b parquet.Value) bool {
|
||||
// Handle both being null
|
||||
if a.IsNull() && b.IsNull() {
|
||||
return true
|
||||
}
|
||||
if a.IsNull() != b.IsNull() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare kind first
|
||||
if a.Kind() != b.Kind() {
|
||||
return false
|
||||
}
|
||||
|
||||
// Compare based on type
|
||||
switch a.Kind() {
|
||||
case parquet.Boolean:
|
||||
return a.Boolean() == b.Boolean()
|
||||
case parquet.Int32:
|
||||
return a.Int32() == b.Int32()
|
||||
case parquet.Int64:
|
||||
return a.Int64() == b.Int64()
|
||||
case parquet.Float:
|
||||
return a.Float() == b.Float()
|
||||
case parquet.Double:
|
||||
return a.Double() == b.Double()
|
||||
case parquet.ByteArray:
|
||||
aBytes := a.ByteArray()
|
||||
bBytes := b.ByteArray()
|
||||
if len(aBytes) != len(bBytes) {
|
||||
return false
|
||||
}
|
||||
for i, v := range aBytes {
|
||||
if v != bBytes[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
case parquet.FixedLenByteArray:
|
||||
aBytes := a.ByteArray() // FixedLenByteArray also uses ByteArray() method
|
||||
bBytes := b.ByteArray()
|
||||
if len(aBytes) != len(bBytes) {
|
||||
return false
|
||||
}
|
||||
for i, v := range aBytes {
|
||||
if v != bBytes[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Benchmark tests
|
||||
func BenchmarkToParquetValue_BasicTypes(b *testing.B) {
|
||||
value := &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_Int64Value{Int64Value: 12345678901234},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = toParquetValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToParquetValue_TimestampValue(b *testing.B) {
|
||||
value := &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: time.Now().UnixMicro(),
|
||||
IsUtc: true,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = toParquetValue(value)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkToParquetValue_DecimalValue(b *testing.B) {
|
||||
value := &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: encodeBigIntToBytes(big.NewInt(123456789012345)),
|
||||
Precision: 15,
|
||||
Scale: 2,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _ = toParquetValue(value)
|
||||
}
|
||||
}
|
||||
146
weed/mq/schema/to_schema_value.go
Normal file
146
weed/mq/schema/to_schema_value.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/parquet-go/parquet-go"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
// ToRecordValue converts a parquet.Row to a schema_pb.RecordValue
|
||||
// This does not work or did not test with nested structures.
|
||||
// Using this may fail to convert the parquet.Row to schema_pb.RecordValue
|
||||
func ToRecordValue(recordType *schema_pb.RecordType, parquetLevels *ParquetLevels, row parquet.Row) (*schema_pb.RecordValue, error) {
|
||||
values := []parquet.Value(row)
|
||||
recordValue, _, err := toRecordValue(recordType, parquetLevels, values, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return recordValue.GetRecordValue(), nil
|
||||
}
|
||||
|
||||
func ToValue(t *schema_pb.Type, levels *ParquetLevels, values []parquet.Value, valueIndex int) (value *schema_pb.Value, endValueIndex int, err error) {
|
||||
switch t.Kind.(type) {
|
||||
case *schema_pb.Type_ScalarType:
|
||||
return toScalarValue(t.GetScalarType(), levels, values, valueIndex)
|
||||
case *schema_pb.Type_ListType:
|
||||
return toListValue(t.GetListType(), levels, values, valueIndex)
|
||||
case *schema_pb.Type_RecordType:
|
||||
return toRecordValue(t.GetRecordType(), levels, values, valueIndex)
|
||||
}
|
||||
return nil, valueIndex, fmt.Errorf("unsupported type: %v", t)
|
||||
}
|
||||
|
||||
func toRecordValue(recordType *schema_pb.RecordType, levels *ParquetLevels, values []parquet.Value, valueIndex int) (*schema_pb.Value, int, error) {
|
||||
recordValue := schema_pb.RecordValue{Fields: make(map[string]*schema_pb.Value)}
|
||||
for _, field := range recordType.Fields {
|
||||
fieldLevels := levels.levels[field.Name]
|
||||
fieldValue, endValueIndex, err := ToValue(field.Type, fieldLevels, values, valueIndex)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
valueIndex = endValueIndex
|
||||
recordValue.Fields[field.Name] = fieldValue
|
||||
}
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_RecordValue{RecordValue: &recordValue}}, valueIndex, nil
|
||||
}
|
||||
|
||||
func toListValue(listType *schema_pb.ListType, levels *ParquetLevels, values []parquet.Value, valueIndex int) (listValue *schema_pb.Value, endValueIndex int, err error) {
|
||||
listValues := make([]*schema_pb.Value, 0)
|
||||
var value *schema_pb.Value
|
||||
for valueIndex < len(values) {
|
||||
if values[valueIndex].Column() != levels.startColumnIndex {
|
||||
break
|
||||
}
|
||||
value, valueIndex, err = ToValue(listType.ElementType, levels, values, valueIndex)
|
||||
if err != nil {
|
||||
return nil, valueIndex, err
|
||||
}
|
||||
listValues = append(listValues, value)
|
||||
}
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_ListValue{ListValue: &schema_pb.ListValue{Values: listValues}}}, valueIndex, nil
|
||||
}
|
||||
|
||||
func toScalarValue(scalarType schema_pb.ScalarType, levels *ParquetLevels, values []parquet.Value, valueIndex int) (*schema_pb.Value, int, error) {
|
||||
value := values[valueIndex]
|
||||
if value.Column() != levels.startColumnIndex {
|
||||
return nil, valueIndex, nil
|
||||
}
|
||||
switch scalarType {
|
||||
case schema_pb.ScalarType_BOOL:
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_BoolValue{BoolValue: value.Boolean()}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_INT32:
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_Int32Value{Int32Value: value.Int32()}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_INT64:
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: value.Int64()}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_FLOAT:
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: value.Float()}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_DOUBLE:
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: value.Double()}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_BYTES:
|
||||
// Handle nil byte arrays from parquet to prevent growslice panic
|
||||
byteData := value.ByteArray()
|
||||
if byteData == nil {
|
||||
byteData = []byte{} // Use empty slice instead of nil
|
||||
}
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_BytesValue{BytesValue: byteData}}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_STRING:
|
||||
// Handle nil byte arrays from parquet to prevent string conversion issues
|
||||
byteData := value.ByteArray()
|
||||
if byteData == nil {
|
||||
byteData = []byte{} // Use empty slice instead of nil
|
||||
}
|
||||
return &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: string(byteData)}}, valueIndex + 1, nil
|
||||
// Parquet logical types - convert from their physical storage back to logical values
|
||||
case schema_pb.ScalarType_TIMESTAMP:
|
||||
// Stored as INT64, convert back to TimestampValue
|
||||
return &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimestampValue{
|
||||
TimestampValue: &schema_pb.TimestampValue{
|
||||
TimestampMicros: value.Int64(),
|
||||
IsUtc: true, // Default to UTC for compatibility
|
||||
},
|
||||
},
|
||||
}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_DATE:
|
||||
// Stored as INT32, convert back to DateValue
|
||||
return &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DateValue{
|
||||
DateValue: &schema_pb.DateValue{
|
||||
DaysSinceEpoch: value.Int32(),
|
||||
},
|
||||
},
|
||||
}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_DECIMAL:
|
||||
// Stored as FixedLenByteArray, convert back to DecimalValue
|
||||
fixedBytes := value.ByteArray() // FixedLenByteArray also uses ByteArray() method
|
||||
if fixedBytes == nil {
|
||||
fixedBytes = []byte{} // Use empty slice instead of nil
|
||||
}
|
||||
// Remove leading zeros to get the minimal representation
|
||||
trimmedBytes := bytes.TrimLeft(fixedBytes, "\x00")
|
||||
if len(trimmedBytes) == 0 {
|
||||
trimmedBytes = []byte{0} // Ensure we have at least one byte for zero
|
||||
}
|
||||
return &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_DecimalValue{
|
||||
DecimalValue: &schema_pb.DecimalValue{
|
||||
Value: trimmedBytes,
|
||||
Precision: 38, // Maximum precision supported by schema
|
||||
Scale: 18, // Maximum scale supported by schema
|
||||
},
|
||||
},
|
||||
}, valueIndex + 1, nil
|
||||
case schema_pb.ScalarType_TIME:
|
||||
// Stored as INT64, convert back to TimeValue
|
||||
return &schema_pb.Value{
|
||||
Kind: &schema_pb.Value_TimeValue{
|
||||
TimeValue: &schema_pb.TimeValue{
|
||||
TimeMicros: value.Int64(),
|
||||
},
|
||||
},
|
||||
}, valueIndex + 1, nil
|
||||
}
|
||||
return nil, valueIndex, fmt.Errorf("unsupported scalar type: %v", scalarType)
|
||||
}
|
||||
113
weed/mq/schema/value_builder.go
Normal file
113
weed/mq/schema/value_builder.go
Normal file
@@ -0,0 +1,113 @@
|
||||
package schema
|
||||
|
||||
import "github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
|
||||
// RecordValueBuilder helps in constructing RecordValue protobuf messages
|
||||
type RecordValueBuilder struct {
|
||||
recordValue *schema_pb.RecordValue
|
||||
}
|
||||
|
||||
// RecordBegin creates a new RecordValueBuilder instance
|
||||
func RecordBegin() *RecordValueBuilder {
|
||||
return &RecordValueBuilder{recordValue: &schema_pb.RecordValue{Fields: make(map[string]*schema_pb.Value)}}
|
||||
}
|
||||
|
||||
// RecordEnd returns the constructed RecordValue message
|
||||
func (rvb *RecordValueBuilder) RecordEnd() *schema_pb.RecordValue {
|
||||
return rvb.recordValue
|
||||
}
|
||||
|
||||
func (rvb *RecordValueBuilder) SetBool(key string, value bool) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_BoolValue{BoolValue: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetInt32(key string, value int32) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_Int32Value{Int32Value: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetInt64(key string, value int64) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetFloat(key string, value float32) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetDouble(key string, value float64) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetBytes(key string, value []byte) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_BytesValue{BytesValue: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetString(key string, value string) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: value}}
|
||||
return rvb
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetRecord(key string, value *schema_pb.RecordValue) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_RecordValue{RecordValue: value}}
|
||||
return rvb
|
||||
}
|
||||
|
||||
func (rvb *RecordValueBuilder) addListValue(key string, values []*schema_pb.Value) *RecordValueBuilder {
|
||||
rvb.recordValue.Fields[key] = &schema_pb.Value{Kind: &schema_pb.Value_ListValue{ListValue: &schema_pb.ListValue{Values: values}}}
|
||||
return rvb
|
||||
}
|
||||
|
||||
func (rvb *RecordValueBuilder) SetBoolList(key string, values ...bool) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_BoolValue{BoolValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetInt32List(key string, values ...int32) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_Int32Value{Int32Value: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetInt64List(key string, values ...int64) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_Int64Value{Int64Value: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetFloatList(key string, values ...float32) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_FloatValue{FloatValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetDoubleList(key string, values ...float64) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_DoubleValue{DoubleValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetBytesList(key string, values ...[]byte) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_BytesValue{BytesValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetStringList(key string, values ...string) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_StringValue{StringValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
func (rvb *RecordValueBuilder) SetRecordList(key string, values ...*schema_pb.RecordValue) *RecordValueBuilder {
|
||||
var listValues []*schema_pb.Value
|
||||
for _, v := range values {
|
||||
listValues = append(listValues, &schema_pb.Value{Kind: &schema_pb.Value_RecordValue{RecordValue: v}})
|
||||
}
|
||||
return rvb.addListValue(key, listValues)
|
||||
}
|
||||
174
weed/mq/schema/write_parquet_test.go
Normal file
174
weed/mq/schema/write_parquet_test.go
Normal file
@@ -0,0 +1,174 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/parquet-go/parquet-go"
|
||||
"github.com/parquet-go/parquet-go/compress/zstd"
|
||||
"github.com/seaweedfs/seaweedfs/weed/pb/schema_pb"
|
||||
)
|
||||
|
||||
func TestWriteReadParquet(t *testing.T) {
|
||||
// create a schema_pb.RecordType
|
||||
recordType := RecordTypeBegin().
|
||||
WithField("ID", TypeInt64).
|
||||
WithField("CreatedAt", TypeInt64).
|
||||
WithRecordField("Person",
|
||||
RecordTypeBegin().
|
||||
WithField("zName", TypeString).
|
||||
WithField("emails", ListOf(TypeString)).
|
||||
RecordTypeEnd()).
|
||||
WithField("Company", TypeString).
|
||||
WithRecordField("Address",
|
||||
RecordTypeBegin().
|
||||
WithField("Street", TypeString).
|
||||
WithField("City", TypeString).
|
||||
RecordTypeEnd()).
|
||||
RecordTypeEnd()
|
||||
fmt.Printf("RecordType: %v\n", recordType)
|
||||
|
||||
// create a parquet schema
|
||||
parquetSchema, err := ToParquetSchema("example", recordType)
|
||||
if err != nil {
|
||||
t.Fatalf("ToParquetSchema failed: %v", err)
|
||||
}
|
||||
fmt.Printf("ParquetSchema: %v\n", parquetSchema)
|
||||
|
||||
fmt.Printf("Go Type: %+v\n", parquetSchema.GoType())
|
||||
|
||||
filename := "example.parquet"
|
||||
|
||||
count := 3
|
||||
|
||||
testWritingParquetFile(t, count, filename, parquetSchema, recordType)
|
||||
|
||||
total := testReadingParquetFile(t, filename, parquetSchema, recordType)
|
||||
|
||||
if total != count {
|
||||
t.Fatalf("total != 128*1024: %v", total)
|
||||
}
|
||||
|
||||
if err = os.Remove(filename); err != nil {
|
||||
t.Fatalf("os.Remove failed: %v", err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func testWritingParquetFile(t *testing.T, count int, filename string, parquetSchema *parquet.Schema, recordType *schema_pb.RecordType) {
|
||||
parquetLevels, err := ToParquetLevels(recordType)
|
||||
if err != nil {
|
||||
t.Fatalf("ToParquetLevels failed: %v", err)
|
||||
}
|
||||
|
||||
// create a parquet file
|
||||
file, err := os.OpenFile(filename, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0664)
|
||||
if err != nil {
|
||||
t.Fatalf("os.Open failed: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
writer := parquet.NewWriter(file, parquetSchema, parquet.Compression(&zstd.Codec{Level: zstd.DefaultLevel}))
|
||||
rowBuilder := parquet.NewRowBuilder(parquetSchema)
|
||||
for i := 0; i < count; i++ {
|
||||
rowBuilder.Reset()
|
||||
// generate random data
|
||||
recordValue := RecordBegin().
|
||||
SetInt64("ID", 1+int64(i)).
|
||||
SetInt64("CreatedAt", 2+2*int64(i)).
|
||||
SetRecord("Person",
|
||||
RecordBegin().
|
||||
SetString("zName", fmt.Sprintf("john_%d", i)).
|
||||
SetStringList("emails",
|
||||
fmt.Sprintf("john_%d@a.com", i),
|
||||
fmt.Sprintf("john_%d@b.com", i),
|
||||
fmt.Sprintf("john_%d@c.com", i),
|
||||
fmt.Sprintf("john_%d@d.com", i),
|
||||
fmt.Sprintf("john_%d@e.com", i)).
|
||||
RecordEnd()).
|
||||
SetString("Company", fmt.Sprintf("company_%d", i)).
|
||||
RecordEnd()
|
||||
AddRecordValue(rowBuilder, recordType, parquetLevels, recordValue)
|
||||
|
||||
if count < 10 {
|
||||
fmt.Printf("Write RecordValue: %v\n", recordValue)
|
||||
}
|
||||
|
||||
row := rowBuilder.Row()
|
||||
|
||||
if count < 10 {
|
||||
fmt.Printf("Build Row: %+v\n", row)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("rowBuilder.Build failed: %v", err)
|
||||
}
|
||||
|
||||
if _, err = writer.WriteRows([]parquet.Row{row}); err != nil {
|
||||
t.Fatalf("writer.Write failed: %v", err)
|
||||
}
|
||||
}
|
||||
if err = writer.Close(); err != nil {
|
||||
t.Fatalf("writer.WriteStop failed: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func testReadingParquetFile(t *testing.T, filename string, parquetSchema *parquet.Schema, recordType *schema_pb.RecordType) (total int) {
|
||||
parquetLevels, err := ToParquetLevels(recordType)
|
||||
if err != nil {
|
||||
t.Fatalf("ToParquetLevels failed: %v", err)
|
||||
}
|
||||
|
||||
// read the parquet file
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
t.Fatalf("os.Open failed: %v", err)
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// Get file info to determine size
|
||||
fileInfo, err := file.Stat()
|
||||
if err != nil {
|
||||
t.Fatalf("file.Stat failed: %v", err)
|
||||
}
|
||||
|
||||
// Create a parquet file from the opened file
|
||||
parquetFile, err := parquet.OpenFile(file, fileInfo.Size())
|
||||
if err != nil {
|
||||
t.Fatalf("parquet.OpenFile failed: %v", err)
|
||||
}
|
||||
|
||||
reader := parquet.NewReader(parquetFile)
|
||||
rows := make([]parquet.Row, 128)
|
||||
for {
|
||||
rowCount, err := reader.ReadRows(rows)
|
||||
|
||||
// Process the rows first, even if EOF is returned
|
||||
for i := 0; i < rowCount; i++ {
|
||||
row := rows[i]
|
||||
// convert parquet row to schema_pb.RecordValue
|
||||
recordValue, err := ToRecordValue(recordType, parquetLevels, row)
|
||||
if err != nil {
|
||||
t.Fatalf("ToRecordValue failed: %v", err)
|
||||
}
|
||||
if rowCount < 10 {
|
||||
fmt.Printf("Read RecordValue: %v\n", recordValue)
|
||||
}
|
||||
}
|
||||
total += rowCount
|
||||
|
||||
// Check for end conditions after processing rows
|
||||
if err != nil {
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
t.Fatalf("reader.Read failed: %v", err)
|
||||
}
|
||||
if rowCount == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Printf("total: %v\n", total)
|
||||
return
|
||||
}
|
||||
Reference in New Issue
Block a user