Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: FIR-44128 report column types in go sdk #131

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ All information for the connection should be specified using the DSN string. The
firebolt://[/database]?account_name=account_name&client_id=client_id&client_secret=client_secret[&engine=engine]
```

- **client_id** - credentials client id.
- **client_secret** - credentials client secret.
- **client_id** - client id of the [service account](https://docs.firebolt.io/sql_reference/information-schema/service-accounts.html).
- **client_secret** - client secret of the [service account](https://docs.firebolt.io/sql_reference/information-schema/service-accounts.html).
- **account_name** - the name of Firebolt account to log in to.
- **database** - (optional) the name of the database to connect to.
- **engine** - (optional) the name of the engine to run SQL on.
Expand Down
107 changes: 107 additions & 0 deletions connection_common_integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,15 @@ import (
"context"
"database/sql"
"fmt"
"math"
"os"
"reflect"
"strconv"
"testing"
"time"

"github.com/shopspring/decimal"

contextUtils "github.com/firebolt-db/firebolt-go-sdk/context"

"github.com/firebolt-db/firebolt-go-sdk/logging"
Expand Down Expand Up @@ -473,3 +476,107 @@ func TestStreamMultipleDataBlocks(t *testing.T) {
}

}

type columnType struct {
Name string
DatabaseTypeName string
ScanType reflect.Type
HasNullable bool
Nullable bool
HasLength bool
Length int64
HasPrecisionScale bool
Precision int64
Scale int64
}

func getExpectedColumnTypes(isStreaming bool) []columnType {
res := []columnType{
{"col_int", "int", reflect.TypeOf(int32(0)), true, false, false, 0, false, 0, 0},
{"col_long", "long", reflect.TypeOf(int64(0)), true, false, false, 0, false, 0, 0},
{"col_float", "float", reflect.TypeOf(float32(0)), true, false, false, 0, false, 0, 0},
{"col_double", "double", reflect.TypeOf(float64(0)), true, false, false, 0, false, 0, 0},
{"col_text", "text", reflect.TypeOf(""), true, false, true, math.MaxInt64, false, 0, 0},
{"col_date", "date", reflect.TypeOf(time.Time{}), true, false, false, 0, false, 0, 0},
{"col_timestamp", "timestamp", reflect.TypeOf(time.Time{}), true, false, false, 0, false, 0, 0},
{"col_timestamptz", "timestamptz", reflect.TypeOf(time.Time{}), true, false, false, 0, false, 0, 0},
{"col_boolean", "boolean", reflect.TypeOf(true), true, false, false, 0, false, 0, 0},
{"col_array", "array(int)", reflect.TypeOf([]int32{}), true, false, true, math.MaxInt64, false, 0, 0},
{"col_decimal", "Decimal(38, 30)", reflect.TypeOf(decimal.Decimal{}), true, false, false, 0, true, 38, 30},
{"col_bytea", "bytea", reflect.TypeOf([]byte{}), true, false, true, math.MaxInt64, false, 0, 0},
{"col_geography", "geography", reflect.TypeOf(""), true, false, false, 0, false, 0, 0},
{"col_nullable", "text", reflect.TypeOf(""), true, true, true, math.MaxInt64, false, 0, 0},
}
// Some types are returned by different alias from database when streaming
if isStreaming {
res[0].DatabaseTypeName = "integer"
res[1].DatabaseTypeName = "bigint"
res[2].DatabaseTypeName = "real"
res[3].DatabaseTypeName = "double precision"
res[9].DatabaseTypeName = "array(integer)"
res[10].DatabaseTypeName = "numeric(38, 30)"
}
return res
}

func TestResponseMetadata(t *testing.T) {
const selectAllTypesSQL = `
select 1 as col_int,
30000000000 as col_long,
1.23::FLOAT4 as col_float,
1.23456789012 as col_double,
'text' as col_text,
'2021-03-28'::date as col_date,
'2019-07-31 01:01:01'::timestamp as col_timestamp,
'1111-01-05 17:04:42.123456'::timestamptz as col_timestamptz,
true as col_boolean,
[1,2,3,4] as col_array,
'1231232.123459999990457054844258706536'::decimal(38, 30) as col_decimal,
'abc123'::bytea as col_bytea,
'point(1 2)'::geography as col_geography,
null as col_nullable;`

utils.RunInMemoryAndStream(t, func(t *testing.T, ctx context.Context) {
expectedColumnTypes := getExpectedColumnTypes(contextUtils.IsStreaming(ctx))

conn, err := sql.Open("firebolt", dsnMock)
if err != nil {
t.Errorf(OPEN_CONNECTION_ERROR_MSG)
t.FailNow()
}

rows, err := conn.QueryContext(ctx, selectAllTypesSQL)
if err != nil {
t.Errorf(STATEMENT_ERROR_MSG, err)
t.FailNow()
}

if !rows.Next() {
t.Errorf("Next() call returned false with error: %v", rows.Err())
t.FailNow()
}

types, err := rows.ColumnTypes()
if err != nil {
t.Errorf("ColumnTypes returned an error, but shouldn't")
t.FailNow()
}

for i, ct := range types {
utils.AssertEqual(ct.Name(), expectedColumnTypes[i].Name, t, fmt.Sprintf("column name is not equal for column %s", ct.Name()))
utils.AssertEqual(ct.DatabaseTypeName(), expectedColumnTypes[i].DatabaseTypeName, t, fmt.Sprintf("database type name is not equal for column %s", ct.Name()))
utils.AssertEqual(ct.ScanType(), expectedColumnTypes[i].ScanType, t, fmt.Sprintf("scan type is not equal for column %s", ct.Name()))
nullable, ok := ct.Nullable()
utils.AssertEqual(ok, expectedColumnTypes[i].HasNullable, t, fmt.Sprintf("nullable ok is not equal for column %s", ct.Name()))
utils.AssertEqual(nullable, expectedColumnTypes[i].Nullable, t, fmt.Sprintf("nullable is not equal for column %s", ct.Name()))
length, ok := ct.Length()
utils.AssertEqual(ok, expectedColumnTypes[i].HasLength, t, fmt.Sprintf("length ok is not equal for column %s", ct.Name()))
utils.AssertEqual(length, expectedColumnTypes[i].Length, t, fmt.Sprintf("length is not equal for column %s", ct.Name()))
precision, scale, ok := ct.DecimalSize()
utils.AssertEqual(ok, expectedColumnTypes[i].HasPrecisionScale, t, fmt.Sprintf("precision scale ok is not equal for column %s", ct.Name()))
utils.AssertEqual(precision, expectedColumnTypes[i].Precision, t, fmt.Sprintf("precision is not equal for column %s", ct.Name()))
utils.AssertEqual(scale, expectedColumnTypes[i].Scale, t, fmt.Sprintf("scale is not equal for column %s", ct.Name()))
}

})
}
70 changes: 70 additions & 0 deletions rows/column_reader.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package rows

import (
"reflect"

"github.com/firebolt-db/firebolt-go-sdk/types"
)

type columnRecord struct {
name string
fbType fireboltType
}

type ColumnReader struct {
columns []columnRecord
}

func (r *ColumnReader) setColumns(columns []types.Column) error {
r.columns = make([]columnRecord, len(columns))
for i, column := range columns {
fbType, err := parseType(column.Type)
if err != nil {
return err
}
r.columns[i] = columnRecord{
name: column.Name,
fbType: fbType,
}
}
return nil

}

// Columns returns a list of column names in the current row set
func (r *ColumnReader) Columns() []string {
numColumns := len(r.columns)
result := make([]string, 0, numColumns)

for _, column := range r.columns {
result = append(result, column.name)
}

return result
}

func (r *ColumnReader) ColumnTypeScanType(index int) reflect.Type {
return r.columns[index].fbType.goType
}

func (r *ColumnReader) ColumnTypeDatabaseTypeName(index int) string {
return r.columns[index].fbType.dbName
}

func (r *ColumnReader) ColumnTypeNullable(index int) (nullable, ok bool) {
return r.columns[index].fbType.isNullable, true
}

func (r *ColumnReader) ColumnTypeLength(index int) (length int64, ok bool) {
if r.columns[index].fbType.length > 0 {
return r.columns[index].fbType.length, true
}
return 0, false
}

func (r *ColumnReader) ColumnTypePrecisionScale(index int) (precision, scale int64, ok bool) {
if r.columns[index].fbType.precision > 0 && r.columns[index].fbType.scale > 0 {
return r.columns[index].fbType.precision, r.columns[index].fbType.scale, true
}
return 0, 0, false
Comment on lines +66 to +69
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think there's a valid case when scale is 0, but precision is > 0. Redundant obviously, as it would be better represented by a different type, but nevertheless could occur.

}
Loading
Loading