You are an expert in writing code in many different languages. Your goal is to perform code completion for the following code, keeping in mind the rest of the code and the file meta data.
    Metadata details:
    filename: workspace.go
    path of file: ./repos/skeema/internal/workspace
    
    the code of the file until where you have to start completion: // Package workspace provides functions for interacting with a temporary MySQL
// schema. It manages creating a schema on a desired location (either an
// existing MySQL instance, or a dynamically-controlled Docker instance),
// running SQL DDL or DML, introspecting the resulting schema, and cleaning
// up the schema when it is no longer needed.
package workspace

import (
	"context"
	"errors"
	"fmt"
	"net/url"
	"sync"
	"time"

	"github.com/VividCortex/mysqlerr"
	"github.com/go-sql-driver/mysql"
	"github.com/jmoiron/sqlx"
	log "github.com/sirupsen/logrus"
	"github.com/skeema/mybase"
	"github.com/skeema/skeema/internal/fs"
	"github.com/skeema/skeema/internal/tengo"
)

// Workspace represents a "scratch space" for DDL operations and schema
// introspection.
type Workspace interface {
	// ConnectionPool returns a *sqlx.DB representing a connection pool for
	// interacting with the workspace. The pool should already be using the
	// correct default database for interacting with the workspace schema.
	ConnectionPool(params string) (*sqlx.DB, error)

	// IntrospectSchema returns a *tengo.Schema representing the current state
	// of the workspace schema.
	IntrospectSchema() (*tengo.Schema, error)

	// Cleanup cleans up the workspace, leaving it in a state where it could be
	// re-used/re-initialized as needed. Repeated calls to Cleanup() may error.
	// The arg may be nil, and/or the implementation may ignore the arg; it is
	// supplied to optionally improve performance where relevant.
	Cleanup(schema *tengo.Schema) error
}

// Type represents a kind of workspace to use.
type Type int

// Constants enumerating different types of workspaces
const (
	TypeTempSchema  Type = iota // A temporary schema on a real pre-supplied Instance
	TypeLocalDocker             // A schema on an ephemeral Docker container on localhost
)

// CleanupAction represents how to clean up a workspac
    
func ExecLogicalSchema(logicalSchema *fs.LogicalSchema, opts Options) (_ *Schema, retErr error) {
	if logicalSchema.CharSet != "" {
		opts.DefaultCharacterSet = logicalSchema.CharSet
	}
	if logicalSchema.Collation != "" {
		opts.DefaultCollation = logicalSchema.Collation
	}
	if opts.NameCaseMode > tengo.NameCaseAsIs {
		if err := logicalSchema.LowerCaseNames(opts.NameCaseMode); err != nil {
			return nil, err
		}
	}

	ws, err := New(opts)
	if err != nil {
		return nil, err
	}

	// ExecLogicalSchema names its error return so that a deferred func can check
	// if an error occurred, but otherwise intentionally does not use named return
	// variables, and instead declares new local vars for all other usage. This is
	// to avoid mistakes with variable shadowing, nil pointer panics, etc which are
	// common when dealing with named returns and deferred anonymous functions.
	wsSchema := &Schema{
		LogicalSchema: logicalSchema,
		Failures:      []*StatementError{},
	}
	defer func() {
		cleanupErr := ws.Cleanup(wsSchema.Schema)
		// We only care about a cleanup error if the original returned error was nil
		if retErr == nil && cleanupErr != nil {
			retErr = cleanupErr
		}
	}()

	params := "foreign_key_checks=0"
	if opts.SkipBinlog {
		params += "&sql_log_bin=0"
	}
	db, err := ws.ConnectionPool(params)
	if err != nil {
		return nil, fmt.Errorf("Cannot connect to workspace: %w", err)
	}

	// Run CREATEs in parallel, bounded by opts.Concurrency
	creates := make(chan *tengo.Statement, opts.Concurrency)
	errs := make(chan error, opts.Concurrency)
	go func() {
		for _, stmt := range logicalSchema.Creates {
			creates <- stmt
		}
		close(creates)
	}()
	for n := 0; n < len(logicalSchema.Creates) && n < opts.Concurrency; n++ {
		go func() {
			for stmt := range creates {
				_, err := db.Exec(stmt.Body())
				if err != nil {
					err = wrapFailure(stmt, err)
				}
				errs <- err
			}
		}()
	}

	// Examine statement errors. If any deadlocks occurred, retry them
	// sequentially, since some deadlocks are expected from concurrent CREATEs in
	// MySQL 8+ if FKs are present. Ditto with metadata lock wait timeouts.
	// Also retry errors from CREATE TABLE...LIKE being run out-of-order (only once
	// though; nested chains of CREATE TABLE...LIKE are unsupported)
	sequentialStatements := []*tengo.Statement{}
	for n := 0; n < len(logicalSchema.Creates); n++ {
		if err := <-errs; err != nil {
			stmterr := err.(*StatementError)
			if tengo.IsDatabaseError(stmterr.Err, mysqlerr.ER_LOCK_DEADLOCK, mysqlerr.ER_LOCK_WAIT_TIMEOUT, mysqlerr.ER_NO_SUCH_TABLE) {
				sequentialStatements = append(sequentialStatements, stmterr.Statement)
			} else {
				wsSchema.Failures = append(wsSchema.Failures, stmterr)
			}
		}
	}
	close(errs)

	// Run ALTERs sequentially, since foreign key manipulations don't play
	// nice with concurrency.
	sequentialStatements = append(sequentialStatements, logicalSchema.Alters...)

	for _, statement := range sequentialStatements {
		if _, err := db.Exec(statement.Body()); err != nil {
			wsSchema.Failures = append(wsSchema.Failures, wrapFailure(statement, err))
		}
	}

	wsSchema.Schema, err = ws.IntrospectSchema()
	return wsSchema, err
}