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

SNOW-1488701 Enable structured types, documentation for structured types and vector type #1016

Merged
merged 13 commits into from
Oct 14, 2024
Merged
Show file tree
Hide file tree
Changes from 12 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
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,14 @@ Snowflake data types and their .NET types is covered in: [Data Types and Data Fo
How execute a query, use query bindings, run queries synchronously and asynchronously:
[Running Queries and Reading Results](doc/QueryingData.md)

## Structured types

Using structured types: [Structured types](doc/StructuredTypes.md)

## Vector type

Using vector type: [Vector type](doc/VectorType.md)

## Stage Files

Using stage files within PUT/GET commands:
Expand Down
32 changes: 16 additions & 16 deletions Snowflake.Data.Tests/IntegrationTests/StructuredArraysIT.cs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ public void TestSelectArray()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<string>(0);
var array = reader.GetArray<string>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -54,7 +54,7 @@ public void TestSelectArrayOfObjects()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<Identity>(0);
var array = reader.GetArray<Identity>(0);

// assert
Assert.AreEqual(2, array.Length);
Expand All @@ -79,7 +79,7 @@ public void TestSelectArrayOfArrays()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<string[]>(0);
var array = reader.GetArray<string[]>(0);

// assert
Assert.AreEqual(2, array.Length);
Expand All @@ -104,7 +104,7 @@ public void TestSelectArrayOfMap()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<Dictionary<string, string>>(0);
var array = reader.GetArray<Dictionary<string, string>>(0);

// assert
Assert.AreEqual(1, array.Length);
Expand Down Expand Up @@ -134,7 +134,7 @@ public void TestSelectSemiStructuredTypesInArray(string valueSfString, string ex
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<string>(0);
var array = reader.GetArray<string>(0);

// assert
Assert.NotNull(array);
Expand All @@ -159,7 +159,7 @@ public void TestSelectArrayOfIntegers()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<int>(0);
var array = reader.GetArray<int>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -184,7 +184,7 @@ public void TestSelectArrayOfLong()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<long>(0);
var array = reader.GetArray<long>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -209,7 +209,7 @@ public void TestSelectArrayOfFloats()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<float>(0);
var array = reader.GetArray<float>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -234,7 +234,7 @@ public void TestSelectArrayOfDoubles()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<double>(0);
var array = reader.GetArray<double>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -259,7 +259,7 @@ public void TestSelectArrayOfDoublesWithExponentNotation()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<double>(0);
var array = reader.GetArray<double>(0);

// assert
Assert.AreEqual(2, array.Length);
Expand All @@ -284,7 +284,7 @@ public void TestSelectStringArrayWithNulls()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<string>(0);
var array = reader.GetArray<string>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -309,7 +309,7 @@ public void TestSelectIntArrayWithNulls()
Assert.IsTrue(reader.Read());

// act
var array = reader.GetStucturedArray<int?>(0);
var array = reader.GetArray<int?>(0);

// assert
Assert.AreEqual(3, array.Length);
Expand All @@ -334,7 +334,7 @@ public void TestSelectNullArray()
Assert.IsTrue(reader.Read());

// act
var nullArray = reader.GetStucturedArray<string>(0);
var nullArray = reader.GetArray<string>(0);

// assert
Assert.IsNull(nullArray);
Expand All @@ -358,7 +358,7 @@ public void TestThrowExceptionForInvalidArray()
Assert.IsTrue(reader.Read());

// act
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetStucturedArray<string>(0));
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetArray<string>(0));

// assert
SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR);
Expand All @@ -384,7 +384,7 @@ public void TestThrowExceptionForInvalidArrayElement()
Assert.IsTrue(reader.Read());

// act
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetStucturedArray<Guid>(0));
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetArray<Guid>(0));

// assert
SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_ERROR);
Expand All @@ -411,7 +411,7 @@ public void TestThrowExceptionForNextedInvalidElement()
Assert.IsTrue(reader.Read());

// act
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetStucturedArray<AnnotatedClassForConstructorConstruction>(0));
var thrown = Assert.Throws<SnowflakeDbException>(() => reader.GetArray<AnnotatedClassForConstructorConstruction>(0));

// assert
SnowflakeDbExceptionAssert.HasErrorCode(thrown, SFError.STRUCTURED_TYPE_READ_DETAILED_ERROR);
Expand Down
33 changes: 6 additions & 27 deletions Snowflake.Data/Client/SnowflakeDbDataReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ public override int GetValues(object[] values)
return count;
}

internal T GetObject<T>(int ordinal)
public T GetObject<T>(int ordinal)
where T : class, new()
{
try
Expand Down Expand Up @@ -282,9 +282,11 @@ public T[] GetArray<T>(int ordinal)
{
var rowType = resultSet.sfResultSetMetaData.rowTypes[ordinal];
var fields = rowType.fields;
if (fields == null || fields.Count == 0 || !JsonToStructuredTypeConverter.IsVectorType(rowType.type))
var isArrayOrVector = JsonToStructuredTypeConverter.IsArrayType(rowType.type) ||
JsonToStructuredTypeConverter.IsVectorType(rowType.type);
if (fields == null || fields.Count == 0 || !isArrayOrVector)
{
throw new StructuredTypesReadingException($"Method GetArray<{typeof(T)}> can be used only for vector types");
throw new StructuredTypesReadingException($"Method GetArray<{typeof(T)}> can be used only for structured array or vector types");
}

var stringValue = GetString(ordinal);
Expand All @@ -299,30 +301,7 @@ public T[] GetArray<T>(int ordinal)
}
}

internal T[] GetStucturedArray<T>(int ordinal)
{
try
{
var rowType = resultSet.sfResultSetMetaData.rowTypes[ordinal];
var fields = rowType.fields;
if (fields == null || fields.Count == 0 || !JsonToStructuredTypeConverter.IsArrayType(rowType.type))
{
throw new StructuredTypesReadingException($"Method GetArray<{typeof(T)}> can be used only for structured array");
}

var stringValue = GetString(ordinal);
var json = stringValue == null ? null : JArray.Parse(stringValue);
return JsonToStructuredTypeConverter.ConvertArray<T>(fields, json);
}
catch (Exception e)
{
if (e is SnowflakeDbException)
throw;
throw StructuredTypesReadingHandler.ToSnowflakeDbException(e, "when getting an array");
}
}

internal Dictionary<TKey, TValue> GetMap<TKey, TValue>(int ordinal)
public Dictionary<TKey, TValue> GetMap<TKey, TValue>(int ordinal)
{
try
{
Expand Down
185 changes: 185 additions & 0 deletions doc/StructuredTypes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
## Concept

Snowflake structured types documentation is available here: [Snowflake Structured Types Documentation](https://docs.snowflake.com/en/sql-reference/data-types-structured).

Snowflake offers a way to store structured types which can be:
sfc-gh-knozderko marked this conversation as resolved.
Show resolved Hide resolved
- objects, e.g. ```OBJECT(city VARCHAR, state VARCHAR)```
- arrays, e.g. ```ARRAY(NUMBER)```
- maps, e.g. ```MAP(VARCHAR, VARCHAR)```

The driver allows reading and casting such structured objects into customer classes.

**Note**: Currently, reading structured types is available only for JSON result format.

## Enabling the feature

Currently, reading structured types is available only for JSON result format, so you can make sure you are using JSON result format by:
```sql
ALTER SESSION SET DOTNET_QUERY_RESULT_FORMAT = JSON;
```

The structured types feature is enabled starting from v4.2.0 driver version.

## Structured types vs semi-structured types

The difference between structured types and semi-structured types is that structured types contain types definitions for given objects/arrays/maps.

E.g. for a given object:
```sql
SELECT OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA')::OBJECT(city VARCHAR, state VARCHAR)
```

The part indicating the type of object is `::OBJECT(city VARCHAR, state VARCHAR)`.
This part of definition is essential for structured types because it is used to convert the object into the customer class instance.

Whereas the corresponding semi-structured type does not contain a detailed type definition, for instance:
```sql
SELECT OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA')::OBJECT
```

which means the semi-structured types are returned only as a JSON string.

## Handling objects

You can construct structured objects by using an object constructor and providing type details:

```sql
SELECT OBJECT_CONSTRUCT('city','San Mateo', 'state', 'CA')::OBJECT(city VARCHAR, state VARCHAR)
```

You can read the object into your class by executing `T SnowflakeDbReader.GetObject<T>(int ordinal)` method:

```csharp
var reader = (SnowflakeDbDataReader) command.ExecuteReader();
Assert.IsTrue(reader.Read());
var address = reader.GetObject<Address>(0);
```

where `Address` is a customer class, e.g.
```csharp
public class Address
{
public string city { get; set; }
public string state { get; set; }
public Zip zip { get; set; }
}
```

There are a few possible ways of constructing an object of a customer class.
The customer object (e.g. `Address`) can be created either:
- by the properties order, which is a default method
- by properties names
- by the constructor.

### Creating objects by properties order

Creating objects by properties order is a default construction method.
Objects are created by the non-parametrized constructor, and then the n-th Snowflake object field is converted into the n-th customer object property, one by one.

You can annotate your class with `SnowflakeObject` annotation to make sure this creation method would be chosen (however it is not necessary since it is a default method):
```csharp
[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_ORDER)]
public class Address
{
public string city { get; set; }
public string state { get; set; }
public Zip zip { get; set; }
}
```

If you would like to skip any customer property, you could use a `[SnowflakeColumn(IgnoreForPropertyOrder = true)]` annotation for a given property.
For instance, the annotation used in the following class definition makes the `city` be skipped when mapping the properties:
```csharp
public class Address
{
[SnowflakeColumn(IgnoreForPropertyOrder = true)]
public string city { get; set; }
public string state { get; set; }
public Zip zip { get; set; }
}
```

So, the first field from the database object would be mapped to the `state` property because `city` is skipped.

### Creating objects by property names

Using the `[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_NAMES)]` annotation on the customer class can enable the creation of objects by their property names.
In this creation method, objects are created by the non-parametrised constructor, and then for each of the database object fields a property of the same name is set with the field value.
It is crucial that database object field names are the same as customer property names; otherwise, a given database object field value would not be set in the customer object.
You can use the annotation `SnowflakeColumn` to rename the customer object property to the match database object field name.

In the example:

```csharp
[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.PROPERTIES_NAMES)]
public class Address
{
[SnowflakeColumn(Name = "nearestCity")]
public string city { get; set; }
public string state { get; set; }
public Zip zip { get; set; }
}
```

the database object field `nearestCity` would be mapped to the `city` property of `Address` class.

### Creating objects by the constructor

Using the `[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.CONSTRUCTOR)]` annotation on the customer class enables the creation of objects by a constructor.
In this creation method, an object with all its fields is created by a constructor.
A constructor with the exact number of parameters as the number of database object fields should exist because such a constructor would be chosen to instantiate a customer object.
sfc-gh-pmotacki marked this conversation as resolved.
Show resolved Hide resolved
Database object fields are mapped to customer object constructor parameters based on their order.

Example:
```csharp
[SnowflakeObject(ConstructionMethod = SnowflakeObjectConstructionMethod.CONSTRUCTOR)]
public class Address
{
private string _city;
private string _state;

public Address()
{
}

public Address(string city, string state)
{
_city = city;
_state = state;
}
}
```

## Handling arrays

You can construct structured arrays like this:

```sql
SELECT ARRAY_CONSTRUCT('a', 'b', 'c')::ARRAY(TEXT)
```

You can read such a structured array using `T[] SnowflakeDbReader.GetArray<T>(int ordinal)` method to get an array of specified type.

```csharp
var reader = (SnowflakeDbDataReader) command.ExecuteReader();
Assert.IsTrue(reader.Read());
var array = reader.GetArray<string>(0);
```

## Handling maps

You can construct structured maps like this:

```sql
SELECT OBJECT_CONSTRUCT('5','San Mateo', '8', 'CA', '13', '01-234')::MAP(INTEGER, VARCHAR)
```

**Note**: The only possible map key types are: VARCHAR or NUMBER with scale 0.

You can read a structured map using `Dictionary<TKey, TValue> SnowflakeDbReader.GetMap<TKey, TValue>(int ordinal)` method to get an array of specified type.

```csharp
var reader = (SnowflakeDbDataReader) command.ExecuteReader();
Assert.IsTrue(reader.Read());
var map = reader.GetMap<int, string>(0);
```
Loading
Loading