Repository: jaegertracing/jaeger

Issue:
Relocate docker/ folder from root of the project
### Requirement

We would like to have less clutter in the root of the repository

### Problem

The `docker/` directory contains just two docker files that are used during CI to build images

### Proposal

* Move `docker` dir under `scripts/build`
* Update all references in the repository to match the new location

```sh
$ rg 'docker/(base|debug)'
Makefile
34:				   -not -path './docker/debug/*' \

scripts/makefiles/Docker.mk
16:		docker/base
23:		docker/debug
30:		docker/base

scripts/lint/check-go-version.sh
97:check docker/debug/Dockerfile "^.*golang:$version_regex" "$go_latest_version"
```


### Open questions

_No response_

Discussion:

Solution patch:
diff --git a/Makefile b/Makefile
index d5a9ddfb..2fb8da1a 100644
--- a/Makefile
+++ b/Makefile
@@ -31,7 +31,7 @@ ALL_SRC = $(shell find . -name '*.go' \
 				   -not -path './vendor/*' \
 				   -not -path './idl/*' \
 				   -not -path './internal/tools/*' \
-				   -not -path './docker/debug/*' \
+				   -not -path './scripts/build/docker/debug/*' \
 				   -not -path '*/mocks/*' \
 				   -not -path '*/thrift-0.9.2/*' \
 				   -type f | \
diff --git a/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel.go b/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel.go
index 28586f8a..fd23a376 100644
--- a/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel.go
+++ b/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel.go
@@ -3,53 +3,102 @@
 
 package dbmodel
 
-import "time"
+import (
+	"time"
+)
 
-// Trace Domain model in Clickhouse.
-// This struct represents the schema for storing OTel pipeline Traces in Clickhouse.
-type Trace struct {
-	Resource Resource
-	Scope    Scope
-	Span     Span
-	Links    []Link
-	Events   []Event
-}
+// Span represents a single row in the ClickHouse `spans` table.
+type Span struct {
+	// --- Span ---
+	// TODO: add attributes
+	ID            string    `ch:"id"`
+	TraceID       string    `ch:"trace_id"`
+	TraceState    string    `ch:"trace_state"`
+	ParentSpanID  string    `ch:"parent_span_id"`
+	Name          string    `ch:"name"`
+	Kind          string    `ch:"kind"`
+	StartTime     time.Time `ch:"start_time"`
+	StatusCode    string    `ch:"status_code"`
+	StatusMessage string    `ch:"status_message"`
 
-type Resource struct {
-	Attributes AttributesGroup
-}
+	// Duration is stored in ClickHouse as a UInt64 representing the number of nanoseconds.
+	// In Go, it is manually converted to and from time.Duration for convenience.
+	Duration time.Duration
 
-type Scope struct {
-	Name       string
-	Version    string
-	Attributes AttributesGroup
-}
+	// --- Nested Types ---
+	// The fields below correspond to ClickHouse Nested columns, which act
+	// like a table inside a cell. The clickhouse-go driver does not support
+	// automatic decoding of Nested types via ScanStruct into these slice
+	// structs directly. Therefore, the raw data for these fields must be
+	// scanned into intermediate types (e.g., []map[string]any), and then
+	// manually decoded into the concrete Go structs defined here.
+	// For this reason, these fields do NOT have `ch` tags themselves.
+	// (Ref: https://github.com/ClickHouse/clickhouse-go/blob/main/examples/clickhouse_api/nested.go)
+	Events []Event
+	Links  []Link
 
-type Span struct {
-	Timestamp     time.Time
-	TraceId       string
-	SpanId        string
-	ParentSpanId  string
-	TraceState    string
-	Name          string
-	Kind          string
-	Duration      time.Time
-	StatusCode    string
-	StatusMessage string
-	Attributes    AttributesGroup
+	// --- Resource ---
+	// TODO: add attributes
+	ServiceName string `ch:"service_name"`
+
+	// --- Scope ---
+	// TODO: add attributes
+	ScopeName    string `ch:"scope_name"`
+	ScopeVersion string `ch:"scope_version"`
 }
 
 type Link struct {
-	TraceId    string
-	SpanId     string
+	// TODO: add attributes
+	TraceID    string
+	SpanID     string
 	TraceState string
-	Attributes AttributesGroup
+}
+
+func getLinksFromRaw(raw []map[string]any) []Link {
+	links := make([]Link, 0, len(raw))
+	for _, m := range raw {
+		links = append(links, getLinkFromRaw(m))
+	}
+	return links
+}
+
+func getLinkFromRaw(m map[string]any) Link {
+	var link Link
+	if traceID, ok := m["trace_id"].(string); ok {
+		link.TraceID = traceID
+	}
+	if spanID, ok := m["span_id"].(string); ok {
+		link.SpanID = spanID
+	}
+	if traceState, ok := m["trace_state"].(string); ok {
+		link.TraceState = traceState
+	}
+	return link
 }
 
 type Event struct {
-	Name       string
-	Timestamp  time.Time
-	Attributes AttributesGroup
+	// TODO: add attributes
+	Name      string
+	Timestamp time.Time
+}
+
+func getEventsFromRaw(raw []map[string]any) []Event {
+	events := make([]Event, 0, len(raw))
+	for _, m := range raw {
+		events = append(events, getEventFromRaw(m))
+	}
+	return events
+}
+
+func getEventFromRaw(m map[string]any) Event {
+	var event Event
+	if name, ok := m["name"].(string); ok {
+		event.Name = name
+	}
+	if ts, ok := m["timestamp"].(time.Time); ok {
+		event.Timestamp = ts
+	}
+	return event
 }
 
 type Service struct {
diff --git a/internal/storage/v2/clickhouse/tracestore/dbmodel/fixtures/dbmodel.json b/internal/storage/v2/clickhouse/tracestore/dbmodel/fixtures/dbmodel.json
index a47e892b..c8ee4e1c 100644
--- a/internal/storage/v2/clickhouse/tracestore/dbmodel/fixtures/dbmodel.json
+++ b/internal/storage/v2/clickhouse/tracestore/dbmodel/fixtures/dbmodel.json
@@ -1,213 +1,27 @@
 {
-    "Resource": {
-        "Attributes": {
-            "BoolKeys": [
-                "browser.mobile"
-            ],
-            "BoolValues": [
-                true
-            ],
-            "DoubleKeys": [
-                "host.memory.swap"
-            ],
-            "DoubleValues": [
-                2048
-            ],
-            "IntKeys": [
-                "process.parent.pid",
-                "process.pid",
-                "faas.max_memory"
-            ],
-            "IntValues": [
-                111,
-                1234,
-                134217728
-            ],
-            "StrKeys": [
-                "service.names",
-                "service.instance.id"
-            ],
-            "StrValues": [
-                "clickhouse",
-                "627cc493-f310-47de-96bd-71410b7dec09"
-            ],
-            "BytesKeys": [
-                "oci.manifest.digest"
-            ],
-            "BytesValues": [
-                "c2hhMjU2OmU0Y2E2MmMwZDYyZjNlODg2ZTY4NDgwNmRmZTlkNGUwY2RhNjBkNTQ5ODY4OTgxNzNjMTA4Mzg1NmNmZGEwZjQ="
-            ]
-        }
-    },
-    "Scope": {
-        "Name": "io.opentelemetry.contrib.clickhouse",
-        "Version": "1.0.0",
-        "Attributes": {
-            "BoolKeys": [
-                "library.feature.async_processing_enabled",
-                "library.security.data_masking_active"
-            ],
-            "BoolValues": [
-                true,
-                false
-            ],
-            "DoubleKeys": [
-                "component.config.sampling.ratio"
-            ],
-            "DoubleValues": [
-                0.75
-            ],
-            "IntKeys": [
-                "component.max_workers",
-                "component.min_workers"
-            ],
-            "IntValues": [
-                10,
-                2
-            ],
-            "StrKeys": [
-                "library.language",
-                "library.version"
-            ],
-            "StrValues": [
-                "go",
-                "v2.2.2"
-            ],
-            "BytesKeys": [
-                "scope.test.bytes.value"
-            ],
-            "BytesValues": [
-                "AQIDBA=="
-            ]
-        }
-    },
-    "Span": {
-        "Timestamp": "2023-12-25T09:53:49Z",
-        "TraceId": "01020300000000000000000000000000",
-        "SpanId": "0102030000000000",
-        "ParentSpanId": "0102040000000000",
-        "TraceState": "trace state",
-        "Name": "call db",
-        "Kind": "Internal",
-        "Duration": "2023-12-25T09:54:49Z",
-        "StatusCode": "Error",
-        "StatusMessage": "error",
-        "Attributes": {
-            "BoolKeys": [
-                "app.payment.card_valid",
-                "app.payment.charged"
-            ],
-            "BoolValues": [
-                true,
-                true
-            ],
-            "DoubleKeys": [
-                "app.payment.amount"
-            ],
-            "DoubleValues": [
-                99.99
-            ],
-            "IntKeys": [
-                "app.payment.count"
-            ],
-            "IntValues": [
-                5
-            ],
-            "StrKeys": [
-                "app.payment.id"
-            ],
-            "StrValues": [
-                "123456789"
-            ],
-            "BytesKeys": [
-                "span.test.bytes.value"
-            ],
-            "BytesValues": [
-                "AQIDBAUG"
-            ]
-        }
-    },
-    "Links": [
-        {
-            "TraceId": "01020500000000000000000000000000",
-            "SpanId": "0102050000000000",
-            "TraceState": "test",
-            "Attributes": {
-                "BoolKeys": [
-                    "is.retry"
-                ],
-                "BoolValues": [
-                    true
-                ],
-                "DoubleKeys": [
-                    "similarity.score"
-                ],
-                "DoubleValues": [
-                    0.85
-                ],
-                "IntKeys": [
-                    "correlation.id"
-                ],
-                "IntValues": [
-                    1324141
-                ],
-                "StrKeys": [
-                    "related.resource.id"
-                ],
-                "StrValues": [
-                    "resource-123"
-                ],
-                "BytesKeys": [
-                    "link.test.bytes.value"
-                ],
-                "BytesValues": [
-                    "AQIDBAUG"
-                ]
-            }
-        }
-    ],
-    "Events": [
-        {
-            "Name": "event1",
-            "Timestamp": "2023-12-25T09:53:49Z",
-            "Attributes": {
-                "BoolKeys": [
-                    "inventory.available",
-                    "payment.successful"
-                ],
-                "BoolValues": [
-                    true,
-                    true
-                ],
-                "DoubleKeys": [
-                    "product.price",
-                    "order.discount.rate"
-                ],
-                "DoubleValues": [
-                    6.04,
-                    0.04
-                ],
-                "IntKeys": [
-                    "order.quantity"
-                ],
-                "IntValues": [
-                    2
-                ],
-                "StrKeys": [
-                    "order.id",
-                    "product.id"
-                ],
-                "StrValues": [
-                    "123456789",
-                    "987654321"
-                ],
-                "BytesKeys": [
-                    "event.test.bytes.value"
-                ],
-                "BytesValues": [
-                    "AQIDBAUG"
-                ]
-            }
-        }
-    ]
-}
\ No newline at end of file
+  "ID": "0102030000000000",
+  "TraceID": "01020300000000000000000000000000",
+  "TraceState": "trace state",
+  "ParentSpanID": "0102040000000000",
+  "Name": "call db",
+  "Kind": "Internal",
+  "StartTime": "2023-12-25T09:53:49Z",
+  "Duration": 60000000000,
+  "StatusCode": "Error",
+  "StatusMessage": "error",
+  "ScopeName": "io.opentelemetry.contrib.clickhouse",
+  "ScopeVersion": "1.0.0",
+  "Links": [
+    {
+      "TraceID": "01020500000000000000000000000000",
+      "SpanID": "0102050000000000",
+      "TraceState": "test"
+    }
+  ],
+  "Events": [
+    {
+      "Name": "event1",
+      "Timestamp": "2023-12-25T09:53:49Z"
+    }
+  ]
+}
diff --git a/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel.go b/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel.go
index 209e14f8..3957421e 100644
--- a/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel.go
+++ b/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel.go
@@ -4,7 +4,6 @@
 package dbmodel
 
 import (
-	"encoding/base64"
 	"encoding/hex"
 	"fmt"
 
@@ -14,44 +13,44 @@ import (
 	"github.com/jaegertracing/jaeger/internal/jptrace"
 )
 
-func FromDBModel(dbTrace Trace) ptrace.Traces {
+func FromDBModel(storedSpan Span) ptrace.Traces {
 	trace := ptrace.NewTraces()
 	resourceSpans := trace.ResourceSpans().AppendEmpty()
 	scopeSpans := resourceSpans.ScopeSpans().AppendEmpty()
 	span := scopeSpans.Spans().AppendEmpty()
 
-	sp, err := FromDBSpan(dbTrace.Span)
+	sp, err := convertSpan(storedSpan)
 	sp.CopyTo(span)
 	if err != nil {
 		jptrace.AddWarnings(span, err.Error())
 	}
 
 	resource := resourceSpans.Resource()
-	rs, err := FromDBResource(dbTrace.Resource)
+	rs, err := convertResource(storedSpan)
 	if err != nil {
 		jptrace.AddWarnings(span, err.Error())
 	}
 	rs.CopyTo(resource)
 
 	scope := scopeSpans.Scope()
-	sc, err := FromDBScope(dbTrace.Scope)
+	sc, err := convertScope(storedSpan)
 	if err != nil {
 		jptrace.AddWarnings(span, err.Error())
 	}
 	sc.CopyTo(scope)
 
-	for i := range dbTrace.Events {
+	for i := range storedSpan.Events {
 		event := span.Events().AppendEmpty()
-		e, err := FromDBEvent(dbTrace.Events[i])
+		e, err := convertEvent(storedSpan.Events[i])
 		if err != nil {
 			jptrace.AddWarnings(span, err.Error())
 		}
 		e.CopyTo(event)
 	}
 
-	for i := range dbTrace.Links {
+	for i := range storedSpan.Links {
 		link := span.Links().AppendEmpty()
-		l, err := FromDBLink(dbTrace.Links[i])
+		l, err := convertSpanLink(storedSpan.Links[i])
 		if err != nil {
 			jptrace.AddWarnings(span, err.Error())
 		}
@@ -60,97 +59,78 @@ func FromDBModel(dbTrace Trace) ptrace.Traces {
 	return trace
 }
 
-func FromDBResource(r Resource) (pcommon.Resource, error) {
+func convertResource(Span) (pcommon.Resource, error) {
 	resource := ptrace.NewResourceSpans().Resource()
-	resourceAttributes, err := AttributesGroupToMap(r.Attributes)
-	if err != nil {
-		return resource, fmt.Errorf("failed to decode resource attribute: %w", err)
-	}
-	resourceAttributes.CopyTo(resource.Attributes())
+	// TODO: populate attributes
+	// TODO: do we populate the service name from the span?
 	return resource, nil
 }
 
-func FromDBScope(s Scope) (pcommon.InstrumentationScope, error) {
+func convertScope(s Span) (pcommon.InstrumentationScope, error) {
 	scope := ptrace.NewScopeSpans().Scope()
-	scope.SetName(s.Name)
-	scope.SetVersion(s.Version)
-	attributes, err := AttributesGroupToMap(s.Attributes)
-	if err != nil {
-		return scope, fmt.Errorf("failed to decode scope attribute: %w", err)
-	}
-	attributes.CopyTo(scope.Attributes())
+	scope.SetName(s.ScopeName)
+	scope.SetVersion(s.ScopeVersion)
+	// TODO: populate attributes
 
 	return scope, nil
 }
 
-func FromDBSpan(s Span) (ptrace.Span, error) {
+func convertSpan(s Span) (ptrace.Span, error) {
 	span := ptrace.NewSpan()
-	span.SetStartTimestamp(pcommon.NewTimestampFromTime(s.Timestamp))
-	traceId, err := hex.DecodeString(s.TraceId)
+	span.SetStartTimestamp(pcommon.NewTimestampFromTime(s.StartTime))
+	traceId, err := hex.DecodeString(s.TraceID)
 	if err != nil {
-		return span, fmt.Errorf("failed to decode trace Id: %w", err)
+		return span, fmt.Errorf("failed to decode trace ID: %w", err)
 	}
 	span.SetTraceID(pcommon.TraceID(traceId))
-	spanId, err := hex.DecodeString(s.SpanId)
+	spanId, err := hex.DecodeString(s.ID)
 	if err != nil {
-		return span, fmt.Errorf("failed to decode span Id: %w", err)
+		return span, fmt.Errorf("failed to decode span ID: %w", err)
 	}
 	span.SetSpanID(pcommon.SpanID(spanId))
-	parentSpanId, err := hex.DecodeString(s.ParentSpanId)
+	parentSpanId, err := hex.DecodeString(s.ParentSpanID)
 	if err != nil {
-		return span, fmt.Errorf("failed to decode parent span Id: %w", err)
+		return span, fmt.Errorf("failed to decode parent span ID: %w", err)
 	}
 	span.SetParentSpanID(pcommon.SpanID(parentSpanId))
 	span.TraceState().FromRaw(s.TraceState)
 	span.SetName(s.Name)
-	span.SetKind(FromDBSpanKind(s.Kind))
-	span.SetEndTimestamp(pcommon.NewTimestampFromTime(s.Duration))
-	span.Status().SetCode(FromDBStatusCode(s.StatusCode))
+	span.SetKind(convertSpanKind(s.Kind))
+	span.SetEndTimestamp(pcommon.NewTimestampFromTime(s.StartTime.Add(s.Duration)))
+	span.Status().SetCode(convertStatusCode(s.StatusCode))
 	span.Status().SetMessage(s.StatusMessage)
-	spanAttributes, err := AttributesGroupToMap(s.Attributes)
-	if err != nil {
-		return span, fmt.Errorf("failed to decode span attribute: %w", err)
-	}
-	spanAttributes.CopyTo(span.Attributes())
+	// TODO: populate attributes
 
 	return span, nil
 }
 
-func FromDBEvent(e Event) (ptrace.SpanEvent, error) {
+func convertEvent(e Event) (ptrace.SpanEvent, error) {
 	event := ptrace.NewSpanEvent()
 	event.SetName(e.Name)
 	event.SetTimestamp(pcommon.NewTimestampFromTime(e.Timestamp))
 
-	attributes, err := AttributesGroupToMap(e.Attributes)
-	if err != nil {
-		return event, fmt.Errorf("failed to decode event attribute: %w", err)
-	}
-	attributes.CopyTo(event.Attributes())
+	// TODO: populate attributes
 	return event, nil
 }
 
-func FromDBLink(l Link) (ptrace.SpanLink, error) {
+func convertSpanLink(l Link) (ptrace.SpanLink, error) {
 	link := ptrace.NewSpanLink()
-	traceId, err := hex.DecodeString(l.TraceId)
+	traceId, err := hex.DecodeString(l.TraceID)
 	if err != nil {
-		return link, fmt.Errorf("failed to decode link trace Id: %w", err)
+		return link, fmt.Errorf("failed to decode link trace ID: %w", err)
 	}
 	link.SetTraceID(pcommon.TraceID(traceId))
-	spanId, err := hex.DecodeString(l.SpanId)
+	spanId, err := hex.DecodeString(l.SpanID)
 	if err != nil {
-		return link, fmt.Errorf("failed to decode link span Id: %w", err)
+		return link, fmt.Errorf("failed to decode link span ID: %w", err)
 	}
 	link.SetSpanID(pcommon.SpanID(spanId))
 	link.TraceState().FromRaw(l.TraceState)
-	attributes, err := AttributesGroupToMap(l.Attributes)
-	if err != nil {
-		return link, fmt.Errorf("failed to decode link attribute: %w", err)
-	}
-	attributes.CopyTo(link.Attributes())
+	// TODO: populate attributes
 	return link, nil
 }
 
-func FromDBSpanKind(sk string) ptrace.SpanKind {
+func convertSpanKind(sk string) ptrace.SpanKind {
 	switch sk {
 	case "Unspecified":
 		return ptrace.SpanKindUnspecified
@@ -168,7 +148,7 @@ func FromDBSpanKind(sk string) ptrace.SpanKind {
 	return ptrace.SpanKindUnspecified
 }
 
-func FromDBStatusCode(sc string) ptrace.StatusCode {
+func convertStatusCode(sc string) ptrace.StatusCode {
 	switch sc {
 	case "OK":
 		return ptrace.StatusCodeOk
@@ -179,37 +159,3 @@ func FromDBStatusCode(sc string) ptrace.StatusCode {
 	}
 	return ptrace.StatusCodeUnset
 }
-
-func AttributesGroupToMap(group AttributesGroup) (pcommon.Map, error) {
-	result := pcommon.NewMap()
-	for i := range group.BoolKeys {
-		key := group.BoolKeys[i]
-		value := group.BoolValues[i]
-		result.PutBool(key, value)
-	}
-	for i := range group.IntKeys {
-		key := group.IntKeys[i]
-		value := group.IntValues[i]
-		result.PutInt(key, value)
-	}
-	for i := range group.DoubleKeys {
-		key := group.DoubleKeys[i]
-		value := group.DoubleValues[i]
-		result.PutDouble(key, value)
-	}
-	for i := range group.StrKeys {
-		key := group.StrKeys[i]
-		value := group.StrValues[i]
-		result.PutStr(key, value)
-	}
-	for i := range group.BytesKeys {
-		key := group.BytesKeys[i]
-		value := group.BytesValues[i]
-		bts, err := base64.StdEncoding.DecodeString(value)
-		if err != nil {
-			return result, err
-		}
-		result.PutEmptyBytes(key).FromRaw(bts)
-	}
-	return result, nil
-}
diff --git a/docker/base/Dockerfile b/scripts/build/docker/base/Dockerfile
similarity index 100%
rename from docker/base/Dockerfile
rename to scripts/build/docker/base/Dockerfile
diff --git a/docker/debug/Dockerfile b/scripts/build/docker/debug/Dockerfile
similarity index 100%
rename from docker/debug/Dockerfile
rename to scripts/build/docker/debug/Dockerfile
diff --git a/docker/debug/go.mod b/scripts/build/docker/debug/go.mod
similarity index 100%
rename from docker/debug/go.mod
rename to scripts/build/docker/debug/go.mod
diff --git a/docker/debug/go.sum b/scripts/build/docker/debug/go.sum
similarity index 100%
rename from docker/debug/go.sum
rename to scripts/build/docker/debug/go.sum
diff --git a/docker/debug/tools.go b/scripts/build/docker/debug/tools.go
similarity index 100%
rename from docker/debug/tools.go
rename to scripts/build/docker/debug/tools.go
diff --git a/scripts/lint/check-go-version.sh b/scripts/lint/check-go-version.sh
index bb4fd135..13a5d118 100755
--- a/scripts/lint/check-go-version.sh
+++ b/scripts/lint/check-go-version.sh
@@ -94,7 +94,7 @@ for file in $(find . -type f -name go.mod | grep -v '^./go.mod'); do
     check "$file" "^go\s\+$version_regex" "$go_latest_version"
 done
 
-check docker/debug/Dockerfile "^.*golang:$version_regex" "$go_latest_version"
+check scripts/build/docker/debug/Dockerfile "^.*golang:$version_regex" "$go_latest_version"
 
 IFS='|' read -r -a gha_workflows <<< "$(grep -rl go-version .github | tr '\n' '|')"
 for gha_workflow in "${gha_workflows[@]}"; do
diff --git a/scripts/makefiles/Docker.mk b/scripts/makefiles/Docker.mk
index 873c8a68..4e39f0e7 100644
--- a/scripts/makefiles/Docker.mk
+++ b/scripts/makefiles/Docker.mk
@@ -13,21 +13,21 @@ create-baseimg: prepare-docker-buildx
 	@echo "::group:: create-baseimg"
 	docker buildx build -t $(BASE_IMAGE) --push \
 		--platform=$(LINUX_PLATFORMS) \
-		docker/base
+		scripts/build/docker/base
 	@echo "::endgroup::"
 
 create-debugimg: prepare-docker-buildx
 	@echo "::group:: create-debugimg"
 	docker buildx build -t $(DEBUG_IMAGE) --push \
 		--platform=$(LINUX_PLATFORMS) \
-		docker/debug
+		scripts/build/docker/debug
 	@echo "::endgroup::"
 
 create-fake-debugimg: prepare-docker-buildx
 	@echo "::group:: create-fake-debugimg"
 	docker buildx build -t $(DEBUG_IMAGE) --push \
 		--platform=$(LINUX_PLATFORMS) \
-		docker/base
+		scripts/build/docker/base
 	@echo "::endgroup::"
 
 .PHONY: prepare-docker-buildx



Test patch:
diff --git a/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel_test.go b/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel_test.go
new file mode 100644
index 00000000..f9e85e5a
--- /dev/null
+++ b/internal/storage/v2/clickhouse/tracestore/dbmodel/dbmodel_test.go
@@ -0,0 +1,93 @@
+// Copyright (c) 2025 The Jaeger Authors.
+// SPDX-License-Identifier: Apache-2.0
+
+package dbmodel
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestGetEventsFromRaw(t *testing.T) {
+	tests := []struct {
+		name     string
+		raw      []map[string]any
+		expected []Event
+	}{
+		{
+			name: "valid raw data",
+			raw: []map[string]any{
+				{"name": "event1", "timestamp": time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC)},
+				{"name": "event2", "timestamp": time.Date(2023, 10, 2, 12, 0, 0, 0, time.UTC)},
+			},
+			expected: []Event{
+				{Name: "event1", Timestamp: time.Date(2023, 10, 1, 12, 0, 0, 0, time.UTC)},
+				{Name: "event2", Timestamp: time.Date(2023, 10, 2, 12, 0, 0, 0, time.UTC)},
+			},
+		},
+		{
+			name:     "empty raw data",
+			raw:      []map[string]any{},
+			expected: []Event{},
+		},
+		{
+			name: "invalid raw data",
+			raw: []map[string]any{
+				{"name": 123, "timestamp": "invalid"},
+			},
+			expected: []Event{
+				{},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := getEventsFromRaw(test.raw)
+			require.Equal(t, test.expected, result)
+		})
+	}
+}
+
+func TestGetLinksFromRaw(t *testing.T) {
+	tests := []struct {
+		name     string
+		raw      []map[string]any
+		expected []Link
+	}{
+		{
+			name: "valid raw data",
+			raw: []map[string]any{
+				{"trace_id": "trace1", "span_id": "span1", "trace_state": "state1"},
+				{"trace_id": "trace2", "span_id": "span2", "trace_state": "state2"},
+			},
+			expected: []Link{
+				{TraceID: "trace1", SpanID: "span1", TraceState: "state1"},
+				{TraceID: "trace2", SpanID: "span2", TraceState: "state2"},
+			},
+		},
+		{
+			name:     "empty raw data",
+			raw:      []map[string]any{},
+			expected: []Link{},
+		},
+		{
+			name: "invalid raw data",
+			raw: []map[string]any{
+				{"trace_id": 123, "span_id": nil, "trace_state": true},
+			},
+			expected: []Link{
+				{},
+			},
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(t *testing.T) {
+			result := getLinksFromRaw(test.raw)
+			require.Equal(t, test.expected, result)
+		})
+	}
+}
diff --git a/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel_test.go b/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel_test.go
index 6bdc7b2e..052e6e0a 100644
--- a/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel_test.go
+++ b/internal/storage/v2/clickhouse/tracestore/dbmodel/from_dbmodel_test.go
@@ -39,10 +39,8 @@ func TestFromDBModel_Fixtures(t *testing.T) {
 	exceptedSpan := exceptedScopeSpans.Spans().At(0)
 	actualSpan := actualScopeSpans.Spans().At(0)
 
-	t.Run("Resource", func(t *testing.T) {
-		actualResource := actualResourceSpans.Resource()
-		exceptedResource := expectedResourceSpans.Resource()
-		require.Equal(t, exceptedResource.Attributes().AsRaw(), actualResource.Attributes().AsRaw(), "Resource attributes mismatch")
+	t.Run("Resource", func(*testing.T) {
+		// TODO: add resource attributes comparison
 	})
 
 	t.Run("Scope", func(t *testing.T) {
@@ -50,7 +48,6 @@ func TestFromDBModel_Fixtures(t *testing.T) {
 		expectedScope := exceptedScopeSpans.Scope()
 		require.Equal(t, expectedScope.Name(), actualScope.Name(), "Scope attributes mismatch")
 		require.Equal(t, expectedScope.Version(), actualScope.Version(), "Scope attributes mismatch")
-		require.Equal(t, expectedScope.Attributes().AsRaw(), actualScope.Attributes().AsRaw(), "Scope attributes mismatch")
 	})
 
 	t.Run("Span", func(t *testing.T) {
@@ -64,7 +61,6 @@ func TestFromDBModel_Fixtures(t *testing.T) {
 		require.Equal(t, exceptedSpan.EndTimestamp(), actualSpan.EndTimestamp(), "Span attributes mismatch")
 		require.Equal(t, exceptedSpan.Status().Code(), actualSpan.Status().Code(), "Span attributes mismatch")
 		require.Equal(t, exceptedSpan.Status().Message(), actualSpan.Status().Message(), "Span attributes mismatch")
-		require.Equal(t, exceptedSpan.Attributes().AsRaw(), actualSpan.Attributes().AsRaw(), "Span attributes mismatch")
 	})
 
 	t.Run("Events", func(t *testing.T) {
@@ -76,7 +72,6 @@ func TestFromDBModel_Fixtures(t *testing.T) {
 			actualEvent := actualEvents.At(i)
 			require.Equal(t, exceptedEvent.Name(), actualEvent.Name(), "Event attributes mismatch")
 			require.Equal(t, exceptedEvent.Timestamp(), actualEvent.Timestamp(), "Event attributes mismatch")
-			require.Equal(t, exceptedEvent.Attributes().AsRaw(), actualEvent.Attributes().AsRaw(), "Event %d attributes mismatch", i)
 		}
 	})
 
@@ -90,165 +85,75 @@ func TestFromDBModel_Fixtures(t *testing.T) {
 			require.Equal(t, exceptedLink.TraceID(), actualLink.TraceID(), "Link attributes mismatch")
 			require.Equal(t, exceptedLink.SpanID(), actualLink.SpanID(), "Link attributes mismatch")
 			require.Equal(t, exceptedLink.TraceState(), actualLink.TraceState(), "Link attributes mismatch")
-			require.Equal(t, exceptedLink.Attributes().AsRaw(), actualLink.Attributes().AsRaw(), "Link %d attributes mismatch", i)
 		}
 	})
 }
 
-func jsonToDBModel(t *testing.T, filename string) (m Trace) {
+func jsonToDBModel(t *testing.T, filename string) (m Span) {
 	traceBytes := readJSONBytes(t, filename)
 	err := json.Unmarshal(traceBytes, &m)
 	require.NoError(t, err, "Failed to read file %s", filename)
 	return m
 }
 
-func TestFromDBModel_DecodeAttributes(t *testing.T) {
-	attributesGroup := AttributesGroup{
-		BytesKeys:   []string{"foo"},
-		BytesValues: []string{"0x"},
-	}
-
-	dbTrace := Trace{
-		Resource: Resource{},
-		Scope:    Scope{},
-		Span: Span{
-			TraceId:      "00010001000100010001000100010001",
-			SpanId:       "0001000100010001",
-			ParentSpanId: "0001000100010001",
-		},
-		Events: []Event{
-			{},
-		},
-		Links: []Link{
-			{
-				TraceId: "00010001000100010001000100010001",
-				SpanId:  "0001000100010001",
-			},
-		},
-	}
-
-	tests := []struct {
-		name          string
-		attributeType AttributeType
-		want          string
-	}{
-		{
-			name:          "decode resource attribute failed",
-			attributeType: AttributeTypeResource,
-			want:          "failed to decode resource attribute: illegal base64 data at input byte 0",
-		},
-		{
-			name:          "decode scope attribute failed",
-			attributeType: AttributeTypeScope,
-			want:          "failed to decode scope attribute: illegal base64 data at input byte 0",
-		},
-		{
-			name:          "decode span attribute failed",
-			attributeType: AttributeTypeSpan,
-			want:          "failed to decode span attribute: illegal base64 data at input byte 0",
-		},
-		{
-			name:          "decode event attribute failed",
-			attributeType: AttributeTypeEvent,
-			want:          "failed to decode event attribute: illegal base64 data at input byte 0",
-		},
-		{
-			name:          "decode link attribute failed",
-			attributeType: AttributeTypeLink,
-			want:          "failed to decode link attribute: illegal base64 data at input byte 0",
-		},
-	}
-
-	for _, tt := range tests {
-		t.Run(tt.name, func(t *testing.T) {
-			switch tt.attributeType {
-			case AttributeTypeResource:
-				dbTrace.Resource.Attributes = attributesGroup
-			case AttributeTypeScope:
-				dbTrace.Scope.Attributes = attributesGroup
-			case AttributeTypeSpan:
-				dbTrace.Span.Attributes = attributesGroup
-			case AttributeTypeEvent:
-				dbTrace.Events[0].Attributes = attributesGroup
-			case AttributeTypeLink:
-				dbTrace.Links[0].Attributes = attributesGroup
-			}
-
-			trace := FromDBModel(dbTrace)
-			span := trace.ResourceSpans().At(0).ScopeSpans().At(0).Spans().At(0)
-			require.Contains(t, jptrace.GetWarnings(span), tt.want)
-		})
-	}
-}
-
 func TestFromDBModel_DecodeID(t *testing.T) {
 	tests := []struct {
 		name string
-		arg  Trace
+		arg  Span
 		want string
 	}{
 		{
 			name: "decode span trace id failed",
-			arg: Trace{
-				Span: Span{
-					TraceId: "0x",
-				},
+			arg: Span{
+				TraceID: "0x",
 			},
-			want: "failed to decode trace Id: encoding/hex: invalid byte: U+0078 'x'",
+			want: "failed to decode trace ID: encoding/hex: invalid byte: U+0078 'x'",
 		},
 		{
 			name: "decode span id failed",
-			arg: Trace{
-				Span: Span{
-					TraceId: "00010001000100010001000100010001",
-					SpanId:  "0x",
-				},
+			arg: Span{
+				TraceID: "00010001000100010001000100010001",
+				ID:      "0x",
 			},
-			want: "failed to decode span Id: encoding/hex: invalid byte: U+0078 'x'",
+			want: "failed to decode span ID: encoding/hex: invalid byte: U+0078 'x'",
 		},
 		{
 			name: "decode span parent id failed",
-			arg: Trace{
-				Span: Span{
-					TraceId:      "00010001000100010001000100010001",
-					SpanId:       "0001000100010001",
-					ParentSpanId: "0x",
-				},
+			arg: Span{
+				TraceID:      "00010001000100010001000100010001",
+				ID:           "0001000100010001",
+				ParentSpanID: "0x",
 			},
-			want: "failed to decode parent span Id: encoding/hex: invalid byte: U+0078 'x'",
+			want: "failed to decode parent span ID: encoding/hex: invalid byte: U+0078 'x'",
 		},
 		{
 			name: "decode link trace id failed",
-			arg: Trace{
-				Span: Span{
-					TraceId:      "00010001000100010001000100010001",
-					SpanId:       "0001000100010001",
-					ParentSpanId: "0001000100010001",
-				},
+			arg: Span{
+				TraceID:      "00010001000100010001000100010001",
+				ID:           "0001000100010001",
+				ParentSpanID: "0001000100010001",
 				Links: []Link{
 					{
-						TraceId: "0x",
+						TraceID: "0x",
 					},
 				},
 			},
-			want: "failed to decode link trace Id: encoding/hex: invalid byte: U+0078 'x'",
+			want: "failed to decode link trace ID: encoding/hex: invalid byte: U+0078 'x'",
 		},
 		{
 			name: "decode link span id failed",
-			arg: Trace{
-				Span: Span{
-					TraceId:      "00010001000100010001000100010001",
-					SpanId:       "0001000100010001",
-					ParentSpanId: "0001000100010001",
-				},
+			arg: Span{
+				TraceID:      "00010001000100010001000100010001",
+				ID:           "0001000100010001",
+				ParentSpanID: "0001000100010001",
 				Links: []Link{
 					{
-						TraceId: "00010001000100010001000100010001",
-						SpanId:  "0x",
+						TraceID: "00010001000100010001000100010001",
+						SpanID:  "0x",
 					},
 				},
 			},
-			want: "failed to decode link span Id: encoding/hex: invalid byte: U+0078 'x'",
+			want: "failed to decode link span ID: encoding/hex: invalid byte: U+0078 'x'",
 		},
 	}
 
@@ -322,12 +227,12 @@ func TestFromDBSpanKind(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			require.Equal(t, tt.want, FromDBSpanKind(tt.args.sk))
+			require.Equal(t, tt.want, convertSpanKind(tt.args.sk))
 		})
 	}
 }
 
-func TestFromDBStatusCode(t *testing.T) {
+func TestConvertStatusCode(t *testing.T) {
 	type args struct {
 		sc string
 	}
@@ -367,7 +272,7 @@ func TestFromDBStatusCode(t *testing.T) {
 	}
 	for _, tt := range tests {
 		t.Run(tt.name, func(t *testing.T) {
-			require.Equal(t, tt.want, FromDBStatusCode(tt.args.sc))
+			require.Equal(t, tt.want, convertStatusCode(tt.args.sc))
 		})
 	}
 }