|
1 | 1 | package transformers |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "bytes" |
| 5 | + "encoding/json" |
4 | 6 | "fmt" |
5 | 7 | "reflect" |
6 | 8 | "slices" |
| 9 | + "strings" |
7 | 10 |
|
| 11 | + "github.com/apache/arrow/go/v17/arrow" |
8 | 12 | "github.com/cloudquery/plugin-sdk/v4/schema" |
| 13 | + "github.com/cloudquery/plugin-sdk/v4/types" |
9 | 14 | "github.com/thoas/go-funk" |
10 | 15 | ) |
11 | 16 |
|
| 17 | +const maxJSONTypeSchemaDepth = 5 |
| 18 | + |
12 | 19 | type structTransformer struct { |
13 | 20 | table *schema.Table |
14 | 21 | skipFields []string |
@@ -111,17 +118,11 @@ func (t *structTransformer) addColumnFromField(field reflect.StructField, parent |
111 | 118 | return nil |
112 | 119 | } |
113 | 120 |
|
114 | | - columnType, err := t.typeTransformer(field) |
| 121 | + columnType, err := t.getColumnType(field) |
115 | 122 | if err != nil { |
116 | | - return fmt.Errorf("failed to transform type for field %s: %w", field.Name, err) |
| 123 | + return err |
117 | 124 | } |
118 | 125 |
|
119 | | - if columnType == nil { |
120 | | - columnType, err = DefaultTypeTransformer(field) |
121 | | - if err != nil { |
122 | | - return fmt.Errorf("failed to transform type for field %s: %w", field.Name, err) |
123 | | - } |
124 | | - } |
125 | 126 | if columnType == nil { |
126 | 127 | return nil // ignored |
127 | 128 | } |
@@ -159,6 +160,11 @@ func (t *structTransformer) addColumnFromField(field reflect.StructField, parent |
159 | 160 | IgnoreInTests: t.ignoreInTestsTransformer(field), |
160 | 161 | } |
161 | 162 |
|
| 163 | + // Enrich JSON column with detailed schema |
| 164 | + if columnType == types.ExtensionTypes.JSON { |
| 165 | + column.TypeSchema = structSchemaToJSON(t.fieldToJSONSchema(field, 0)) |
| 166 | + } |
| 167 | + |
162 | 168 | for _, pk := range t.pkFields { |
163 | 169 | if pk == path { |
164 | 170 | // use path to allow the following |
@@ -233,3 +239,101 @@ func TransformWithStruct(st any, opts ...StructTransformerOption) schema.Transfo |
233 | 239 | return nil |
234 | 240 | } |
235 | 241 | } |
| 242 | + |
| 243 | +func (t *structTransformer) getColumnType(field reflect.StructField) (arrow.DataType, error) { |
| 244 | + columnType, err := t.typeTransformer(field) |
| 245 | + if err != nil { |
| 246 | + return nil, fmt.Errorf("failed to transform type for field %s: %w", field.Name, err) |
| 247 | + } |
| 248 | + |
| 249 | + if columnType == nil { |
| 250 | + columnType, err = DefaultTypeTransformer(field) |
| 251 | + if err != nil { |
| 252 | + return nil, fmt.Errorf("failed to transform type for field %s: %w", field.Name, err) |
| 253 | + } |
| 254 | + } |
| 255 | + return columnType, nil |
| 256 | +} |
| 257 | + |
| 258 | +func structSchemaToJSON(s any) string { |
| 259 | + b := new(bytes.Buffer) |
| 260 | + encoder := json.NewEncoder(b) |
| 261 | + encoder.SetEscapeHTML(false) |
| 262 | + _ = encoder.Encode(s) |
| 263 | + return strings.TrimSpace(b.String()) |
| 264 | +} |
| 265 | + |
| 266 | +func normalizePointer(field reflect.StructField) reflect.Value { |
| 267 | + if field.Type.Kind() == reflect.Ptr { |
| 268 | + return reflect.New(field.Type.Elem()) |
| 269 | + } |
| 270 | + return reflect.New(field.Type) |
| 271 | +} |
| 272 | + |
| 273 | +func (t *structTransformer) fieldToJSONSchema(field reflect.StructField, depth int) any { |
| 274 | + transformInput := normalizePointer(field) |
| 275 | + switch transformInput.Elem().Kind() { |
| 276 | + case reflect.Struct: |
| 277 | + fieldsMap := make(map[string]any) |
| 278 | + fieldType := transformInput.Elem().Type() |
| 279 | + for i := 0; i < fieldType.NumField(); i++ { |
| 280 | + name, err := t.nameTransformer(fieldType.Field(i)) |
| 281 | + if err != nil { |
| 282 | + continue |
| 283 | + } |
| 284 | + columnType, err := t.getColumnType(fieldType.Field(i)) |
| 285 | + if err != nil { |
| 286 | + continue |
| 287 | + } |
| 288 | + if columnType == nil { |
| 289 | + fieldsMap[name] = "any" |
| 290 | + continue |
| 291 | + } |
| 292 | + // Avoid infinite recursion |
| 293 | + if columnType == types.ExtensionTypes.JSON && depth < maxJSONTypeSchemaDepth { |
| 294 | + fieldsMap[name] = t.fieldToJSONSchema(fieldType.Field(i), depth+1) |
| 295 | + continue |
| 296 | + } |
| 297 | + asList, ok := columnType.(*arrow.ListType) |
| 298 | + if ok { |
| 299 | + fieldsMap[name] = []any{asList.Elem().String()} |
| 300 | + continue |
| 301 | + } |
| 302 | + fieldsMap[name] = columnType.String() |
| 303 | + } |
| 304 | + return fieldsMap |
| 305 | + case reflect.Map: |
| 306 | + keySchema, ok := t.fieldToJSONSchema(reflect.StructField{ |
| 307 | + Type: field.Type.Key(), |
| 308 | + }, depth+1).(string) |
| 309 | + if keySchema == "" || !ok { |
| 310 | + return "" |
| 311 | + } |
| 312 | + valueSchema := t.fieldToJSONSchema(reflect.StructField{ |
| 313 | + Type: field.Type.Elem(), |
| 314 | + }, depth+1) |
| 315 | + if valueSchema == "" { |
| 316 | + return "" |
| 317 | + } |
| 318 | + return map[string]any{ |
| 319 | + keySchema: valueSchema, |
| 320 | + } |
| 321 | + case reflect.Slice: |
| 322 | + valueSchema := t.fieldToJSONSchema(reflect.StructField{ |
| 323 | + Type: field.Type.Elem(), |
| 324 | + }, depth+1) |
| 325 | + if valueSchema == "" { |
| 326 | + return "" |
| 327 | + } |
| 328 | + return []any{valueSchema} |
| 329 | + } |
| 330 | + |
| 331 | + columnType, err := t.getColumnType(field) |
| 332 | + if err != nil { |
| 333 | + return "" |
| 334 | + } |
| 335 | + if columnType == nil { |
| 336 | + return "any" |
| 337 | + } |
| 338 | + return columnType.String() |
| 339 | +} |
0 commit comments