mirror of
https://github.com/seaweedfs/seaweedfs.git
synced 2026-09-11 00:50:43 +02:00
* filer/postgres: create default filemeta table on startup The postgres filer store hardcoded CreateTableSqlTemplate to empty and never created the filemeta table, unlike postgres2/mysql2/sqlite which all create it during Initialize. Users had to create the table manually or the filer would crash loop with "relation filemeta does not exist". Read the createTable config option (same as postgres2), default to DefaultCreateTableQuery when unset, and execute CREATE TABLE IF NOT EXISTS on the default table after the connection pool is established. SupportBucketTable stays false so per-bucket table creation remains a no-op; only the shared filemeta table is created, via a direct ExecContext since AbstractSqlStore.CreateTable short-circuits without bucket support. * filer/postgres: accept boolean createTable = true/false viper reads a TOML boolean as the string "true"/"false" via GetString, so createTable = true was being used as a SQL template and failed. Add ResolveCreateTableQuery to normalize the value: true and empty select the default template, false disables table creation, anything else is a custom template. Both postgres and postgres2 now use it, and both skip the CREATE TABLE call when the resolved template is empty. * scaffold: document createTable option for postgres filer store Replace the commented-out CREATE TABLE SQL in the [postgres] scaffold with a createTable config hint, matching the [postgres2] section. Users no longer need to manually create the filemeta table before starting the filer. * filer/postgres: make createTable opt-in for postgres, keep postgres2 default The previous commit defaulted postgres to create the filemeta table even when createTable was unset, which could break existing deployments whose DB user lacks CREATE TABLE privileges. ResolveCreateTableQuery now returns empty for an unset value so postgres only creates the table when createTable is explicitly true or a custom template — preserving the prior no-DDL behaviour for existing configurations. postgres2 keeps its existing always-create default: it defaults an empty resolved value to DefaultCreateTableQuery, and only skips when createTable is explicitly false. * filer/postgres2: simplify createTable handling, document all modes Drop the false opt-out from postgres2 — it only skipped the default table while per-bucket CreateTable still ran, leaving restricted DB roles broken on bucket access. postgres2 now accepts true the same way (defaulting to DefaultCreateTableQuery) and keeps its existing always-create behaviour for every other value, matching the original semantics. The scaffold comment now documents true/false/custom for the postgres section so users know false (or unset) is the backward-compatible default. * filer/postgres2: normalize false via ResolveCreateTableQuery postgres2 only handled "" and "true", leaving createTable = false as the literal string "false" which CreateTable then executed as invalid SQL. Route it through ResolveCreateTableQuery (which maps false to empty) and default the empty result to DefaultCreateTableQuery, so false is treated the same as unset for the bucket-aware store. * filer/postgres2: honor createTable = false for default table postgres2 treated false the same as unset and always created the default filemeta table, failing startup for restricted DB roles that explicitly opted out. Track the original false value before ResolveCreateTableQuery collapses it to empty, and skip the default CreateTable call when set. Per-bucket table creation is unaffected — it is a runtime requirement of the bucket-aware store. Users who need to suppress all DDL should use the postgres (non-bucket) store with createTable unset. * filer/postgres2: disable bucket tables when createTable = false Setting SupportBucketTable = false when createTable is explicitly false makes AbstractSqlStore.CreateTable a no-op (it already returns nil when SupportBucketTable is false), so neither the default filemeta table nor per-bucket tables are created. The template stays empty and no DDL runs, honouring the opt-out for restricted DB roles. All data routes to the pre-provisioned filemeta table, matching the postgres (non-bucket) store. * filer: suppress DDL without disabling bucket routing Setting SupportBucketTable = false when createTable = false also disabled per-bucket routing, hiding objects in pre-provisioned per-bucket tables. Keep SupportBucketTable true and instead skip the CREATE TABLE execution when the resolved template is empty. GetSqlCreateTable now returns empty for both postgres and mysql SQL generators when CreateTableSqlTemplate is empty, and AbstractSqlStore.CreateTable skips the ExecContext call when the SQL is empty. This preserves bucket routing while suppressing all DDL for users who explicitly set createTable = false and pre-provision their tables. * filer: add SkipDDL to suppress CREATE and DROP without disabling routing createTable = false with SupportBucketTable = true preserved bucket routing but deleteTable still executed DROP TABLE on bucket deletion, dropping externally managed tables. CanDropWholeBucket also returned true, so the S3 layer tried whole-table drops instead of row-by-row deletes. Add a SkipDDL flag to AbstractSqlStore, independent of SupportBucketTable. CreateTable and deleteTable both skip when SkipDDL is set, and CanDropWholeBucket returns false so bucket deletion falls back to row-by-row metadata deletes. postgres2 sets SkipDDL when createTable is explicitly false — bucket routing is preserved, no DDL runs. * filer: fall back to row-by-row delete when CanDropWholeBucket is false DeleteFolderChildren took the whole-table drop path whenever the path was a bucket root, even when SkipDDL made deleteTable a no-op. The no-op returned nil, the caller returned early, and rows inserted after the recursive enumeration survived the bucket deletion. Gate the whole-table drop on CanDropWholeBucket so the row-by-row DeleteFolderChildren SQL runs when SkipDDL is set, removing all metadata without issuing DROP TABLE.
480 lines
13 KiB
Go
480 lines
13 KiB
Go
package abstract_sql
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/seaweedfs/seaweedfs/weed/filer"
|
|
"github.com/seaweedfs/seaweedfs/weed/glog"
|
|
"github.com/seaweedfs/seaweedfs/weed/pb/filer_pb"
|
|
"github.com/seaweedfs/seaweedfs/weed/s3api/s3bucket"
|
|
"github.com/seaweedfs/seaweedfs/weed/util"
|
|
)
|
|
|
|
type SqlGenerator interface {
|
|
GetSqlInsert(tableName string) string
|
|
GetSqlUpdate(tableName string) string
|
|
GetSqlFind(tableName string) string
|
|
GetSqlDelete(tableName string) string
|
|
GetSqlDeleteFolderChildren(tableName string) string
|
|
GetSqlListExclusive(tableName string) string
|
|
GetSqlListInclusive(tableName string) string
|
|
GetSqlCreateTable(tableName string) string
|
|
GetSqlDropTable(tableName string) string
|
|
}
|
|
|
|
type AbstractSqlStore struct {
|
|
SqlGenerator
|
|
DB *sql.DB
|
|
KvDB *sql.DB
|
|
SupportBucketTable bool
|
|
SkipDDL bool
|
|
dbs map[string]bool
|
|
dbsLock sync.Mutex
|
|
RetryableErrorCallback func(err error) bool
|
|
}
|
|
|
|
var _ filer.BucketAware = (*AbstractSqlStore)(nil)
|
|
|
|
func (store *AbstractSqlStore) CanDropWholeBucket() bool {
|
|
return store.SupportBucketTable && !store.SkipDDL
|
|
}
|
|
func (store *AbstractSqlStore) OnBucketCreation(bucket string) {
|
|
store.dbsLock.Lock()
|
|
defer store.dbsLock.Unlock()
|
|
|
|
store.CreateTable(context.Background(), bucket)
|
|
|
|
if store.dbs == nil {
|
|
return
|
|
}
|
|
store.dbs[bucket] = true
|
|
}
|
|
func (store *AbstractSqlStore) OnBucketDeletion(bucket string) {
|
|
store.dbsLock.Lock()
|
|
defer store.dbsLock.Unlock()
|
|
|
|
store.deleteTable(context.Background(), bucket)
|
|
|
|
if store.dbs == nil {
|
|
return
|
|
}
|
|
delete(store.dbs, bucket)
|
|
}
|
|
|
|
const (
|
|
DEFAULT_TABLE = "filemeta"
|
|
)
|
|
|
|
type TxOrDB interface {
|
|
ExecContext(ctx context.Context, query string, args ...interface{}) (sql.Result, error)
|
|
QueryRowContext(ctx context.Context, query string, args ...interface{}) *sql.Row
|
|
QueryContext(ctx context.Context, query string, args ...interface{}) (*sql.Rows, error)
|
|
}
|
|
|
|
func (store *AbstractSqlStore) BeginTransaction(ctx context.Context) (context.Context, error) {
|
|
tx, err := store.DB.BeginTx(ctx, &sql.TxOptions{
|
|
Isolation: sql.LevelReadCommitted,
|
|
ReadOnly: false,
|
|
})
|
|
if err != nil {
|
|
return ctx, err
|
|
}
|
|
|
|
return context.WithValue(ctx, "tx", tx), nil
|
|
}
|
|
func (store *AbstractSqlStore) CommitTransaction(ctx context.Context) error {
|
|
if tx, ok := ctx.Value("tx").(*sql.Tx); ok {
|
|
return tx.Commit()
|
|
}
|
|
return nil
|
|
}
|
|
func (store *AbstractSqlStore) RollbackTransaction(ctx context.Context) error {
|
|
if tx, ok := ctx.Value("tx").(*sql.Tx); ok {
|
|
return tx.Rollback()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// A listing holds its connection for the whole row iteration while its callback
|
|
// reads a hard link through KvGet, so both cannot come out of one bounded pool:
|
|
// the listings fill it and then wait for a connection none of them will release.
|
|
// Give the key-value reads their own slice of connection_max_open. A cap of 1 is
|
|
// the exception -- it has to become 2, or a single listing cannot finish.
|
|
func splitPoolForKv(maxOpen int) (mainOpen, kvOpen int) {
|
|
if maxOpen <= 0 {
|
|
return maxOpen, 0
|
|
}
|
|
kvOpen = min(max(maxOpen/4, 1), maxKvPoolSize)
|
|
return max(maxOpen-kvOpen, 1), kvOpen
|
|
}
|
|
|
|
const maxKvPoolSize = 8
|
|
|
|
// UseConnectionPools sizes the store's pool and, when it is bounded, opens the
|
|
// separate pool the key-value reads run on.
|
|
func (store *AbstractSqlStore) UseConnectionPools(db *sql.DB, openKv func() (*sql.DB, error), maxIdle, maxOpen, maxLifetimeSeconds int) error {
|
|
|
|
lifetime := time.Duration(maxLifetimeSeconds) * time.Second
|
|
mainOpen, kvOpen := splitPoolForKv(maxOpen)
|
|
|
|
db.SetMaxIdleConns(maxIdle)
|
|
db.SetMaxOpenConns(mainOpen)
|
|
db.SetConnMaxLifetime(lifetime)
|
|
store.DB = db
|
|
|
|
if kvOpen == 0 {
|
|
return nil
|
|
}
|
|
|
|
kvDB, err := openKv()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
kvDB.SetMaxIdleConns(kvOpen)
|
|
kvDB.SetMaxOpenConns(kvOpen)
|
|
kvDB.SetConnMaxLifetime(lifetime)
|
|
store.KvDB = kvDB
|
|
|
|
return nil
|
|
}
|
|
|
|
func (store *AbstractSqlStore) getTxOrDB(ctx context.Context, fullpath util.FullPath, isForChildren bool) (txOrDB TxOrDB, bucket string, shortPath util.FullPath, err error) {
|
|
|
|
shortPath = fullpath
|
|
bucket = DEFAULT_TABLE
|
|
|
|
if tx, ok := ctx.Value("tx").(*sql.Tx); ok {
|
|
txOrDB = tx
|
|
} else {
|
|
txOrDB = store.DB
|
|
}
|
|
|
|
if !store.SupportBucketTable {
|
|
return
|
|
}
|
|
|
|
if !strings.HasPrefix(string(fullpath), "/buckets/") {
|
|
return
|
|
}
|
|
|
|
// detect bucket
|
|
bucketAndObjectKey := string(fullpath)[len("/buckets/"):]
|
|
t := strings.Index(bucketAndObjectKey, "/")
|
|
if t < 0 && !isForChildren {
|
|
return
|
|
}
|
|
bucket = bucketAndObjectKey
|
|
shortPath = "/"
|
|
if t > 0 {
|
|
bucket = bucketAndObjectKey[:t]
|
|
shortPath = util.FullPath(bucketAndObjectKey[t:])
|
|
}
|
|
|
|
// Dot-prefixed entries directly under /buckets (e.g. .system) are internal
|
|
// folders, not S3 buckets; keep them in the default table by full path.
|
|
if strings.HasPrefix(bucket, ".") {
|
|
bucket = DEFAULT_TABLE
|
|
shortPath = fullpath
|
|
return
|
|
}
|
|
|
|
if isValidBucket(bucket) {
|
|
store.dbsLock.Lock()
|
|
defer store.dbsLock.Unlock()
|
|
|
|
if store.dbs == nil {
|
|
store.dbs = make(map[string]bool)
|
|
}
|
|
|
|
if _, found := store.dbs[bucket]; !found {
|
|
if err = store.CreateTable(ctx, bucket); err == nil {
|
|
store.dbs[bucket] = true
|
|
}
|
|
}
|
|
|
|
} else {
|
|
err = fmt.Errorf("invalid bucket name %s", bucket)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func (store *AbstractSqlStore) InsertEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
|
|
// define the work to be done
|
|
var doInsert func() error
|
|
doInsert = func() error {
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, entry.FullPath, false)
|
|
if err != nil {
|
|
return fmt.Errorf("findDB %s : %w", entry.FullPath, err)
|
|
}
|
|
|
|
dir, name := shortPath.DirAndName()
|
|
meta, err := entry.EncodeAttributesAndChunks()
|
|
if err != nil {
|
|
return fmt.Errorf("encode %s: %w", entry.FullPath, err)
|
|
}
|
|
|
|
if len(entry.GetChunks()) > filer.CountEntryChunksForGzip {
|
|
meta = util.MaybeGzipData(meta)
|
|
}
|
|
sqlInsert := "insert"
|
|
res, err := db.ExecContext(ctx, store.GetSqlInsert(bucket), util.HashStringToLong(dir), name, dir, meta)
|
|
if err != nil && strings.Contains(strings.ToLower(err.Error()), "duplicate entry") {
|
|
// now the insert failed possibly due to duplication constraints
|
|
sqlInsert = "falls back to update"
|
|
glog.V(1).InfofCtx(ctx, "insert %s %s: %v", entry.FullPath, sqlInsert, err)
|
|
res, err = db.ExecContext(ctx, store.GetSqlUpdate(bucket), meta, util.HashStringToLong(dir), name, dir)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s: %w", sqlInsert, entry.FullPath, err)
|
|
}
|
|
|
|
_, err = res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("%s %s but no rows affected: %w", sqlInsert, entry.FullPath, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
if store.RetryableErrorCallback != nil {
|
|
if ctx.Value("tx") != nil {
|
|
return doInsert()
|
|
}
|
|
return util.RetryUntil("InsertEntry", doInsert, store.RetryableErrorCallback)
|
|
}
|
|
return doInsert()
|
|
}
|
|
|
|
func (store *AbstractSqlStore) UpdateEntry(ctx context.Context, entry *filer.Entry) (err error) {
|
|
|
|
var doUpdate func() error
|
|
doUpdate = func() error {
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, entry.FullPath, false)
|
|
if err != nil {
|
|
return fmt.Errorf("findDB %s : %w", entry.FullPath, err)
|
|
}
|
|
|
|
dir, name := shortPath.DirAndName()
|
|
meta, err := entry.EncodeAttributesAndChunks()
|
|
if err != nil {
|
|
return fmt.Errorf("encode %s: %w", entry.FullPath, err)
|
|
}
|
|
|
|
res, err := db.ExecContext(ctx, store.GetSqlUpdate(bucket), meta, util.HashStringToLong(dir), name, dir)
|
|
if err != nil {
|
|
return fmt.Errorf("update %s: %w", entry.FullPath, err)
|
|
}
|
|
|
|
_, err = res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("update %s but no rows affected: %w", entry.FullPath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if store.RetryableErrorCallback != nil {
|
|
if ctx.Value("tx") != nil {
|
|
return doUpdate()
|
|
}
|
|
return util.RetryUntil("UpdateEntry", doUpdate, store.RetryableErrorCallback)
|
|
}
|
|
return doUpdate()
|
|
}
|
|
|
|
func (store *AbstractSqlStore) FindEntry(ctx context.Context, fullpath util.FullPath) (*filer.Entry, error) {
|
|
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, fullpath, false)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("findDB %s : %v", fullpath, err)
|
|
}
|
|
|
|
dir, name := shortPath.DirAndName()
|
|
row := db.QueryRowContext(ctx, store.GetSqlFind(bucket), util.HashStringToLong(dir), name, dir)
|
|
|
|
var data []byte
|
|
if err := row.Scan(&data); err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, filer_pb.ErrNotFound
|
|
}
|
|
return nil, fmt.Errorf("find %s: %v", fullpath, err)
|
|
}
|
|
|
|
entry := &filer.Entry{
|
|
FullPath: fullpath,
|
|
}
|
|
if err := entry.DecodeAttributesAndChunks(util.MaybeDecompressData(data)); err != nil {
|
|
return entry, fmt.Errorf("decode %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
return entry, nil
|
|
}
|
|
|
|
func (store *AbstractSqlStore) DeleteEntry(ctx context.Context, fullpath util.FullPath) error {
|
|
|
|
var doDelete func() error
|
|
doDelete = func() error {
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, fullpath, false)
|
|
if err != nil {
|
|
return fmt.Errorf("findDB %s : %w", fullpath, err)
|
|
}
|
|
|
|
dir, name := shortPath.DirAndName()
|
|
|
|
res, err := db.ExecContext(ctx, store.GetSqlDelete(bucket), util.HashStringToLong(dir), name, dir)
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s: %w", fullpath, err)
|
|
}
|
|
|
|
_, err = res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("delete %s but no rows affected: %w", fullpath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if store.RetryableErrorCallback != nil {
|
|
if ctx.Value("tx") != nil {
|
|
return doDelete()
|
|
}
|
|
return util.RetryUntil("DeleteEntry", doDelete, store.RetryableErrorCallback)
|
|
}
|
|
return doDelete()
|
|
}
|
|
|
|
func (store *AbstractSqlStore) DeleteFolderChildren(ctx context.Context, fullpath util.FullPath) error {
|
|
|
|
var doDeleteFolderChildren func() error
|
|
doDeleteFolderChildren = func() error {
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, fullpath, true)
|
|
if err != nil {
|
|
return fmt.Errorf("findDB %s : %w", fullpath, err)
|
|
}
|
|
|
|
if isValidBucket(bucket) && shortPath == "/" && store.CanDropWholeBucket() {
|
|
if err = store.deleteTable(ctx, bucket); err == nil {
|
|
store.dbsLock.Lock()
|
|
delete(store.dbs, bucket)
|
|
store.dbsLock.Unlock()
|
|
return nil
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
glog.V(4).InfofCtx(ctx, "delete %s SQL %s %d", string(shortPath), store.GetSqlDeleteFolderChildren(bucket), util.HashStringToLong(string(shortPath)))
|
|
res, err := db.ExecContext(ctx, store.GetSqlDeleteFolderChildren(bucket), util.HashStringToLong(string(shortPath)), string(shortPath))
|
|
if err != nil {
|
|
return fmt.Errorf("deleteFolderChildren %s: %w", fullpath, err)
|
|
}
|
|
|
|
_, err = res.RowsAffected()
|
|
if err != nil {
|
|
return fmt.Errorf("deleteFolderChildren %s but no rows affected: %w", fullpath, err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
if store.RetryableErrorCallback != nil {
|
|
if ctx.Value("tx") != nil {
|
|
return doDeleteFolderChildren()
|
|
}
|
|
return util.RetryUntil("DeleteFolderChildren", doDeleteFolderChildren, store.RetryableErrorCallback)
|
|
}
|
|
return doDeleteFolderChildren()
|
|
}
|
|
|
|
func (store *AbstractSqlStore) ListDirectoryPrefixedEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, prefix string, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
|
|
db, bucket, shortPath, err := store.getTxOrDB(ctx, dirPath, true)
|
|
if err != nil {
|
|
return lastFileName, fmt.Errorf("findDB %s : %v", dirPath, err)
|
|
}
|
|
|
|
sqlText := store.GetSqlListExclusive(bucket)
|
|
if includeStartFile {
|
|
sqlText = store.GetSqlListInclusive(bucket)
|
|
}
|
|
|
|
rows, err := db.QueryContext(ctx, sqlText, util.HashStringToLong(string(shortPath)), startFileName, string(shortPath), prefix+"%", limit+1)
|
|
if err != nil {
|
|
return lastFileName, fmt.Errorf("list %s : %v", dirPath, err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
for rows.Next() {
|
|
var name string
|
|
var data []byte
|
|
if err = rows.Scan(&name, &data); err != nil {
|
|
glog.V(0).InfofCtx(ctx, "scan %s : %v", dirPath, err)
|
|
return lastFileName, fmt.Errorf("scan %s: %v", dirPath, err)
|
|
}
|
|
lastFileName = name
|
|
|
|
entry := &filer.Entry{
|
|
FullPath: util.NewFullPath(string(dirPath), name),
|
|
}
|
|
if err = entry.DecodeAttributesAndChunks(util.MaybeDecompressData(data)); err != nil {
|
|
glog.V(0).InfofCtx(ctx, "scan decode %s : %v", entry.FullPath, err)
|
|
return lastFileName, fmt.Errorf("scan decode %s : %v", entry.FullPath, err)
|
|
}
|
|
|
|
resEachEntryFunc, resEachEntryFuncErr := eachEntryFunc(entry)
|
|
if resEachEntryFuncErr != nil {
|
|
err = fmt.Errorf("failed to process eachEntryFunc: %w", resEachEntryFuncErr)
|
|
break
|
|
}
|
|
|
|
if !resEachEntryFunc {
|
|
break
|
|
}
|
|
|
|
}
|
|
|
|
return lastFileName, err
|
|
}
|
|
|
|
func (store *AbstractSqlStore) ListDirectoryEntries(ctx context.Context, dirPath util.FullPath, startFileName string, includeStartFile bool, limit int64, eachEntryFunc filer.ListEachEntryFunc) (lastFileName string, err error) {
|
|
return store.ListDirectoryPrefixedEntries(ctx, dirPath, startFileName, includeStartFile, limit, "", eachEntryFunc)
|
|
}
|
|
|
|
func (store *AbstractSqlStore) Shutdown() {
|
|
store.DB.Close()
|
|
if store.KvDB != nil {
|
|
store.KvDB.Close()
|
|
}
|
|
}
|
|
|
|
func isValidBucket(bucket string) bool {
|
|
if s3bucket.VerifyS3BucketName(bucket) != nil {
|
|
return false
|
|
}
|
|
return bucket != DEFAULT_TABLE && bucket != ""
|
|
}
|
|
|
|
func (store *AbstractSqlStore) CreateTable(ctx context.Context, bucket string) error {
|
|
if !store.SupportBucketTable || store.SkipDDL {
|
|
return nil
|
|
}
|
|
sql := store.SqlGenerator.GetSqlCreateTable(bucket)
|
|
if sql == "" {
|
|
return nil
|
|
}
|
|
_, err := store.DB.ExecContext(ctx, sql)
|
|
return err
|
|
}
|
|
|
|
func (store *AbstractSqlStore) deleteTable(ctx context.Context, bucket string) error {
|
|
if !store.SupportBucketTable || store.SkipDDL {
|
|
return nil
|
|
}
|
|
_, err := store.DB.ExecContext(ctx, store.SqlGenerator.GetSqlDropTable(bucket))
|
|
return err
|
|
}
|