github.com/dolthub/dolt/go@v0.40.5-0.20240520175717-68db7794bea6/libraries/doltcore/sqle/writer/noms_write_session.go (about)

     1  // Copyright 2020 Dolthub, Inc.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package writer
    16  
    17  import (
    18  	"context"
    19  	"fmt"
    20  	"sync"
    21  
    22  	"github.com/dolthub/go-mysql-server/sql"
    23  	"golang.org/x/sync/errgroup"
    24  
    25  	"github.com/dolthub/dolt/go/libraries/doltcore/doltdb"
    26  	"github.com/dolthub/dolt/go/libraries/doltcore/schema"
    27  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/globalstate"
    28  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/index"
    29  	"github.com/dolthub/dolt/go/libraries/doltcore/sqle/sqlutil"
    30  	"github.com/dolthub/dolt/go/libraries/doltcore/table/editor"
    31  	"github.com/dolthub/dolt/go/store/types"
    32  )
    33  
    34  // WriteSession encapsulates writes made within a SQL session.
    35  // It's responsible for creating and managing the lifecycle of TableWriter's.
    36  type WriteSession interface {
    37  	// GetTableWriter creates a TableWriter and adds it to the WriteSession.
    38  	GetTableWriter(ctx *sql.Context, tableName doltdb.TableName, db string, setter SessionRootSetter) (TableWriter, error)
    39  
    40  	// SetWorkingSet modifies the state of the WriteSession. The WorkingSetRef of |ws| must match the existing Ref.
    41  	SetWorkingSet(ctx *sql.Context, ws *doltdb.WorkingSet) error
    42  
    43  	// GetOptions returns the editor.Options for this session.
    44  	GetOptions() editor.Options
    45  
    46  	// SetOptions sets the editor.Options for this session.
    47  	SetOptions(opts editor.Options)
    48  
    49  	WriteSessionFlusher
    50  }
    51  
    52  // WriteSessionFlusher is responsible for flushing any pending edits to the session
    53  type WriteSessionFlusher interface {
    54  	// Flush flushes the pending writes in the session.
    55  	Flush(ctx *sql.Context) (*doltdb.WorkingSet, error)
    56  	// FlushWithAutoIncrementOverrides flushes the pending writes in the session, overriding the auto increment values
    57  	// for any tables provided in the map
    58  	FlushWithAutoIncrementOverrides(ctx *sql.Context, increment bool, autoIncrements map[string]uint64) (*doltdb.WorkingSet, error)
    59  }
    60  
    61  // nomsWriteSession handles all edit operations on a table that may also update other tables.
    62  // Serves as coordination for SessionedTableEditors.
    63  type nomsWriteSession struct {
    64  	workingSet *doltdb.WorkingSet
    65  	tables     map[string]*sessionedTableEditor
    66  	aiTracker  globalstate.AutoIncrementTracker
    67  	mut        *sync.RWMutex // This mutex is specifically for changes that affect the TES or all STEs
    68  	opts       editor.Options
    69  }
    70  
    71  var _ WriteSession = &nomsWriteSession{}
    72  
    73  // NewWriteSession creates and returns a WriteSession. Inserting a nil root is not an error, as there are
    74  // locations that do not have a root at the time of this call. However, a root must be set through SetWorkingRoot before any
    75  // table editors are returned.
    76  func NewWriteSession(nbf *types.NomsBinFormat, ws *doltdb.WorkingSet, aiTracker globalstate.AutoIncrementTracker, opts editor.Options) WriteSession {
    77  	if types.IsFormat_DOLT(nbf) {
    78  		return &prollyWriteSession{
    79  			workingSet: ws,
    80  			tables:     make(map[doltdb.TableName]*prollyTableWriter),
    81  			aiTracker:  aiTracker,
    82  			mut:        &sync.RWMutex{},
    83  		}
    84  	}
    85  
    86  	return &nomsWriteSession{
    87  		workingSet: ws,
    88  		tables:     make(map[string]*sessionedTableEditor),
    89  		aiTracker:  aiTracker,
    90  		mut:        &sync.RWMutex{},
    91  		opts:       opts,
    92  	}
    93  }
    94  
    95  func (s *nomsWriteSession) GetTableWriter(ctx *sql.Context, table doltdb.TableName, db string, setter SessionRootSetter) (TableWriter, error) {
    96  	s.mut.Lock()
    97  	defer s.mut.Unlock()
    98  
    99  	t, ok, err := s.workingSet.WorkingRoot().GetTable(ctx, table)
   100  	if err != nil {
   101  		return nil, err
   102  	}
   103  	if !ok {
   104  		return nil, doltdb.ErrTableNotFound
   105  	}
   106  	vrw := t.ValueReadWriter()
   107  
   108  	sch, err := t.GetSchema(ctx)
   109  	if err != nil {
   110  		return nil, err
   111  	}
   112  	sqlSch, err := sqlutil.FromDoltSchema("", table.Name, sch)
   113  	if err != nil {
   114  		return nil, err
   115  	}
   116  
   117  	te, err := s.getTableEditor(ctx, table.Name, sch)
   118  	if err != nil {
   119  		return nil, err
   120  	}
   121  
   122  	conv := index.NewKVToSqlRowConverterForCols(t.Format(), sch, nil)
   123  
   124  	return &nomsTableWriter{
   125  		tableName:   table.Name,
   126  		dbName:      db,
   127  		sch:         sch,
   128  		sqlSch:      sqlSch.Schema,
   129  		vrw:         vrw,
   130  		kvToSQLRow:  conv,
   131  		tableEditor: te,
   132  		flusher:     s,
   133  		autoInc:     s.aiTracker,
   134  		setter:      setter,
   135  	}, nil
   136  }
   137  
   138  // Flush returns an updated root with all of the changed tables.
   139  func (s *nomsWriteSession) Flush(ctx *sql.Context) (*doltdb.WorkingSet, error) {
   140  	s.mut.Lock()
   141  	defer s.mut.Unlock()
   142  	return s.flush(ctx)
   143  }
   144  
   145  func (s *nomsWriteSession) FlushWithAutoIncrementOverrides(ctx *sql.Context, increment bool, autoIncrements map[string]uint64) (*doltdb.WorkingSet, error) {
   146  	// auto increment overrides not implemented
   147  	return s.Flush(ctx)
   148  }
   149  
   150  // SetWorkingSet implements WriteSession.
   151  func (s *nomsWriteSession) SetWorkingSet(ctx *sql.Context, ws *doltdb.WorkingSet) error {
   152  	s.mut.Lock()
   153  	defer s.mut.Unlock()
   154  	return s.setWorkingSet(ctx, ws)
   155  }
   156  
   157  func (s *nomsWriteSession) GetOptions() editor.Options {
   158  	return s.opts
   159  }
   160  
   161  func (s *nomsWriteSession) SetOptions(opts editor.Options) {
   162  	s.opts = opts
   163  }
   164  
   165  // flush is the inner implementation for Flush that does not acquire any locks
   166  func (s *nomsWriteSession) flush(ctx *sql.Context) (*doltdb.WorkingSet, error) {
   167  	newRoot := s.workingSet.WorkingRoot()
   168  	mu := &sync.Mutex{}
   169  	rootUpdate := func(name string, table *doltdb.Table) (err error) {
   170  		mu.Lock()
   171  		defer mu.Unlock()
   172  		if newRoot != nil {
   173  			newRoot, err = newRoot.PutTable(ctx, doltdb.TableName{Name: name}, table)
   174  		}
   175  		return err
   176  	}
   177  
   178  	eg, egCtx := errgroup.WithContext(ctx)
   179  	ctx = ctx.WithContext(egCtx)
   180  
   181  	for tblName, tblEditor := range s.tables {
   182  		if !tblEditor.HasEdits() {
   183  			continue
   184  		}
   185  
   186  		// copy variables
   187  		name, ed := tblName, tblEditor
   188  
   189  		eg.Go(func() error {
   190  			tbl, err := ed.tableEditor.Table(ctx)
   191  			if err != nil {
   192  				return err
   193  			}
   194  
   195  			// Update the auto increment value for the table if a tracker was provided
   196  			// TODO: the table probably needs an autoincrement tracker no matter what
   197  			if schema.HasAutoIncrement(ed.Schema()) {
   198  				v := s.aiTracker.Current(name)
   199  				tbl, err = tbl.SetAutoIncrementValue(ctx, v)
   200  				if err != nil {
   201  					return err
   202  				}
   203  			}
   204  
   205  			return rootUpdate(name, tbl)
   206  		})
   207  	}
   208  	if err := eg.Wait(); err != nil {
   209  		return nil, err
   210  	}
   211  	s.workingSet = s.workingSet.WithWorkingRoot(newRoot)
   212  
   213  	return s.workingSet, nil
   214  }
   215  
   216  // getTableEditor is the inner implementation for GetTableEditor, allowing recursive calls
   217  func (s *nomsWriteSession) getTableEditor(ctx context.Context, tableName string, tableSch schema.Schema) (*sessionedTableEditor, error) {
   218  	if s.workingSet == nil {
   219  		return nil, fmt.Errorf("must call SetWorkingSet before a table editor will be returned")
   220  	}
   221  
   222  	var t *doltdb.Table
   223  	var err error
   224  	localTableEditor, ok := s.tables[tableName]
   225  	if ok {
   226  		if tableSch == nil {
   227  			return localTableEditor, nil
   228  		} else if schema.SchemasAreEqual(tableSch, localTableEditor.tableEditor.Schema()) {
   229  			return localTableEditor, nil
   230  		}
   231  	} else {
   232  		localTableEditor = &sessionedTableEditor{
   233  			tableEditSession: s,
   234  			tableEditor:      nil,
   235  		}
   236  		s.tables[tableName] = localTableEditor
   237  	}
   238  
   239  	root := s.workingSet.WorkingRoot()
   240  
   241  	t, ok, err = root.GetTable(ctx, doltdb.TableName{Name: tableName})
   242  	if err != nil {
   243  		return nil, err
   244  	}
   245  	if !ok {
   246  		return nil, fmt.Errorf("unable to create table editor as `%s` is missing", tableName)
   247  	}
   248  	if tableSch == nil {
   249  		tableSch, err = t.GetSchema(ctx)
   250  		if err != nil {
   251  			return nil, err
   252  		}
   253  	}
   254  
   255  	tableEditor, err := editor.NewTableEditor(ctx, t, tableSch, tableName, s.opts)
   256  	if err != nil {
   257  		return nil, err
   258  	}
   259  
   260  	localTableEditor.tableEditor = tableEditor
   261  
   262  	return localTableEditor, nil
   263  }
   264  
   265  // setRoot is the inner implementation for SetWorkingRoot that does not acquire any locks
   266  func (s *nomsWriteSession) setWorkingSet(ctx context.Context, ws *doltdb.WorkingSet) error {
   267  	if ws == nil {
   268  		return fmt.Errorf("cannot set a nomsWriteSession's working set to nil once it has been created")
   269  	}
   270  	if s.workingSet != nil && s.workingSet.Ref() != ws.Ref() {
   271  		return fmt.Errorf("cannot change working set ref using SetWorkingSet")
   272  	}
   273  	s.workingSet = ws
   274  
   275  	root := ws.WorkingRoot()
   276  	for tableName, localTableEditor := range s.tables {
   277  		t, ok, err := root.GetTable(ctx, doltdb.TableName{Name: tableName})
   278  		if err != nil {
   279  			return err
   280  		}
   281  		if !ok { // table was removed in newer root
   282  			if err := localTableEditor.tableEditor.Close(ctx); err != nil {
   283  				return err
   284  			}
   285  			delete(s.tables, tableName)
   286  			continue
   287  		}
   288  		tSch, err := t.GetSchema(ctx)
   289  		if err != nil {
   290  			return err
   291  		}
   292  
   293  		newTableEditor, err := editor.NewTableEditor(ctx, t, tSch, tableName, s.opts)
   294  		if err != nil {
   295  			return err
   296  		}
   297  		if err := localTableEditor.tableEditor.Close(ctx); err != nil {
   298  			return err
   299  		}
   300  		localTableEditor.tableEditor = newTableEditor
   301  	}
   302  	return nil
   303  }