diff --git a/chdb/driver/arrow.go b/chdb/driver/arrow.go new file mode 100644 index 0000000..3066ca0 --- /dev/null +++ b/chdb/driver/arrow.go @@ -0,0 +1,175 @@ +package chdbdriver + +import ( + "database/sql/driver" + "fmt" + "reflect" + "time" + + "github.com/apache/arrow/go/v15/arrow" + "github.com/apache/arrow/go/v15/arrow/array" + "github.com/apache/arrow/go/v15/arrow/decimal128" + "github.com/apache/arrow/go/v15/arrow/decimal256" + "github.com/apache/arrow/go/v15/arrow/ipc" + "github.com/chdb-io/chdb-go/chdbstable" +) + +type arrowRows struct { + localResult *chdbstable.LocalResult + reader *ipc.FileReader + curRecord arrow.Record + curRow int64 +} + +func (r *arrowRows) Columns() (out []string) { + sch := r.reader.Schema() + for i := 0; i < sch.NumFields(); i++ { + out = append(out, sch.Field(i).Name) + } + return +} + +func (r *arrowRows) Close() error { + if r.curRecord != nil { + r.curRecord = nil + } + // ignore reader close + _ = r.reader.Close() + r.reader = nil + r.localResult = nil + return nil +} + +func (r *arrowRows) Next(dest []driver.Value) error { + if r.curRecord != nil && r.curRow == r.curRecord.NumRows() { + r.curRecord = nil + } + for r.curRecord == nil { + record, err := r.reader.Read() + if err != nil { + return err + } + if record.NumRows() == 0 { + continue + } + r.curRecord = record + r.curRow = 0 + } + + for i, col := range r.curRecord.Columns() { + if col.IsNull(int(r.curRow)) { + dest[i] = nil + continue + } + switch col := col.(type) { + case *array.Boolean: + dest[i] = col.Value(int(r.curRow)) + case *array.Int8: + dest[i] = col.Value(int(r.curRow)) + case *array.Uint8: + dest[i] = col.Value(int(r.curRow)) + case *array.Int16: + dest[i] = col.Value(int(r.curRow)) + case *array.Uint16: + dest[i] = col.Value(int(r.curRow)) + case *array.Int32: + dest[i] = col.Value(int(r.curRow)) + case *array.Uint32: + dest[i] = col.Value(int(r.curRow)) + case *array.Int64: + dest[i] = col.Value(int(r.curRow)) + case *array.Uint64: + dest[i] = col.Value(int(r.curRow)) + case *array.Float32: + dest[i] = col.Value(int(r.curRow)) + case *array.Float64: + dest[i] = col.Value(int(r.curRow)) + case *array.String: + dest[i] = col.Value(int(r.curRow)) + case *array.LargeString: + dest[i] = col.Value(int(r.curRow)) + case *array.Binary: + dest[i] = col.Value(int(r.curRow)) + case *array.LargeBinary: + dest[i] = col.Value(int(r.curRow)) + case *array.Date32: + dest[i] = col.Value(int(r.curRow)).ToTime() + case *array.Date64: + dest[i] = col.Value(int(r.curRow)).ToTime() + case *array.Time32: + dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.Time32Type).Unit) + case *array.Time64: + dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.Time64Type).Unit) + case *array.Timestamp: + dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.TimestampType).Unit) + case *array.Decimal128: + dest[i] = col.Value(int(r.curRow)) + case *array.Decimal256: + dest[i] = col.Value(int(r.curRow)) + default: + return fmt.Errorf( + "not yet implemented populating from columns of type " + col.DataType().String(), + ) + } + } + + r.curRow++ + return nil +} + +func (r *arrowRows) ColumnTypeDatabaseTypeName(index int) string { + return r.reader.Schema().Field(index).Type.String() +} + +func (r *arrowRows) ColumnTypeNullable(index int) (nullable, ok bool) { + return r.reader.Schema().Field(index).Nullable, true +} + +func (r *arrowRows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + typ := r.reader.Schema().Field(index).Type + switch dt := typ.(type) { + case *arrow.Decimal128Type: + return int64(dt.Precision), int64(dt.Scale), true + case *arrow.Decimal256Type: + return int64(dt.Precision), int64(dt.Scale), true + } + return 0, 0, false +} + +func (r *arrowRows) ColumnTypeScanType(index int) reflect.Type { + switch r.reader.Schema().Field(index).Type.ID() { + case arrow.BOOL: + return reflect.TypeOf(false) + case arrow.INT8: + return reflect.TypeOf(int8(0)) + case arrow.UINT8: + return reflect.TypeOf(uint8(0)) + case arrow.INT16: + return reflect.TypeOf(int16(0)) + case arrow.UINT16: + return reflect.TypeOf(uint16(0)) + case arrow.INT32: + return reflect.TypeOf(int32(0)) + case arrow.UINT32: + return reflect.TypeOf(uint32(0)) + case arrow.INT64: + return reflect.TypeOf(int64(0)) + case arrow.UINT64: + return reflect.TypeOf(uint64(0)) + case arrow.FLOAT32: + return reflect.TypeOf(float32(0)) + case arrow.FLOAT64: + return reflect.TypeOf(float64(0)) + case arrow.DECIMAL128: + return reflect.TypeOf(decimal128.Num{}) + case arrow.DECIMAL256: + return reflect.TypeOf(decimal256.Num{}) + case arrow.BINARY: + return reflect.TypeOf([]byte{}) + case arrow.STRING: + return reflect.TypeOf(string("")) + case arrow.TIME32, arrow.TIME64, arrow.DATE32, arrow.DATE64, arrow.TIMESTAMP: + return reflect.TypeOf(time.Time{}) + } + return nil +} diff --git a/chdb/driver/arrow_test.go b/chdb/driver/arrow_test.go new file mode 100644 index 0000000..769db00 --- /dev/null +++ b/chdb/driver/arrow_test.go @@ -0,0 +1,106 @@ +package chdbdriver + +import ( + "database/sql" + "fmt" + "os" + "testing" + + "github.com/chdb-io/chdb-go/chdb" +) + +func TestDbWithArrow(t *testing.T) { + + db, err := sql.Open("chdb", fmt.Sprintf("driverType=%s", "ARROW")) + if err != nil { + t.Errorf("open db fail, err:%s", err) + } + if db.Ping() != nil { + t.Errorf("ping db fail") + } + rows, err := db.Query(`SELECT 1,'abc'`) + if err != nil { + t.Errorf("run Query fail, err:%s", err) + } + cols, err := rows.Columns() + if err != nil { + t.Errorf("get result columns fail, err: %s", err) + } + if len(cols) != 2 { + t.Errorf("select result columns length should be 2") + } + var ( + bar int + foo string + ) + defer rows.Close() + for rows.Next() { + err := rows.Scan(&bar, &foo) + if err != nil { + t.Errorf("scan fail, err: %s", err) + } + if bar != 1 { + t.Errorf("expected error") + } + if foo != "abc" { + t.Errorf("expected error") + } + } +} + +func TestDBWithArrowSession(t *testing.T) { + sessionDir, err := os.MkdirTemp("", "unittest-sessiondata") + if err != nil { + t.Fatalf("create temp directory fail, err: %s", err) + } + defer os.RemoveAll(sessionDir) + session, err := chdb.NewSession(sessionDir) + if err != nil { + t.Fatalf("new session fail, err: %s", err) + } + defer session.Cleanup() + + session.Query("CREATE DATABASE IF NOT EXISTS testdb; " + + "CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;") + + session.Query("INSERT INTO testdb.testtable VALUES (1), (2), (3);") + + ret, err := session.Query("SELECT * FROM testdb.testtable;") + if err != nil { + t.Fatalf("Query fail, err: %s", err) + } + if string(ret.Buf()) != "1\n2\n3\n" { + t.Errorf("Query result should be 1\n2\n3\n, got %s", string(ret.Buf())) + } + db, err := sql.Open("chdb", fmt.Sprintf("session=%s;driverType=%s", sessionDir, "ARROW")) + if err != nil { + t.Fatalf("open db fail, err: %s", err) + } + if db.Ping() != nil { + t.Fatalf("ping db fail, err: %s", err) + } + rows, err := db.Query("select * from testdb.testtable;") + if err != nil { + t.Fatalf("exec create function fail, err: %s", err) + } + defer rows.Close() + cols, err := rows.Columns() + if err != nil { + t.Fatalf("get result columns fail, err: %s", err) + } + if len(cols) != 1 { + t.Fatalf("result columns length shoule be 3, actual: %d", len(cols)) + } + var bar = 0 + var count = 1 + for rows.Next() { + err = rows.Scan(&bar) + if err != nil { + t.Fatalf("scan fail, err: %s", err) + } + if bar != count { + t.Fatalf("result is not match, want: %d actual: %d", count, bar) + } + count++ + } +} diff --git a/chdb/driver/driver.go b/chdb/driver/driver.go index cc422c8..9c2061e 100644 --- a/chdb/driver/driver.go +++ b/chdb/driver/driver.go @@ -6,22 +6,67 @@ import ( "database/sql" "database/sql/driver" "fmt" - "reflect" + "strconv" "strings" - "time" - "github.com/apache/arrow/go/v14/arrow" - "github.com/apache/arrow/go/v14/arrow/array" - "github.com/apache/arrow/go/v14/arrow/decimal128" - "github.com/apache/arrow/go/v14/arrow/decimal256" "github.com/chdb-io/chdb-go/chdb" "github.com/chdb-io/chdb-go/chdbstable" + "github.com/parquet-go/parquet-go" - "github.com/apache/arrow/go/v14/arrow/ipc" + "github.com/apache/arrow/go/v15/arrow/ipc" +) + +type DriverType int + +const ( + ARROW DriverType = iota + PARQUET + INVALID ) const sessionOptionKey = "session" const udfPathOptionKey = "udfPath" +const driverTypeKey = "driverType" +const driverBufferSizeKey = "bufferSize" + +const defaultBufferSize = 512 + +func (d DriverType) String() string { + switch d { + case ARROW: + return "Arrow" + case PARQUET: + return "Parquet" + case INVALID: + return "Invalid" + } + return "" +} + +func (d DriverType) PrepareRows(result *chdbstable.LocalResult, buf []byte, bufSize int) (driver.Rows, error) { + switch d { + case ARROW: + reader, err := ipc.NewFileReader(bytes.NewReader(buf)) + if err != nil { + return nil, err + } + return &arrowRows{localResult: result, reader: reader}, nil + case PARQUET: + reader := parquet.NewGenericReader[any](bytes.NewReader(buf)) + return &parquetRows{localResult: result, reader: reader, bufferSize: bufSize, needNewBuffer: true}, nil + } + return nil, fmt.Errorf("Unsupported driver type") +} + +func parseDriverType(s string) DriverType { + switch strings.ToUpper(s) { + case "ARROW": + return ARROW + case "PARQUET": + return PARQUET + } + return INVALID +} func init() { sql.Register("chdb", Driver{}) @@ -30,13 +75,18 @@ func init() { type queryHandle func(string, ...string) (*chdbstable.LocalResult, error) type connector struct { - udfPath string - session *chdb.Session + udfPath string + driverType DriverType + bufferSize int + session *chdb.Session } // Connect returns a connection to a database. func (c *connector) Connect(ctx context.Context) (driver.Conn, error) { - cc := &conn{udfPath: c.udfPath, session: c.session} + if c.driverType == INVALID { + return nil, fmt.Errorf("DriverType not supported") + } + cc := &conn{udfPath: c.udfPath, session: c.session, driverType: c.driverType, bufferSize: c.bufferSize} cc.SetupQueryFun() return cc, nil } @@ -70,6 +120,24 @@ func NewConnect(opts map[string]string) (ret *connector, err error) { return nil, err } } + driverType, ok := opts[driverTypeKey] + if ok { + ret.driverType = parseDriverType(driverType) + } else { + ret.driverType = ARROW //default to arrow + } + bufferSize, ok := opts[driverBufferSizeKey] + if ok { + sz, err := strconv.Atoi(bufferSize) + if err != nil { + ret.bufferSize = defaultBufferSize + } else { + ret.bufferSize = sz + } + } else { + ret.bufferSize = defaultBufferSize + } + udfPath, ok := opts[udfPathOptionKey] if ok { ret.udfPath = udfPath @@ -98,9 +166,11 @@ func (d Driver) OpenConnector(name string) (driver.Connector, error) { } type conn struct { - udfPath string - session *chdb.Session - QueryFun queryHandle + udfPath string + driverType DriverType + bufferSize int + session *chdb.Session + QueryFun queryHandle } func (c *conn) Close() error { @@ -127,19 +197,18 @@ func (c *conn) Query(query string, values []driver.Value) (driver.Rows, error) { } func (c *conn) QueryContext(ctx context.Context, query string, args []driver.NamedValue) (driver.Rows, error) { - result, err := c.QueryFun(query, "Arrow", c.udfPath) + + result, err := c.QueryFun(query, c.driverType.String(), c.udfPath) if err != nil { return nil, err } + buf := result.Buf() if buf == nil { return nil, fmt.Errorf("result is nil") } - reader, err := ipc.NewFileReader(bytes.NewReader(buf)) - if err != nil { - return nil, err - } - return &rows{localResult: result, reader: reader}, nil + return c.driverType.PrepareRows(result, buf, c.bufferSize) + } func (c *conn) Begin() (driver.Tx, error) { @@ -157,163 +226,3 @@ func (c *conn) PrepareContext(ctx context.Context, query string) (driver.Stmt, e // todo: func(c *conn) Prepare(query string) // todo: func(c *conn) PrepareContext(ctx context.Context, query string) // todo: prepared statment - -type rows struct { - localResult *chdbstable.LocalResult - reader *ipc.FileReader - curRecord arrow.Record - curRow int64 -} - -func (r *rows) Columns() (out []string) { - sch := r.reader.Schema() - for i := 0; i < sch.NumFields(); i++ { - out = append(out, sch.Field(i).Name) - } - return -} - -func (r *rows) Close() error { - if r.curRecord != nil { - r.curRecord = nil - } - // ignore reader close - _ = r.reader.Close() - r.reader = nil - r.localResult = nil - return nil -} - -func (r *rows) Next(dest []driver.Value) error { - if r.curRecord != nil && r.curRow == r.curRecord.NumRows() { - r.curRecord = nil - } - for r.curRecord == nil { - record, err := r.reader.Read() - if err != nil { - return err - } - if record.NumRows() == 0 { - continue - } - r.curRecord = record - r.curRow = 0 - } - - for i, col := range r.curRecord.Columns() { - if col.IsNull(int(r.curRow)) { - dest[i] = nil - continue - } - switch col := col.(type) { - case *array.Boolean: - dest[i] = col.Value(int(r.curRow)) - case *array.Int8: - dest[i] = col.Value(int(r.curRow)) - case *array.Uint8: - dest[i] = col.Value(int(r.curRow)) - case *array.Int16: - dest[i] = col.Value(int(r.curRow)) - case *array.Uint16: - dest[i] = col.Value(int(r.curRow)) - case *array.Int32: - dest[i] = col.Value(int(r.curRow)) - case *array.Uint32: - dest[i] = col.Value(int(r.curRow)) - case *array.Int64: - dest[i] = col.Value(int(r.curRow)) - case *array.Uint64: - dest[i] = col.Value(int(r.curRow)) - case *array.Float32: - dest[i] = col.Value(int(r.curRow)) - case *array.Float64: - dest[i] = col.Value(int(r.curRow)) - case *array.String: - dest[i] = col.Value(int(r.curRow)) - case *array.LargeString: - dest[i] = col.Value(int(r.curRow)) - case *array.Binary: - dest[i] = col.Value(int(r.curRow)) - case *array.LargeBinary: - dest[i] = col.Value(int(r.curRow)) - case *array.Date32: - dest[i] = col.Value(int(r.curRow)).ToTime() - case *array.Date64: - dest[i] = col.Value(int(r.curRow)).ToTime() - case *array.Time32: - dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.Time32Type).Unit) - case *array.Time64: - dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.Time64Type).Unit) - case *array.Timestamp: - dest[i] = col.Value(int(r.curRow)).ToTime(col.DataType().(*arrow.TimestampType).Unit) - case *array.Decimal128: - dest[i] = col.Value(int(r.curRow)) - case *array.Decimal256: - dest[i] = col.Value(int(r.curRow)) - default: - return fmt.Errorf( - "not yet implemented populating from columns of type " + col.DataType().String(), - ) - } - } - - r.curRow++ - return nil -} - -func (r *rows) ColumnTypeDatabaseTypeName(index int) string { - return r.reader.Schema().Field(index).Type.String() -} - -func (r *rows) ColumnTypeNullable(index int) (nullable, ok bool) { - return r.reader.Schema().Field(index).Nullable, true -} - -func (r *rows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { - typ := r.reader.Schema().Field(index).Type - switch dt := typ.(type) { - case *arrow.Decimal128Type: - return int64(dt.Precision), int64(dt.Scale), true - case *arrow.Decimal256Type: - return int64(dt.Precision), int64(dt.Scale), true - } - return 0, 0, false -} - -func (r *rows) ColumnTypeScanType(index int) reflect.Type { - switch r.reader.Schema().Field(index).Type.ID() { - case arrow.BOOL: - return reflect.TypeOf(false) - case arrow.INT8: - return reflect.TypeOf(int8(0)) - case arrow.UINT8: - return reflect.TypeOf(uint8(0)) - case arrow.INT16: - return reflect.TypeOf(int16(0)) - case arrow.UINT16: - return reflect.TypeOf(uint16(0)) - case arrow.INT32: - return reflect.TypeOf(int32(0)) - case arrow.UINT32: - return reflect.TypeOf(uint32(0)) - case arrow.INT64: - return reflect.TypeOf(int64(0)) - case arrow.UINT64: - return reflect.TypeOf(uint64(0)) - case arrow.FLOAT32: - return reflect.TypeOf(float32(0)) - case arrow.FLOAT64: - return reflect.TypeOf(float64(0)) - case arrow.DECIMAL128: - return reflect.TypeOf(decimal128.Num{}) - case arrow.DECIMAL256: - return reflect.TypeOf(decimal256.Num{}) - case arrow.BINARY: - return reflect.TypeOf([]byte{}) - case arrow.STRING: - return reflect.TypeOf(string("")) - case arrow.TIME32, arrow.TIME64, arrow.DATE32, arrow.DATE64, arrow.TIMESTAMP: - return reflect.TypeOf(time.Time{}) - } - return nil -} diff --git a/chdb/driver/parquet.go b/chdb/driver/parquet.go new file mode 100644 index 0000000..42b2665 --- /dev/null +++ b/chdb/driver/parquet.go @@ -0,0 +1,184 @@ +package chdbdriver + +import ( + "database/sql/driver" + "fmt" + "io" + "time" + "unsafe" + + "reflect" + + "github.com/chdb-io/chdb-go/chdbstable" + "github.com/parquet-go/parquet-go" +) + +func bytesToString(data []byte) string { + return *(*string)(unsafe.Pointer(&data)) +} + +func getStringFromBytes(v parquet.Value) string { + return bytesToString(v.ByteArray()) +} + +type parquetRows struct { + localResult *chdbstable.LocalResult // result from clickhouse + reader *parquet.GenericReader[any] // parquet reader + curRecord parquet.Row // TODO: delete this? + buffer []parquet.Row // record buffer + bufferSize int // amount of records to preload into buffer + bufferIndex int64 // index in the current buffer + curRow int64 // row counter + needNewBuffer bool +} + +func (r *parquetRows) Columns() (out []string) { + sch := r.reader.Schema() + for _, f := range sch.Fields() { + out = append(out, f.Name()) + } + + return +} + +func (r *parquetRows) Close() error { + if r.curRecord != nil { + r.curRecord = nil + } + // ignore reader close + _ = r.reader.Close() + r.reader = nil + r.localResult = nil + r.buffer = nil + return nil +} + +func (r *parquetRows) readNextChunk() error { + r.buffer = make([]parquet.Row, r.bufferSize) + readAmount, err := r.reader.ReadRows(r.buffer) + if err == io.EOF && readAmount == 0 { + return err // no records read, should exit the loop + } + if err == io.EOF && readAmount > 0 { + return nil //here we are at EOF, but since we read at least 1 record, we should consume it + } + if readAmount == 0 { + return io.EOF //same thing + } + if readAmount < r.bufferSize { + r.buffer = r.buffer[:readAmount] //eliminate empty items so the loop will exit before + } + r.bufferIndex = 0 + r.needNewBuffer = false + return nil +} + +func (r *parquetRows) Next(dest []driver.Value) error { + if r.curRow == 0 && r.localResult.RowsRead() == 0 { + return io.EOF //here we can simply return early since we don't need to issue a read to the file + } + if r.needNewBuffer { + err := r.readNextChunk() + if err != nil { + return err + } + + } + r.curRecord = r.buffer[r.bufferIndex] + if r.curRecord == nil || len(r.curRecord) == 0 { + return fmt.Errorf("empty row") + } + var scanError error + r.curRecord.Range(func(columnIndex int, columnValues []parquet.Value) bool { + if len(columnValues) != 1 { + return false + } + curVal := columnValues[0] + if curVal.IsNull() { + dest[columnIndex] = nil + return true + } + switch r.ColumnTypeDatabaseTypeName(columnIndex) { + case "STRING": + dest[columnIndex] = getStringFromBytes(curVal) + case "INT8", "INT(8,true)": + dest[columnIndex] = int8(curVal.Int32()) //check if this is correct + case "INT16", "INT(16,true)": + dest[columnIndex] = int16(curVal.Int32()) + case "INT64", "INT(64,true)": + dest[columnIndex] = curVal.Int64() + case "INT(64,false)": + dest[columnIndex] = curVal.Uint64() + case "INT(32,false)": + dest[columnIndex] = curVal.Uint32() + case "INT(8,false)": + dest[columnIndex] = uint8(curVal.Uint32()) //check if this is correct + case "INT(16,false)": + dest[columnIndex] = uint16(curVal.Uint32()) + case "INT32", "INT(32,true)": + dest[columnIndex] = curVal.Int32() + case "FLOAT32": + dest[columnIndex] = curVal.Float() + case "DOUBLE": + dest[columnIndex] = curVal.Double() + case "BOOLEAN": + dest[columnIndex] = curVal.Boolean() + case "BYTE_ARRAY", "FIXED_LEN_BYTE_ARRAY": + dest[columnIndex] = curVal.ByteArray() + case "TIMESTAMP(isAdjustedToUTC=true,unit=MILLIS)", "TIME(isAdjustedToUTC=true,unit=MILLIS)": + dest[columnIndex] = time.UnixMilli(curVal.Int64()).UTC() + case "TIMESTAMP(isAdjustedToUTC=true,unit=MICROS)", "TIME(isAdjustedToUTC=true,unit=MICROS)": + dest[columnIndex] = time.UnixMicro(curVal.Int64()).UTC() + case "TIMESTAMP(isAdjustedToUTC=true,unit=NANOS)", "TIME(isAdjustedToUTC=true,unit=NANOS)": + dest[columnIndex] = time.Unix(0, curVal.Int64()).UTC() + case "TIMESTAMP(isAdjustedToUTC=false,unit=MILLIS)", "TIME(isAdjustedToUTC=false,unit=MILLIS)": + dest[columnIndex] = time.UnixMilli(curVal.Int64()) + case "TIMESTAMP(isAdjustedToUTC=false,unit=MICROS)", "TIME(isAdjustedToUTC=false,unit=MICROS)": + dest[columnIndex] = time.UnixMicro(curVal.Int64()) + case "TIMESTAMP(isAdjustedToUTC=false,unit=NANOS)", "TIME(isAdjustedToUTC=false,unit=NANOS)": + dest[columnIndex] = time.Unix(0, curVal.Int64()) + default: + scanError = fmt.Errorf("could not cast to type: %s", r.ColumnTypeDatabaseTypeName(columnIndex)) + return false + + } + return true + }) + if scanError != nil { + return scanError + } + r.curRow++ + r.bufferIndex++ + r.needNewBuffer = r.bufferIndex == int64(len(r.buffer)) // if we achieved the buffer size, we need a new one + return nil +} + +func (r *parquetRows) ColumnTypeDatabaseTypeName(index int) string { + return r.reader.Schema().Fields()[index].Type().String() +} + +func (r *parquetRows) ColumnTypeNullable(index int) (nullable, ok bool) { + return r.reader.Schema().Fields()[index].Optional(), true +} + +func (r *parquetRows) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) { + return 0, 0, false +} + +func (r *parquetRows) ColumnTypeScanType(index int) reflect.Type { + switch r.reader.Schema().Fields()[index].Type().Kind() { + case parquet.Boolean: + return reflect.TypeOf(false) + case parquet.Int32: + return reflect.TypeOf(int32(0)) + case parquet.Int64: + return reflect.TypeOf(int64(0)) + case parquet.Float: + return reflect.TypeOf(float32(0)) + case parquet.Double: + return reflect.TypeOf(float64(0)) + case parquet.ByteArray, parquet.FixedLenByteArray: + return reflect.TypeOf("") + } + return nil +} diff --git a/chdb/driver/parquet_test.go b/chdb/driver/parquet_test.go new file mode 100644 index 0000000..44fdcc1 --- /dev/null +++ b/chdb/driver/parquet_test.go @@ -0,0 +1,106 @@ +package chdbdriver + +import ( + "database/sql" + "fmt" + "os" + "testing" + + "github.com/chdb-io/chdb-go/chdb" +) + +func TestDbWithParquet(t *testing.T) { + + db, err := sql.Open("chdb", fmt.Sprintf("driverType=%s", "PARQUET")) + if err != nil { + t.Errorf("open db fail, err:%s", err) + } + if db.Ping() != nil { + t.Errorf("ping db fail") + } + rows, err := db.Query(`SELECT 1,'abc'`) + if err != nil { + t.Errorf("run Query fail, err:%s", err) + } + cols, err := rows.Columns() + if err != nil { + t.Errorf("get result columns fail, err: %s", err) + } + if len(cols) != 2 { + t.Errorf("select result columns length should be 2") + } + var ( + bar int + foo string + ) + defer rows.Close() + for rows.Next() { + err := rows.Scan(&bar, &foo) + if err != nil { + t.Errorf("scan fail, err: %s", err) + } + if bar != 1 { + t.Errorf("expected error") + } + if foo != "abc" { + t.Errorf("expected error") + } + } +} + +func TestDBWithParquetSession(t *testing.T) { + sessionDir, err := os.MkdirTemp("", "unittest-sessiondata") + if err != nil { + t.Fatalf("create temp directory fail, err: %s", err) + } + defer os.RemoveAll(sessionDir) + session, err := chdb.NewSession(sessionDir) + if err != nil { + t.Fatalf("new session fail, err: %s", err) + } + defer session.Cleanup() + + session.Query("CREATE DATABASE IF NOT EXISTS testdb; " + + "CREATE TABLE IF NOT EXISTS testdb.testtable (id UInt32) ENGINE = MergeTree() ORDER BY id;") + + session.Query("INSERT INTO testdb.testtable VALUES (1), (2), (3);") + + ret, err := session.Query("SELECT * FROM testdb.testtable;") + if err != nil { + t.Fatalf("Query fail, err: %s", err) + } + if string(ret.Buf()) != "1\n2\n3\n" { + t.Errorf("Query result should be 1\n2\n3\n, got %s", string(ret.Buf())) + } + db, err := sql.Open("chdb", fmt.Sprintf("session=%s;driverType=%s", sessionDir, "PARQUET")) + if err != nil { + t.Fatalf("open db fail, err: %s", err) + } + if db.Ping() != nil { + t.Fatalf("ping db fail, err: %s", err) + } + rows, err := db.Query("select * from testdb.testtable;") + if err != nil { + t.Fatalf("exec create function fail, err: %s", err) + } + defer rows.Close() + cols, err := rows.Columns() + if err != nil { + t.Fatalf("get result columns fail, err: %s", err) + } + if len(cols) != 1 { + t.Fatalf("result columns length shoule be 3, actual: %d", len(cols)) + } + var bar = 0 + var count = 1 + for rows.Next() { + err = rows.Scan(&bar) + if err != nil { + t.Fatalf("scan fail, err: %s", err) + } + if bar != count { + t.Fatalf("result is not match, want: %d actual: %d", count, bar) + } + count++ + } +} diff --git a/chdb/wrapper_test.go b/chdb/wrapper_test.go index 7f30ebb..10ae5bc 100644 --- a/chdb/wrapper_test.go +++ b/chdb/wrapper_test.go @@ -65,7 +65,7 @@ func TestQueryToBuffer(t *testing.T) { outputFormat: "CSV", path: tempDir, udfPath: "", - expectedErrMsg: "Code: 60. DB::Exception: Table _local.nonexist does not exist. (UNKNOWN_TABLE)", + expectedErrMsg: "Code: 60. DB::Exception: Unknown table expression identifier 'nonexist' in scope SELECT * FROM nonexist. (UNKNOWN_TABLE)", expectedResult: "", }, } diff --git a/go.mod b/go.mod index 332f502..783aeb2 100644 --- a/go.mod +++ b/go.mod @@ -3,24 +3,32 @@ module github.com/chdb-io/chdb-go go 1.21 require ( - github.com/apache/arrow/go/v14 v14.0.2 + github.com/apache/arrow/go/v15 v15.0.2 github.com/c-bata/go-prompt v0.2.6 + github.com/parquet-go/parquet-go v0.23.0 ) require ( - github.com/goccy/go-json v0.10.2 // indirect - github.com/google/flatbuffers v23.5.26+incompatible // indirect - github.com/klauspost/compress v1.16.7 // indirect - github.com/klauspost/cpuid/v2 v2.2.5 // indirect + github.com/andybalholm/brotli v1.1.0 // indirect + github.com/goccy/go-json v0.10.3 // indirect + github.com/google/flatbuffers v24.3.25+incompatible // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/cpuid/v2 v2.2.8 // indirect github.com/mattn/go-colorable v0.1.13 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/mattn/go-runewidth v0.0.9 // indirect - github.com/mattn/go-tty v0.0.3 // indirect - github.com/pierrec/lz4/v4 v4.1.18 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mattn/go-runewidth v0.0.15 // indirect + github.com/mattn/go-tty v0.0.5 // indirect + github.com/olekukonko/tablewriter v0.0.5 // indirect + github.com/pierrec/lz4/v4 v4.1.21 // indirect github.com/pkg/term v1.2.0-beta.2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect + github.com/segmentio/encoding v0.4.0 // indirect github.com/zeebo/xxh3 v1.0.2 // indirect - golang.org/x/mod v0.13.0 // indirect - golang.org/x/sys v0.13.0 // indirect - golang.org/x/tools v0.14.0 // indirect - golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 // indirect + golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect + golang.org/x/mod v0.19.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/tools v0.23.0 // indirect + golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 // indirect ) diff --git a/go.sum b/go.sum index dce11f8..ca90107 100644 --- a/go.sum +++ b/go.sum @@ -1,19 +1,23 @@ -github.com/apache/arrow/go/v14 v14.0.2 h1:N8OkaJEOfI3mEZt07BIkvo4sC6XDbL+48MBPWO5IONw= -github.com/apache/arrow/go/v14 v14.0.2/go.mod h1:u3fgh3EdgN/YQ8cVQRguVW3R+seMybFg8QBQ5LU+eBY= +github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= +github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= +github.com/apache/arrow/go/v15 v15.0.2 h1:60IliRbiyTWCWjERBCkO1W4Qun9svcYoZrSLcyOsMLE= +github.com/apache/arrow/go/v15 v15.0.2/go.mod h1:DGXsR3ajT524njufqf95822i+KTh+yea1jass9YXgjA= github.com/c-bata/go-prompt v0.2.6 h1:POP+nrHE+DfLYx370bedwNhsqmpCUynWPxuHi0C5vZI= github.com/c-bata/go-prompt v0.2.6/go.mod h1:/LMAke8wD2FsNu9EXNdHxNLbd9MedkPnCdfpU9wwHfY= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/google/flatbuffers v23.5.26+incompatible h1:M9dgRyhJemaM4Sw8+66GHBu8ioaQmyPLg1b8VwK5WJg= -github.com/google/flatbuffers v23.5.26+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= -github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4= -github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= -github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/qbZg= -github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA= +github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/google/flatbuffers v24.3.25+incompatible h1:CX395cjN9Kke9mmalRoL3d81AtFUxJM+yDthflgJGkI= +github.com/google/flatbuffers v24.3.25+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/hexops/gotextdiff v1.0.3 h1:gitA9+qJrrTCsiCl7+kh75nPqQt1cx4ZkudSTLoUqJM= +github.com/hexops/gotextdiff v1.0.3/go.mod h1:pSWU5MAI3yDq+fZBTazCSJysOMbxWL1BSow5/V2vxeg= +github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= +github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= +github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM= +github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.7/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= @@ -22,32 +26,44 @@ github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hd github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.6/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-runewidth v0.0.9 h1:Lm995f3rfxdpd6TSmuVCHVb/QhupuXlYr8sCI/QdE+0= +github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/mattn/go-tty v0.0.3 h1:5OfyWorkyO7xP52Mq7tB36ajHDG5OHrmBGIS/DtakQI= +github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U= +github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mattn/go-tty v0.0.3/go.mod h1:ihxohKRERHTVzN+aSVRwACLCeqIoZAWpoICkkvrWyR0= -github.com/pierrec/lz4/v4 v4.1.18 h1:xaKrnTkyoqfh1YItXl56+6KJNVYWlEEPuAQW9xsplYQ= -github.com/pierrec/lz4/v4 v4.1.18/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/mattn/go-tty v0.0.5 h1:s09uXI7yDbXzzTTfw3zonKFzwGkyYlgU3OMjqA0ddz4= +github.com/mattn/go-tty v0.0.5/go.mod h1:u5GGXBtZU6RQoKV8gY5W6UhMudbR5vXnUe7j3pxse28= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/parquet-go/parquet-go v0.23.0 h1:dyEU5oiHCtbASyItMCD2tXtT2nPmoPbKpqf0+nnGrmk= +github.com/parquet-go/parquet-go v0.23.0/go.mod h1:MnwbUcFHU6uBYMymKAlPPAw9yh3kE1wWl6Gl1uLdkNk= +github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ= +github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/term v1.2.0-beta.2 h1:L3y/h2jkuBVFdWiJvNfYfKmzcCnILw7mJWm2JQuMppw= github.com/pkg/term v1.2.0-beta.2/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= +github.com/segmentio/encoding v0.4.0 h1:MEBYvRqiUB2nfR2criEXWqwdY6HJOUrCn5hboVOVmy8= +github.com/segmentio/encoding v0.4.0/go.mod h1:/d03Cd8PoaDeceuhUUUQWjU0KhWjrmYrWPgtJHYZSnI= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ= github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= github.com/zeebo/xxh3 v1.0.2 h1:xZmwmqxHZA8AI603jOQ0tMqmBr9lPeFwGg6d+xy9DC0= github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8= +golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.4.0 h1:zxkM55ReGkDlKSM+Fu41A+zmbZuaPVbGMzvvdUPznYQ= -golang.org/x/sync v0.4.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -58,13 +74,15 @@ golang.org/x/sys v0.0.0-20200918174421-af09f7315aff/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg= +golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9 h1:LLhsEBxRTBLuKlQxFBYUOU8xyFgXv6cOTp2HASDlsDk= +golang.org/x/xerrors v0.0.0-20240716161551-93cc26a95ae9/go.mod h1:NDW/Ps6MPRej6fsCIbMTohpP40sJ/P/vI1MoTEGwX90= gonum.org/v1/gonum v0.12.0 h1:xKuo6hzt+gMav00meVPUlXwSdoEJP46BR+wdxQEFK2o= gonum.org/v1/gonum v0.12.0/go.mod h1:73TDxJfAAHeA8Mk9mf8NlIppyhQNo5GLTcYeqgo2lvY= +google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg= +google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=