No way to add a row with different typed columns through gosnowflake driver
madisonchamberlain opened this issue · 2 comments
Hello, I am trying to add a row to a table with different column types. It doesn't seem to work. I believe the issue is due to the fact that if you want to specify a type, all of the types like DataTypeText
for example are just bytes under the hood, and if no type is specified the driver will assume that all variables are of the same type. Here is a test which can be used to reproduce:
const (
// define these out here so they can be used through the test
ID_COL_NAME = "ID"
SEQ_NUM_COL_NAME = "SEQ_NUM"
TEXT_COL_NAME = "TEXT_COL"
)
// specify your test db, schema and some test table name
func TestSnowflakeSupportsAddingOneRowWithDifferentTypes(t *testing.T, databaseName string, schemaName string, tableName string) {
ctx := context.Background()
tablePath := strings.Join([]string{databaseName, schemaName, tableName}, ".")
// Do what you need to do to get a sql connection here... this is what we need to do
config := readTestConfig(t, "snowflake")
poolCache := wh.NewPoolCache()
c, err := config.NewConnector(ctx, poolCache)
require.NoError(t, err)
sfConnector, isSfConnector := c.(*connector)
require.True(t, isSfConnector, "connector is not a snowflake connector ")
conn, err := sfConnector.pool.AcquireConn(ctx)
require.NoError(t, err)
// create a table with the initial two columns having binary types
createTableQuery := fmt.Sprintf(
"CREATE TABLE IF NOT EXISTS %s ("+
"%s BINARY(16) NOT NULL PRIMARY KEY, "+ // 16-byte UUID
"%s BINARY NOT NULL"+ // variable-length binary string
")",
tablePath,
ID_COL_NAME,
SEQ_NUM_COL_NAME,
)
// execute the query
_, err = conn.Exec(ctx, query.NewQuery(createTableQuery), nil)
require.NoError(t, err, "error creating initial table")
// Add another column which is of type text
columnType := "string"
addColQuery := fmt.Sprintf("ALTER TABLE %s ADD COLUMN %s %s", tablePath, TEXT_COL_NAME, columnType)
// execute the query
_, err = conn.Exec(ctx, query.NewQuery(addColQuery), nil)
require.NoError(t, err, "error adding string column")
sql, args := generateQueryWithNoTypes(tablePath)
_, err = conn.Exec(ctx, sql, args...)
if err != nil {
sql, args := generateQueryWithWithTypes(tablePath)
_, newErr := conn.Exec(ctx, sql, args...)
require.NoError(t, newErr, "both options failed :(, other error is %s", err)
}
}
func mkUuidBytes() []byte {
u := uuid.New()
return u[:]
}
func generateQueryWithNoTypes(tablePath string) (string, []interface{}) {
colNames := []string{
ID_COL_NAME,
SEQ_NUM_COL_NAME,
TEXT_COL_NAME,
}
// This will be passed directly to the query, so it will contain both type args and values
colArgs := []interface{}{
mkUuidBytes(), // data to add to ID row
[]byte{byte(10)}, // data to add to seqNumRow
"0 2", // data to add to text col
}
colQuestionMarks := make([]string, len(colNames))
for i := range colNames {
colQuestionMarks[i] = "?"
}
sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
tablePath,
strings.Join(colNames, ","),
strings.Join(colQuestionMarks, ","),
)
return sql, colArgs
}
func generateQueryWithWithTypes(tablePath string) (string, []interface{}) {
colNames := []string{
ID_COL_NAME,
SEQ_NUM_COL_NAME,
TEXT_COL_NAME,
}
// This will be passed directly to the query, so it will contain both type args and values
colArgs := []interface{}{
gosnowflake.DataTypeBinary,
mkUuidBytes(), // data to add to ID row
gosnowflake.DataTypeBinary,
[]byte{byte(10)}, // data to add to seqNumRow
gosnowflake.DataTypeText,
"0 2", // data to add to text col
}
colQuestionMarks := make([]string, len(colNames))
for i := range colNames {
colQuestionMarks[i] = "?"
}
sql := fmt.Sprintf(
"INSERT INTO %s (%s) VALUES (%s)",
tablePath,
strings.Join(colNames, ","),
strings.Join(colQuestionMarks, ","),
)
return sql, colArgs
}
I think that what would work as a solution is if you wrapped all of the publicly exposed dataTypes in datatype.go with something like this
// SnowflakeDataType is the type used by clients to explicitly indicate the type
// of an argument to Exec/QueryContext. We use a separate public-facing
// type rather than a Go primitive type so that we can always differentiate
// between args that indicate type and args that are values. Otherwise snowflake
// will default to thinking the data is actually bytes
type SnowflakeDataType []byte
var (
// DataTypeFixed is a FIXED datatype.
DataTypeFixed = SnowflakeDataType{fixedType.Byte()}
// DataTypeReal is a REAL datatype.
DataTypeReal = SnowflakeDataType{realType.Byte()}
// DataTypeText is a TEXT datatype.
DataTypeText = SnowflakeDataType{textType.Byte()}
// DataTypeDate is a Date datatype.
DataTypeDate = SnowflakeDataType{dateType.Byte()}
// DataTypeVariant is a TEXT datatype.
DataTypeVariant = SnowflakeDataType{variantType.Byte()}
// DataTypeTimestampLtz is a TIMESTAMP_LTZ datatype.
DataTypeTimestampLtz = SnowflakeDataType{timestampLtzType.Byte()}
// DataTypeTimestampNtz is a TIMESTAMP_NTZ datatype.
DataTypeTimestampNtz = SnowflakeDataType{timestampNtzType.Byte()}
// DataTypeTimestampTz is a TIMESTAMP_TZ datatype.
DataTypeTimestampTz = SnowflakeDataType{timestampTzType.Byte()}
// DataTypeObject is a OBJECT datatype.
DataTypeObject = SnowflakeDataType{objectType.Byte()}
// DataTypeArray is a ARRAY datatype.
DataTypeArray = SnowflakeDataType{arrayType.Byte()}
// DataTypeBinary is a BINARY datatype.
DataTypeBinary = SnowflakeDataType{binaryType.Byte()}
// DataTypeTime is a TIME datatype.
DataTypeTime = SnowflakeDataType{timeType.Byte()}
// DataTypeBoolean is a BOOLEAN datatype.
DataTypeBoolean = SnowflakeDataType{booleanType.Byte()}
// DataTypeNull is a NULL datatype.
DataTypeNull = SnowflakeDataType{nullType.Byte()}
)
Then if there were a function like this
func clientTypeToInternal(cType SnowflakeDataType) (iType snowflakeType, err error) {
if cType != nil {
switch {
case cType.Equals(DataTypeFixed):
iType = fixedType
case cType.Equals(DataTypeReal):
iType = realType
case cType.Equals(DataTypeText):
iType = textType
case cType.Equals(DataTypeDate):
iType = dateType
case cType.Equals(DataTypeVariant):
iType = variantType
case cType.Equals(DataTypeTimestampLtz):
iType = timestampLtzType
case cType.Equals(DataTypeTimestampNtz):
iType = timestampNtzType
case cType.Equals(DataTypeTimestampTz):
iType = timestampTzType
case cType.Equals(DataTypeObject):
iType = objectType
case cType.Equals(DataTypeArray):
iType = arrayType
case cType.Equals(DataTypeBinary):
iType = binaryType
case cType.Equals(DataTypeTime):
iType = timeType
case cType.Equals(DataTypeBoolean):
iType = booleanType
case cType.Equals(DataTypeNull):
iType = nullType
default:
return nullType, fmt.Errorf(errMsgInvalidByteArray, ([]byte)(cType))
}
} else {
return nullType, fmt.Errorf(errMsgInvalidByteArray, nil)
}
return iType, nil
}
which could be called in goTypeToSnowflake
I think it would do the trick. Maybe there is a work around, but I cant find any way to do this
(this is in progress on a different forum, will update with the outcome)
Closing this one, the issue turns out to be an issue dealing with [][]byte. I will open a new issue