Repository: scylladb/scylla-manager
Pull request #4257: feat(1-1-restore): adds support for views
Author: login: VAveryanov8
Created: 2025-02-18T09:44:27Z | Merged: 2025-03-05T08:43:43Z

This adds support for restoring views in 1-1-restore. Views should be dropped before copy data stage and then re-created to avoid data inconsistency between base table and view.
Integration test extended with MaterializedView. 

Refs: #4252

---

Please make sure that:
- Code is split to commits that address a single change
- Commit messages are informative
- Commit titles have module prefix
- Commit titles have issue nr. suffix



Diff:
diff --git a/pkg/service/one2onerestore/model.go b/pkg/service/one2onerestore/model.go
index 8dd6988e55..1b8e40302a 100644
--- a/pkg/service/one2onerestore/model.go
+++ b/pkg/service/one2onerestore/model.go
@@ -41,6 +41,32 @@ type Host struct {
 	Addr string
 }
 
+// ViewType either Materialized View or Secondary Index.
+type ViewType string
+
+// ViewType enumeration.
+const (
+	MaterializedView ViewType = "MaterializedView"
+	SecondaryIndex   ViewType = "SecondaryIndex"
+)
+
+// View represents statement used for recreating restored (dropped) views.
+type View struct {
+	Keyspace   string   `json:"keyspace" db:"keyspace_name"`
+	View       string   `json:"view" db:"view_name"`
+	Type       ViewType `json:"type" db:"view_type"`
+	BaseTable  string   `json:"base_table"`
+	CreateStmt string   `json:"create_stmt"`
+}
+
+// hostWorkload represents what data (manifest) from the backup should be handled
+// by which node (host) in the target cluster.
+type hostWorkload struct {
+	host            Host
+	manifestInfo    *ManifestInfo
+	manifestContent *ManifestContentWithIndex
+}
+
 func (t *Target) validateProperties() error {
 	if len(t.Location) == 0 {
 		return errors.New("missing location")
diff --git a/pkg/service/one2onerestore/service.go b/pkg/service/one2onerestore/service.go
index 7d8e44f5d4..46a64bcb89 100644
--- a/pkg/service/one2onerestore/service.go
+++ b/pkg/service/one2onerestore/service.go
@@ -87,8 +87,13 @@ func (s *Service) One2OneRestore(ctx context.Context, clusterID, taskID, runID u
 	}
 	s.logger.Info(ctx, "Can proceed with 1-1-restore")
 
+	workload, err := w.prepareHostWorkload(ctx, manifests, hosts, target.NodesMapping)
+	if err != nil {
+		return errors.Wrap(err, "prepare hosts workload")
+	}
+
 	start := timeutc.Now()
-	if err := w.restoreTables(ctx, manifests, hosts, target.NodesMapping, target.Keyspace); err != nil {
+	if err := w.restore(ctx, workload, target); err != nil {
 		return errors.Wrap(err, "restore data")
 	}
 	s.logger.Info(ctx, "Data restore is completed", "took", timeutc.Since(start))
diff --git a/pkg/service/one2onerestore/service_integration_test.go b/pkg/service/one2onerestore/service_integration_test.go
index dc3f3c2978..660f7eac9b 100644
--- a/pkg/service/one2onerestore/service_integration_test.go
+++ b/pkg/service/one2onerestore/service_integration_test.go
@@ -29,10 +29,16 @@ func TestOne2OneRestoreServiceIntegration(t *testing.T) {
 
 	ksName := "testrestore"
 	WriteData(t, clusterSession, ksName, 10)
+	mvName := "testmv"
+	CreateMaterializedView(t, clusterSession, ksName, BigTableName, mvName)
 
 	srcCnt := rowCount(t, clusterSession, ksName, BigTableName)
 	if srcCnt == 0 {
-		t.Fatalf("Unexpected row count: 0")
+		t.Fatalf("Unexpected row count in table: 0")
+	}
+	srcCntMV := rowCount(t, clusterSession, ksName, mvName)
+	if srcCntMV == 0 {
+		t.Fatalf("Unexpected row count in materialized view: 0")
 	}
 
 	Print("Run backup")
@@ -44,8 +50,10 @@ func TestOne2OneRestoreServiceIntegration(t *testing.T) {
 
 	Print("Truncate tables")
 	truncateAllTablesInKeyspace(t, clusterSession, ksName)
-	if cnt := rowCount(t, clusterSession, ksName, BigTableName); cnt != 0 {
-		t.Fatalf("Unexpected row count: %d", cnt)
+	for _, tableName := range []string{BigTableName, mvName} {
+		if cnt := rowCount(t, clusterSession, ksName, tableName); cnt != 0 {
+			t.Fatalf("Unexpected row count: %d", cnt)
+		}
 	}
 
 	Print("Run 1-1-restore")
@@ -59,7 +67,11 @@ func TestOne2OneRestoreServiceIntegration(t *testing.T) {
 	Print("Validate data")
 	dstCnt := rowCount(t, clusterSession, ksName, BigTableName)
 	if srcCnt != dstCnt {
-		t.Fatalf("Expected row count %d, but got %d", srcCnt, dstCnt)
+		t.Fatalf("Expected row count in table %d, but got %d", srcCnt, dstCnt)
+	}
+	dstCntMV := rowCount(t, clusterSession, ksName, mvName)
+	if srcCntMV != dstCntMV {
+		t.Fatalf("Expected row count in materialized view %d, but got %d", srcCntMV, dstCntMV)
 	}
 }
 
diff --git a/pkg/service/one2onerestore/worker.go b/pkg/service/one2onerestore/worker.go
index c1ece8a031..2dbdba2063 100644
--- a/pkg/service/one2onerestore/worker.go
+++ b/pkg/service/one2onerestore/worker.go
@@ -4,12 +4,17 @@ package one2onerestore
 
 import (
 	"context"
+	"strings"
+	"time"
 
 	"github.com/pkg/errors"
 	"github.com/scylladb/go-log"
 	"github.com/scylladb/gocqlx/v2"
 	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
 	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/parallel"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/retry"
+	"go.uber.org/multierr"
 )
 
 type worker struct {
@@ -21,6 +26,24 @@ type worker struct {
 	logger log.Logger
 }
 
+// restore is an actual 1-1-restore stages.
+func (w *worker) restore(ctx context.Context, workload []hostWorkload, target Target) (err error) {
+	views, err := w.dropViews(ctx, workload, target.Keyspace)
+	if err != nil {
+		return errors.Wrap(err, "drop views")
+	}
+	defer func() {
+		if rErr := w.reCreateViews(ctx, views); rErr != nil {
+			err = multierr.Combine(
+				err,
+				errors.Wrap(rErr, "recreate views"),
+			)
+		}
+	}()
+
+	return w.restoreTables(ctx, workload, target.Keyspace)
+}
+
 // getAllSnapshotManifestsAndTargetHosts gets backup(source) cluster node represented by manifests and target cluster nodes.
 func (w *worker) getAllSnapshotManifestsAndTargetHosts(ctx context.Context, target Target) ([]*backupspec.ManifestInfo, []Host, error) {
 	nodeStatus, err := w.client.Status(ctx)
@@ -79,3 +102,55 @@ func nodesToHosts(nodes scyllaclient.NodeStatusInfoSlice) []Host {
 	}
 	return hosts
 }
+
+// prepareHostWorkload is a helper function that creates a hostWorkload structure convenient for use in later 1-1-restore stages.
+// This avoids the need to repeat operations like node mapping and fetching manifest content.
+func (w *worker) prepareHostWorkload(ctx context.Context, manifests []*backupspec.ManifestInfo, hosts []Host, nodeMappings []nodeMapping) ([]hostWorkload, error) {
+	targetBySourceHostID, err := mapTargetHostToSource(hosts, nodeMappings)
+	if err != nil {
+		return nil, errors.Wrap(err, "invalid node mapping")
+	}
+
+	result := make([]hostWorkload, len(manifests))
+	return result, parallel.Run(len(manifests), len(manifests), func(i int) error {
+		m := manifests[i]
+		h := targetBySourceHostID[m.NodeID]
+
+		mc, err := w.getManifestContent(ctx, h.Addr, m)
+		if err != nil {
+			return errors.Wrap(err, "manifest content")
+		}
+		result[i] = hostWorkload{
+			host:            h,
+			manifestInfo:    m,
+			manifestContent: mc,
+		}
+
+		return nil
+	}, parallel.NopNotify)
+}
+
+// alterSchemaRetryWrapper is useful when executing many statements altering schema,
+// as it might take more time for Scylla to process them one after another.
+// This wrapper exits on: success, context cancel, op returned non-timeout error or after maxTotalTime has passed.
+func alterSchemaRetryWrapper(ctx context.Context, op func() error, notify func(err error, wait time.Duration)) error {
+	const (
+		minWait      = 5 * time.Second
+		maxWait      = 1 * time.Minute
+		maxTotalTime = 15 * time.Minute
+		multiplier   = 2
+		jitter       = 0.2
+	)
+	backoff := retry.NewExponentialBackoff(minWait, maxTotalTime, maxWait, multiplier, jitter)
+
+	wrappedOp := func() error {
+		err := op()
+		if err == nil || strings.Contains(err.Error(), "timeout") {
+			return err
+		}
+		// All non-timeout errors shouldn't be retried
+		return retry.Permanent(err)
+	}
+
+	return retry.WithNotify(ctx, wrappedOp, backoff, notify)
+}
diff --git a/pkg/service/one2onerestore/worker_tables.go b/pkg/service/one2onerestore/worker_tables.go
index 57d9ebb5de..d86238b441 100644
--- a/pkg/service/one2onerestore/worker_tables.go
+++ b/pkg/service/one2onerestore/worker_tables.go
@@ -13,41 +13,29 @@ import (
 	"github.com/scylladb/scylla-manager/v3/pkg/util/parallel"
 )
 
-func (w *worker) restoreTables(ctx context.Context, manifests []*backupspec.ManifestInfo, hosts []Host, nodeMappings []nodeMapping, keyspaces []string) error {
-	targetBySourceHostID, err := mapTargetHostToSource(hosts, nodeMappings)
-	if err != nil {
-		return errors.Wrap(err, "invalid node mapping")
-	}
-
+func (w *worker) restoreTables(ctx context.Context, workload []hostWorkload, keyspaces []string) error {
 	logError := func(i int, err error) {
-		w.logger.Error(ctx, "Restore data", "err", err, "node_id", manifests[i].NodeID)
+		w.logger.Error(ctx, "Restore data", "err", err, "host", workload[i].host)
 	}
-	return parallel.Run(len(manifests), len(manifests), func(i int) error {
-		m := manifests[i]
-		h := targetBySourceHostID[m.NodeID]
-
-		mc, err := w.getManifestContent(ctx, h.Addr, m)
-		if err != nil {
-			return errors.Wrap(err, "manifest content")
-		}
-
+	return parallel.Run(len(workload), len(workload), func(i int) error {
+		hostTask := workload[i]
 		const (
 			repeatInterval  = 10 * time.Second
 			pollIntervalSec = 10
 		)
-		return mc.ForEachIndexIterWithError(keyspaces, func(table backupspec.FilesMeta) error {
+		return hostTask.manifestContent.ForEachIndexIterWithError(keyspaces, func(table backupspec.FilesMeta) error {
 			w.logger.Info(ctx, "Restoring data", "ks", table.Keyspace, "table", table.Table, "size", table.Size)
 
-			jobID, err := w.createDownloadJob(ctx, table, m, h)
+			jobID, err := w.createDownloadJob(ctx, table, hostTask.manifestInfo, hostTask.host)
 			if err != nil {
 				return errors.Wrapf(err, "create download job: %s.%s", table.Keyspace, table.Table)
 			}
 
-			if err := w.waitJob(ctx, h, jobID, pollIntervalSec); err != nil {
+			if err := w.waitJob(ctx, hostTask.host, jobID, pollIntervalSec); err != nil {
 				return errors.Wrapf(err, "wait job: %s.%s", table.Keyspace, table.Table)
 			}
 
-			if err := w.refreshNode(ctx, table, h, repeatInterval); err != nil {
+			if err := w.refreshNode(ctx, table, hostTask.host, repeatInterval); err != nil {
 				return errors.Wrapf(err, "refresh node: %s.%s", table.Keyspace, table.Table)
 			}
 			return nil
diff --git a/pkg/service/one2onerestore/worker_views.go b/pkg/service/one2onerestore/worker_views.go
new file mode 100644
index 0000000000..a806a77a89
--- /dev/null
+++ b/pkg/service/one2onerestore/worker_views.go
@@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
+		}
+		return w.waitForViewBuilding(ctx, view)
+	}
+
+	return nil
+}
diff --git a/pkg/service/one2onerestore/worker_views_test.go b/pkg/service/one2onerestore/worker_views_test.go
new file mode 100644
index 0000000000..8ecd4ac8e6
--- /dev/null
+++ b/pkg/service/one2onerestore/worker_views_test.go
@@ -0,0 +1,211 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"net/http"
+	"net/http/httptest"
+	"net/url"
+	"testing"
+	"time"
+
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient/scyllaclienttest"
+)
+
+func TestAddIfNotExists(t *testing.T) {
+	testCases := []struct {
+		name         string
+		viewType     ViewType
+		stmt         string
+		expectedStmt string
+		expectedErr  bool
+	}{
+		{
+			name:     "Modifies MaterializedView stmt",
+			viewType: MaterializedView,
+			stmt: `CREATE MATERIALIZED VIEW ks.mv AS SELECT a,b FROM ks.t 
+				WHERE b is NOT NULL 
+				PRIMARY KEY(b);`,
+			expectedStmt: `CREATE MATERIALIZED VIEW IF NOT EXISTS ks.mv AS SELECT a,b FROM ks.t 
+				WHERE b is NOT NULL 
+				PRIMARY KEY(b);`,
+			expectedErr: false,
+		},
+		{
+			name:         "Modifies Secondary Index stmt",
+			viewType:     SecondaryIndex,
+			stmt:         `CREATE INDEX buildings_by_city ON buildings (city);`,
+			expectedStmt: `CREATE INDEX IF NOT EXISTS buildings_by_city ON buildings (city);`,
+			expectedErr:  false,
+		},
+		{
+			name:     "Wrong view type, SecondaryIndex instead of MaterializedView",
+			viewType: SecondaryIndex,
+			stmt: `CREATE MATERIALIZED VIEW ks.mv AS SELECT a,b FROM ks.t 
+				WHERE b is NOT NULL 
+				PRIMARY KEY(b);`,
+			expectedErr: true,
+		},
+		{
+			name:        "Wrong view type, MaterializedView instead of SecondaryIndex",
+			viewType:    MaterializedView,
+			stmt:        `CREATE INDEX buildings_by_city ON buildings (city);`,
+			expectedErr: true,
+		},
+		{
+			name:     "Unknown view type",
+			viewType: "Hello",
+			stmt: `CREATE MATERIALIZED VIEW ks.mv AS SELECT a,b FROM ks.t 
+				WHERE b is NOT NULL 
+				PRIMARY KEY(b);`,
+			expectedErr: true,
+		},
+		{
+			name:        "Unknown statement",
+			viewType:    SecondaryIndex,
+			stmt:        "Hello world",
+			expectedErr: true,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			actual, err := addIfNotExists(tc.stmt, tc.viewType)
+			if err != nil && !tc.expectedErr {
+				t.Fatalf("Unexpected err: %v", err)
+			}
+			if err == nil && tc.expectedErr {
+				t.Fatalf("Expected err, but got nil")
+			}
+
+			if actual != tc.expectedStmt {
+				t.Fatalf("Actual != Expected: %q != %q", actual, tc.expectedStmt)
+			}
+		})
+	}
+}
+
+func TestWaitForViewBuilding(t *testing.T) {
+	testCases := []struct {
+		name            string
+		view            View
+		handler         http.HandlerFunc
+		contextProvider func() context.Context
+		expectedCalls   int32
+		expectedErr     bool
+	}{
+		{
+			name: "Everything is fine",
+			view: View{
+				Keyspace: "ks",
+				View:     "mv",
+				Type:     MaterializedView,
+			},
+			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.Write([]byte(`[{"value":"SUCCESS"}]`))
+			}),
+			contextProvider: context.Background,
+			expectedCalls:   1,
+			expectedErr:     false,
+		},
+		{
+			name: "Retries when not SUCCESS",
+			view: View{
+				Keyspace: "ks",
+				View:     "mv",
+				Type:     MaterializedView,
+			},
+			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				// This magic header is set by testHandler.
+				i := r.Header.Get("test-call-i")
+				if i == "1" {
+					w.Write([]byte(`[{"value":"STARTED"}]`))
+					return
+				}
+
+				w.Write([]byte(`[{"value":"SUCCESS"}]`))
+			}),
+			contextProvider: context.Background,
+			expectedCalls:   2,
+			expectedErr:     false,
+		},
+		{
+			name: "Exit on context cancel",
+			view: View{
+				Keyspace: "ks",
+				View:     "mv",
+				Type:     MaterializedView,
+			},
+			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				time.Sleep(100 * time.Millisecond)
+				w.Write([]byte(`[{"value":"SUCCESS"}]`))
+			}),
+			contextProvider: func() context.Context {
+				ctx, cancel := context.WithTimeout(context.Background(), 50*time.Millisecond)
+				_ = cancel
+				return ctx
+			},
+			expectedErr:   true,
+			expectedCalls: 1,
+		},
+		{
+			name: "Exit on error",
+			view: View{
+				Keyspace: "ks",
+				View:     "mv",
+				Type:     MaterializedView,
+			},
+			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(http.StatusTeapot)
+				w.Write([]byte(`{"message": "wtf", "code": 418}`))
+			}),
+			contextProvider: context.Background,
+			expectedErr:     true,
+			expectedCalls:   1,
+		},
+		{
+			name: "Exit on timeout",
+			view: View{
+				Keyspace: "ks",
+				View:     "mv",
+				Type:     MaterializedView,
+			},
+			handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+				w.WriteHeader(http.StatusRequestTimeout)
+				w.Write([]byte(`{"message": "timeout", "code": 408}`))
+			}),
+			contextProvider: context.Background,
+			expectedErr:     true,
+			expectedCalls:   1,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			handler := &testHandler{Handler: tc.handler}
+			ts := httptest.NewServer(handler)
+			defer ts.Close()
+
+			tsAddr, err := url.Parse(ts.URL)
+			if err != nil {
+				t.Fatalf("Unexpected err: %v", err)
+			}
+
+			w := &worker{
+				client: scyllaclienttest.MakeClient(t, tsAddr.Hostname(), tsAddr.Port()),
+			}
+			err = w.waitForViewBuilding(tc.contextProvider(), tc.view)
+			if err != nil && !tc.expectedErr {
+				t.Fatalf("Unexpected err: %v", err)
+			}
+			if err == nil && tc.expectedErr {
+				t.Fatalf("Expected err, but got nil")
+			}
+			if tc.expectedCalls != handler.calls.Load() {
+				t.Fatalf("Expected %d calls, but got %d", tc.expectedCalls, handler.calls.Load())
+			}
+		})
+	}
+}


Reviews:
user: login: karol-kokoszka
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-03T11:51:46Z
user: login: copilot-pull-request-reviewer[bot]
body: ## PR Overview

This PR adds support for restoring views during a 1-1 restore operation by dropping views (materialized views and secondary indexes) prior to data copy and re-creating them afterward. Key changes include:
- Introducing functions to drop and re-create views with proper logging and error handling.
- Adding comprehensive tests for view restoration and view building status.
- Refactoring worker and table restore functionalities to use the new view handling functions.

### Reviewed Changes

| File                                           | Description                                                                                       |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------- |
| pkg/service/one2onerestore/worker_views.go     | Adds functions to process view dropping/re-creation and view statement modification.               |
| pkg/service/one2onerestore/worker_views_test.go  | Provides unit tests for view statement modifications and waiting for view building.                |
| pkg/service/one2onerestore/worker.go           | Refactors the restore flow to prepare host workloads and properly handle view restoration.         |
| pkg/service/one2onerestore/worker_tables.go      | Adjusts table restore function to use the new hostWorkload approach.                               |
| pkg/service/one2onerestore/service_integration_test.go | Updates integration test with more descriptive error messages and validation for views.             |
| pkg/service/one2onerestore/model.go            | Introduces model definitions for View, ViewType, and hostWorkload.                                |

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.



state: COMMENTED
author_association: NONE
submitted_at: 2025-03-04T09:58:57Z
user: login: Michal-Leszczynski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:02:05Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:19:01Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:19:06Z
user: login: Michal-Leszczynski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T10:57:59Z
user: login: Michal-Leszczynski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T11:46:25Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:27:16Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:53:11Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T14:56:41Z
user: login: Michal-Leszczynski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:00:12Z
user: login: Michal-Leszczynski
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:05:47Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T15:10:22Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T16:03:51Z
user: login: VAveryanov8
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-04T16:10:08Z
user: login: Michal-Leszczynski
body: Looks good, I will make a follow up issue from https://github.com/scylladb/scylla-manager/pull/4257#issuecomment-2696947984 so that it's not lost.
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-05T08:39:31Z

Review comments:
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: nit: are there plans to make this 10 seconds configurable? 
We can't use long polling when waiting for view build status, so it adds up to 10 seconds of idle time per restored view. In the prod it might not matter that much, but it might significantly increase the test time.
created_at: 2025-03-04T09:38:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
+		}
+		return w.waitForViewBuilding(ctx, view)
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: nit: do you think that recursion simplifies the flow here?
I would say that replacing recursion with iteration is usually a good practice, unless recursive code is nicer and easier to maintain, but it looks like it's simple to do here. Moreover, for a bigger view, we might make quite a lot of the recursive calls (even though they shouldn't impact stack in a significant way).
created_at: 2025-03-04T09:44:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: So this is a code copied from regular restore.
It was written for older times when we didn't back up schema as CQL files, so we needed to get the create statements in some other way. In general we should prefer to rely on the backed up CQL file, as here we are using a drivers method which might not be up to date with the current state of describing the view.

In terms of implementing pause/resume and SM crash resistance, it also makes things easier for us, as we don't need to store view descriptions in SM DB, as they are always available as a part of the backup.

The question is: should this be done as a separate issue/PR (also for the regular restore), or do we want to start doing it properly here, so that we avoid thins like introducing views to SM DB and having to worry about it.
created_at: 2025-03-04T09:51:10Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: I know that this is copied from regular restore, but it looks silly (of course - implemented by me some time ago). If we want to support pause/resume and SM crash resistance, at the beginning of the restore view stage we should just:
- check all already recreated views
- check if there is any view that is still being recreated (if so, wait for it)
- recreate and wait for the remaining views

This seems way more robust than just sticking the "IF NOT EXISTS" clause and moving forward. It also allows for avoiding string parsing, which is nice.

The question is: should this be done as a separate issue/PR (also for the regular restore), or do we want to start doing it properly here? In contrast to the comment about `getViews`, it shouldn't be difficult to change it in the future.
created_at: 2025-03-04T09:58:15Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
path: pkg/service/one2onerestore/worker_views.go
user: login: Copilot
body: Accessing the second element after splitting the schema string may result in an index out of range error if the schema does not contain a semicolon or if its format changes. Consider checking that the split result has the expected length before indexing.
```suggestion
			schemaParts := strings.Split(schema, ";")
			if len(schemaParts) < 2 {
				return nil, errors.Errorf("invalid schema format for index %s.%s", ks, index.Name)
			}
			stmt := schemaParts[1]
```
created_at: 2025-03-04T09:58:57Z
author_association: NONE
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: Not sure about making them configurable - it doesn't affect the integration test time, because test views are small and rebuilding them is happening almost immediately, so we don't wait at all. But if it will be ever needed, it's won't take any time to change. 
created_at: 2025-03-04T10:19:01Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
+		}
+		return w.waitForViewBuilding(ctx, view)
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body:  Usually I try to avoid recursion as well, but in this particular case it looked nice to me, because I didn't want this code to sleep for 10 sec before the first call - this reduces test time significantly. Obliviously the same can be done without recursion, but imho the code won't look nice. 

BTW: we may want to modify https://github.com/scylladb/scylla-manager/blob/5cb098900e65910ae9ac44dd277b304a445d2d11/pkg/service/restore/worker.go#L587, so it's doesn't sleep for 10 sec, before making first call - it will speed up test time.
created_at: 2025-03-04T10:19:05Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
+		}
+		return w.waitForViewBuilding(ctx, view)
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: > BTW: we may want to modify 

Should be `we MUST modify`:)) It's crazy that something like was overlooked for so long.
The funny part is that initially it was implemented without the wait for the first tick, but this got lost somewhere during refactor.
created_at: 2025-03-04T10:57:59Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
+
+// reCreateViews re-creates views (materialized views or secondary indexes).
+func (w *worker) reCreateViews(ctx context.Context, views []View) error {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Re-create views", "took", timeutc.Since(start))
+	}()
+	for _, view := range views {
+		if err := w.createView(ctx, view); err != nil {
+			return errors.Wrap(err, "create view")
+		}
+		if err := w.waitForViewBuilding(ctx, view); err != nil {
+			return errors.Wrap(err, "wait for view")
+		}
+	}
+	return nil
+}
+
+// createView creates specified Materialized View or Secondary Index.
+func (w *worker) createView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Creating view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+		"statement", view.CreateStmt,
+	)
+
+	op := func() error {
+		return w.clusterSession.ExecStmt(view.CreateStmt)
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Creating view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+// Scylla operation might take a really long (and difficult to estimate) time.
+// This func exits ONLY on: success, context cancel or error.
+func (w *worker) waitForViewBuilding(ctx context.Context, view View) error {
+	viewTableName := view.View
+	if view.Type == SecondaryIndex {
+		viewTableName += "_index"
+	}
+
+	status, err := w.client.ViewBuildStatus(ctx, view.Keyspace, viewTableName)
+	if err != nil {
+		return err
+	}
+
+	if status == scyllaclient.StatusUnknown || status == scyllaclient.StatusStarted {
+		w.logger.Info(ctx, "Waiting for view",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"status", status,
+		)
+		select {
+		case <-ctx.Done():
+			return ctx.Err()
+		case <-time.After(10 * time.Second):
+		}
+		return w.waitForViewBuilding(ctx, view)
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: I created an issue for that: https://github.com/scylladb/scylla-manager/issues/4282.
created_at: 2025-03-04T11:46:25Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: Good point! In general I'd prefer to do it right await, but at first glance, it doesn't seem straightforward - e.g. `query.DescribeSchema` is missing information about base table, which is useful to drop only views based on what tables will be restored. So maybe it's better to do it in a separate issue/pr, so it can be properly solved. 
created_at: 2025-03-04T14:27:16Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: > * check all already recreated views
> * check if there is any view that is still being recreated (if so, wait for it)
> * recreate and wait for the remaining views

Agree that it would be better, then just relying on `if not exists` statement. As it's more related to pause/resume, then it's better to create either as a separate issue or just include into existing one related to pause/resume. (looks like issue for pause/resume hasn't been created yet :) ) 
created_at: 2025-03-04T14:53:11Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: @Michal-Leszczynski  Maybe you know a good way of how to get a base table name for the mv/si from the describe schema output?  
created_at: 2025-03-04T14:56:41Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: That's right, although at first we are going to restore with keyspace granularity, so we always restore all tables and views from given keyspace, but it might be problematic when we allow for more granular filtering.
created_at: 2025-03-04T15:00:12Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: Michal-Leszczynski
body: > @Michal-Leszczynski Maybe you know a good way of how to get a base table name for the mv/si from the describe schema output?

Not really, so I guess that we would need to either:
- save the list of restore views in the DB (alongside the list of restored tables)
- treat restored views just like restored tables in therms of the more granular --keyspace flag, so that we can use it to determine which views should be recreated
created_at: 2025-03-04T15:05:47Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: I have in my mind some kind of mixed approach: 
 - get the mv/si metadata using driver (it contains base table info) 
 - get `create ...` statement from describe schema output

But, yeah, let's address it in a separate issue/pr
created_at: 2025-03-04T15:10:22Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: https://github.com/scylladb/scylla-manager/issues/4285
created_at: 2025-03-04T16:03:51Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,252 @@
+// Copyright (C) 2025 ScyllaDB
+
+package one2onerestore
+
+import (
+	"context"
+	"fmt"
+	"regexp"
+	"strings"
+	"time"
+
+	"github.com/gocql/gocql"
+	"github.com/pkg/errors"
+	"github.com/scylladb/go-set/strset"
+	"github.com/scylladb/scylla-manager/v3/pkg/scyllaclient"
+	"github.com/scylladb/scylla-manager/v3/pkg/service/backup/backupspec"
+	"github.com/scylladb/scylla-manager/v3/pkg/util/timeutc"
+)
+
+// dropsViews drops all Materialized View or Secondary Index which base tables are exists in the backup and returns
+// what was dropped, so it can be re-created using worker.reCreateViews.
+func (w *worker) dropViews(ctx context.Context, workload []hostWorkload, keyspaceFilter []string) ([]View, error) {
+	start := timeutc.Now()
+	defer func() {
+		w.logger.Info(ctx, "Drop views", "took", timeutc.Since(start))
+	}()
+
+	tablesToRestore := strset.New()
+	for _, wl := range workload {
+		err := wl.manifestContent.ForEachIndexIter(keyspaceFilter, func(fm backupspec.FilesMeta) {
+			tablesToRestore.Add(fm.Keyspace + "." + fm.Table)
+		})
+		if err != nil {
+			return nil, errors.Wrap(err, "manifest content files")
+		}
+	}
+
+	views, err := w.getViews(ctx, tablesToRestore)
+	if err != nil {
+		return nil, errors.Wrap(err, "get views")
+	}
+
+	for _, view := range views {
+		if err := w.dropView(ctx, view); err != nil {
+			return nil, errors.Wrap(err, "drop view")
+		}
+	}
+
+	return views, nil
+}
+
+func (w *worker) dropView(ctx context.Context, view View) error {
+	w.logger.Info(ctx, "Dropping view",
+		"keyspace", view.Keyspace,
+		"view", view.View,
+		"type", view.Type,
+	)
+
+	op := func() error {
+		dropStmt := ""
+		switch view.Type {
+		case SecondaryIndex:
+			dropStmt = "DROP INDEX IF EXISTS %q.%q"
+		case MaterializedView:
+			dropStmt = "DROP MATERIALIZED VIEW IF EXISTS %q.%q"
+		}
+
+		return w.clusterSession.ExecStmt(fmt.Sprintf(dropStmt, view.Keyspace, view.View))
+	}
+
+	notify := func(err error, wait time.Duration) {
+		w.logger.Info(ctx, "Dropping view failed",
+			"keyspace", view.Keyspace,
+			"view", view.View,
+			"type", view.Type,
+			"error", err,
+			"wait", wait,
+		)
+	}
+
+	return alterSchemaRetryWrapper(ctx, op, notify)
+}
+
+func (w *worker) getViews(ctx context.Context, tablesToRestore *strset.Set) ([]View, error) {
+	keyspaces, err := w.client.Keyspaces(ctx)
+	if err != nil {
+		return nil, errors.Wrapf(err, "get keyspaces")
+	}
+
+	var views []View
+	for _, ks := range keyspaces {
+		meta, err := w.clusterSession.KeyspaceMetadata(ks)
+		if err != nil {
+			return nil, errors.Wrapf(err, "get keyspace %s metadata", ks)
+		}
+
+		for _, index := range meta.Indexes {
+			if !tablesToRestore.Has(index.KeyspaceName + "." + index.TableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Indexes: map[string]*gocql.IndexMetadata{index.Name: index},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get index %s.%s create statement", ks, index.Name)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, SecondaryIndex)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   index.KeyspaceName,
+				View:       index.Name,
+				Type:       SecondaryIndex,
+				BaseTable:  index.TableName,
+				CreateStmt: stmt,
+			})
+		}
+
+		for _, view := range meta.Views {
+			if !tablesToRestore.Has(view.KeyspaceName + "." + view.BaseTableName) {
+				continue
+			}
+			dummyMeta := gocql.KeyspaceMetadata{
+				Views: map[string]*gocql.ViewMetadata{view.ViewName: view},
+			}
+
+			schema, err := dummyMeta.ToCQL()
+			if err != nil {
+				return nil, errors.Wrapf(err, "get view %s.%s create statement", ks, view.ViewName)
+			}
+
+			// DummyMeta schema consists of create keyspace and create view statements
+			stmt := strings.Split(schema, ";")[1]
+			stmt, err = addIfNotExists(stmt, MaterializedView)
+			if err != nil {
+				return nil, err
+			}
+
+			views = append(views, View{
+				Keyspace:   view.KeyspaceName,
+				View:       view.ViewName,
+				Type:       MaterializedView,
+				BaseTable:  view.BaseTableName,
+				CreateStmt: stmt,
+			})
+		}
+	}
+
+	return views, nil
+}
+
+var (
+	regexMV = regexp.MustCompile(`CREATE\s*MATERIALIZED\s*VIEW`)
+	regexSI = regexp.MustCompile(`CREATE\s*INDEX`)
+)
+
+// create stmt has to contain "IF NOT EXISTS" clause as we have to be able to resume restore from any point.
+func addIfNotExists(stmt string, t ViewType) (string, error) {
+	var loc []int
+	switch t {
+	case MaterializedView:
+		loc = regexMV.FindStringIndex(stmt)
+	case SecondaryIndex:
+		loc = regexSI.FindStringIndex(stmt)
+	}
+	if loc == nil {
+		return "", fmt.Errorf("unknown create view statement %s", stmt)
+	}
+
+	return stmt[loc[0]:loc[1]] + " IF NOT EXISTS" + stmt[loc[1]:], nil
+}
path: pkg/service/one2onerestore/worker_views.go
user: login: VAveryanov8
body: https://github.com/scylladb/scylla-manager/issues/4286
created_at: 2025-03-04T16:10:08Z
author_association: COLLABORATOR

PR comments:
user: login: Michal-Leszczynski
created_at: 2025-03-03T12:13:28Z
body: @VAveryanov8 I will review this PR tomorrow morning. Sorry for the delay!!!
author_association: COLLABORATOR
user: login: Michal-Leszczynski
created_at: 2025-03-04T10:15:13Z
body: This PR adds support for view restoration during 1-1 full cluster restore. The procedure assumes that the correct schema is already in the cluster and that it needs to restore just the table's data (user is not supposed to use the cluster when the restore is running):
- drop all views
- restore the data of the base tables
- recreate all views one by one (issue create stmt for a view, wait for its build to finish, move to next view)

Restoring views one by one gives us nicer progress tracking (IIRC we don't have percentage view build status updates), but perhaps it does not utilize cluster resources to its fullest.

@nyh should we expect Scylla to correctly handle concurrent creation of all of the views from the backup (possibly thousands of them) while utilizing cluster resources to the fullest, or is it expected to either have no performance boost or just simply overwhelm Scylla and cause performance degradation.
Another approach could be to concurrently restore up to X (e.g. 2 or 3) views at any moment, so that we don't overwhelm Scylla but we utilize cluster resources to a better degree.
author_association: COLLABORATOR