mirror of
https://github.com/featurebasedb/featurebase.git
synced 2026-08-28 02:44:59 +00:00
Switch Serverless from using BoltDB to Postgres as metadata store. Previously, the controller stored all metadata to BoltDB. This implements SQLDB (currently Postgres flavored) as the backing store for metadata. This will allow us to have multiple instances of the controller running for HA, and to easily inspect and repair the contents of the metadata store. Unfortunately, it was not straightforward to keep the BoltDB implementation working alongside the SQL one, so it will be removed in a later patch. Once that's done, the SQL implementation should allow for a number of simplifications of the schemar and balancer interfaces. Database migration is built directly into the application by embedding the migration files and logic from the `soda` command line tool. When connecting to the RDBMS, the app will always attempt to create the necessary database and apply any outstanding migrations. Integration tests truncate all tables upon start, but *not* at the end, so the state of the database can be inspected after integration tests. Had to refactor some of the controller's background tasks to make sure they get properly shut down on controller exit.
339 lines
9.8 KiB
Go
339 lines
9.8 KiB
Go
package schemar_test
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"github.com/featurebasedb/featurebase/v3/dax"
|
|
cschemar "github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
|
|
"github.com/featurebasedb/featurebase/v3/dax/controller/sqldb"
|
|
"github.com/featurebasedb/featurebase/v3/errors"
|
|
"github.com/featurebasedb/featurebase/v3/logger"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
orgID = "orgid"
|
|
orgID2 = "orgid2"
|
|
dbID = "blah"
|
|
dbID2 = "blah2"
|
|
dbID3 = "blah3"
|
|
dbID4 = "blah4"
|
|
dbName = "haha"
|
|
dbName2 = "haha2"
|
|
tblName = "tbl"
|
|
)
|
|
|
|
var (
|
|
qdbid = dax.QualifiedDatabaseID{OrganizationID: orgID, DatabaseID: dbID}
|
|
)
|
|
|
|
// TODO these tests can be generalized to share setup (getting
|
|
// transactor/tx), and then run the same tests against multiple
|
|
// underlying implementations
|
|
|
|
func TestSQLSchemar(t *testing.T) {
|
|
conf := sqldb.GetTestConfigRandomDB("sql_schemar")
|
|
trans, err := sqldb.Connect(conf, logger.StderrLogger)
|
|
require.NoError(t, err, "connecting")
|
|
defer sqldb.DropDatabase(trans)
|
|
|
|
tx, err := trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err, "getting transaction")
|
|
|
|
schemar := sqldb.NewSchemar(nil)
|
|
|
|
err = schemar.CreateDatabase(tx,
|
|
&dax.QualifiedDatabase{
|
|
OrganizationID: orgID,
|
|
Database: dax.Database{ID: dbID, Name: dbName}})
|
|
require.NoError(t, err)
|
|
|
|
// create 2nd db in same org
|
|
err = schemar.CreateDatabase(tx,
|
|
&dax.QualifiedDatabase{
|
|
OrganizationID: orgID,
|
|
Database: dax.Database{ID: dbID2, Name: dbName2}})
|
|
require.NoError(t, err)
|
|
|
|
// create 3rd db in new org
|
|
schemar.CreateDatabase(tx,
|
|
&dax.QualifiedDatabase{
|
|
OrganizationID: orgID2,
|
|
Database: dax.Database{ID: dbID3, Name: dbName2}})
|
|
require.NoError(t, err)
|
|
|
|
err = schemar.CreateDatabase(tx,
|
|
&dax.QualifiedDatabase{OrganizationID: orgID,
|
|
Database: dax.Database{
|
|
ID: dbID,
|
|
Name: dbName},
|
|
})
|
|
if !errors.Is(err, dax.ErrDatabaseIDExists) {
|
|
t.Fatalf("got unexpected error creating DB that already exists: %v", err)
|
|
}
|
|
|
|
// make sure querying with empty org ID brings back all databases
|
|
dbs, err := schemar.Databases(tx, "")
|
|
require.NoError(t, err)
|
|
require.Equal(t, 3, len(dbs))
|
|
|
|
db, err := schemar.DatabaseByName(tx, orgID, dbName)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, dbID, db.ID)
|
|
|
|
err = schemar.SetDatabaseOption(tx, qdbid, dax.DatabaseOptionWorkersMax, "4")
|
|
require.NoError(t, err)
|
|
|
|
err = schemar.SetDatabaseOption(tx, qdbid, dax.DatabaseOptionWorkersMin, "2")
|
|
require.NoError(t, err)
|
|
|
|
db, err = schemar.DatabaseByID(tx, qdbid)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, dbName, db.Name)
|
|
require.EqualValues(t, 4, db.Options.WorkersMax)
|
|
require.EqualValues(t, 2, db.Options.WorkersMin)
|
|
|
|
dbs, err = schemar.Databases(tx, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 2, len(dbs))
|
|
require.EqualValues(t, orgID, dbs[0].OrganizationID)
|
|
require.EqualValues(t, orgID, dbs[1].OrganizationID)
|
|
|
|
dbs, err = schemar.Databases(tx, orgID, dbID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(dbs))
|
|
require.EqualValues(t, orgID, dbs[0].OrganizationID)
|
|
require.EqualValues(t, dbID, dbs[0].Database.ID)
|
|
|
|
// test create table
|
|
qtbl := &dax.QualifiedTable{
|
|
QualifiedDatabaseID: qdbid,
|
|
Table: dax.Table{
|
|
Name: tblName,
|
|
Fields: []*dax.Field{{
|
|
Name: "_id",
|
|
Type: "string",
|
|
Options: dax.FieldOptions{},
|
|
}},
|
|
PartitionN: 4,
|
|
Description: "desc",
|
|
Owner: "own",
|
|
UpdatedBy: "me",
|
|
},
|
|
}
|
|
|
|
_, err = qtbl.CreateID()
|
|
require.NoError(t, err)
|
|
err = schemar.CreateTable(tx, qtbl)
|
|
require.NoError(t, err)
|
|
|
|
// test create field
|
|
err = schemar.CreateField(tx, qtbl.QualifiedID(), &dax.Field{Name: "age", Type: "int"})
|
|
require.NoError(t, err)
|
|
|
|
qtbl, err = schemar.Table(tx, qtbl.QualifiedID())
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, 2, len(qtbl.Fields))
|
|
|
|
var ageField *dax.Field
|
|
|
|
for _, f := range qtbl.Fields {
|
|
if f.Name == "age" {
|
|
ageField = f
|
|
}
|
|
}
|
|
|
|
require.NotNil(t, ageField)
|
|
|
|
// drop field
|
|
err = schemar.DropField(tx, qtbl.QualifiedID(), "age")
|
|
require.NoError(t, err)
|
|
|
|
// ensure field was dropped
|
|
qtbl, err = schemar.Table(tx, qtbl.QualifiedID())
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(qtbl.Fields))
|
|
|
|
if qtbl.Fields[0].Name != "_id" {
|
|
t.Fatalf("unexpected field: %+v", qtbl.Fields[0])
|
|
}
|
|
|
|
tables, err := schemar.Tables(tx, qdbid)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(tables))
|
|
|
|
// TODO add test for Tables passing table ids
|
|
tables, err = schemar.Tables(tx, qdbid, tables[0].ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(tables))
|
|
|
|
_, err = schemar.TableID(tx, qdbid, tblName)
|
|
require.NoError(t, err)
|
|
|
|
err = schemar.DropTable(tx, qtbl.QualifiedID())
|
|
require.NoError(t, err)
|
|
|
|
// make sure Table was deleted
|
|
_, err = schemar.Table(tx, qtbl.QualifiedID())
|
|
require.NotNil(t, err)
|
|
|
|
err = schemar.DropDatabase(tx, qdbid)
|
|
require.NoError(t, err)
|
|
|
|
// make sure DB was deleted
|
|
dbs, err = schemar.Databases(tx, orgID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 1, len(dbs))
|
|
require.EqualValues(t, dbID2, dbs[0].Database.ID)
|
|
|
|
// rollback so we have clean state to test failure cases
|
|
err = tx.Rollback()
|
|
if err != nil {
|
|
require.NoError(t, err, "rolling back to test failure cases")
|
|
}
|
|
|
|
qtbl = &dax.QualifiedTable{
|
|
QualifiedDatabaseID: qdbid,
|
|
Table: dax.Table{
|
|
Name: tblName,
|
|
Fields: []*dax.Field{
|
|
{
|
|
Name: "_id",
|
|
Type: "string",
|
|
Options: dax.FieldOptions{},
|
|
},
|
|
{
|
|
Name: "age",
|
|
Type: "int",
|
|
Options: dax.FieldOptions{},
|
|
},
|
|
},
|
|
PartitionN: 4,
|
|
Description: "desc",
|
|
Owner: "own",
|
|
UpdatedBy: "me",
|
|
},
|
|
}
|
|
qtbl.ID = ""
|
|
_, err = qtbl.CreateID()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Create Table no DB fails", func(t *testing.T) {
|
|
tx2, err := trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx2.Rollback()
|
|
err = schemar.CreateTable(tx2, qtbl)
|
|
require.NotNil(t, err)
|
|
})
|
|
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err, "beginning transaction")
|
|
|
|
err = schemar.CreateDatabase(tx,
|
|
&dax.QualifiedDatabase{
|
|
OrganizationID: orgID,
|
|
Database: dax.Database{ID: dbID, Name: dbName}})
|
|
require.NoError(t, err)
|
|
|
|
err = schemar.CreateTable(tx, qtbl)
|
|
require.NoError(t, err)
|
|
|
|
err = tx.Commit()
|
|
require.NoError(t, err)
|
|
|
|
t.Run("Drop non-existent field fails with correct error", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.DropField(tx, qtbl.QualifiedID(), "unknownField")
|
|
requireCode(t, err, dax.ErrFieldDoesNotExist)
|
|
})
|
|
|
|
t.Run("Drop field from non-existent table", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.DropField(tx, dax.QualifiedTableID{QualifiedDatabaseID: qdbid, ID: "blah", Name: "blah"}, "age")
|
|
requireCode(t, err, dax.ErrFieldDoesNotExist)
|
|
})
|
|
|
|
t.Run("Test Lookup non-existent table fails with correct error (by name)", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
_, err = schemar.Table(tx, dax.QualifiedTableID{QualifiedDatabaseID: qdbid, Name: "humbug"})
|
|
requireCode(t, err, dax.ErrTableNameDoesNotExist)
|
|
})
|
|
|
|
t.Run("Test Lookup non-existent table fails with correct error (by name)", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
_, err = schemar.Table(tx, dax.QualifiedTableID{QualifiedDatabaseID: qdbid, ID: "bumhug", Name: "humbug"})
|
|
requireCode(t, err, dax.ErrTableIDDoesNotExist)
|
|
})
|
|
|
|
t.Run("Test Lookup non-existent tableID fails with correct error (by name)", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
_, err = schemar.TableID(tx, qdbid, "humbug")
|
|
requireCode(t, err, dax.ErrTableNameDoesNotExist)
|
|
})
|
|
|
|
t.Run("Test Create existing field fails", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.CreateField(tx, qtbl.QualifiedID(), &dax.Field{Name: "age", Type: "int", Options: dax.FieldOptions{}})
|
|
requireCode(t, err, dax.ErrFieldExists)
|
|
})
|
|
|
|
t.Run("Test Create field empty name fails", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.CreateField(tx, qtbl.QualifiedID(), &dax.Field{Name: "", Type: "int", Options: dax.FieldOptions{}})
|
|
requireCode(t, err, cschemar.ErrCodeFieldNameInvalid)
|
|
})
|
|
|
|
t.Run("Test create table that already exists", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.CreateTable(tx, qtbl)
|
|
requireCode(t, err, dax.ErrTableIDExists)
|
|
})
|
|
|
|
t.Run("Find database by name that doesn't exist", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
_, err = schemar.DatabaseByName(tx, orgID, "blooooooo")
|
|
requireCode(t, err, dax.ErrDatabaseNameDoesNotExist)
|
|
})
|
|
|
|
t.Run("Find database by ID that doesn't exist", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
_, err = schemar.DatabaseByID(tx, dax.QualifiedDatabaseID{OrganizationID: orgID, DatabaseID: "zeeeeeeeeeeeeep"})
|
|
requireCode(t, err, dax.ErrDatabaseIDDoesNotExist)
|
|
})
|
|
|
|
t.Run("Drop non-existent database", func(t *testing.T) {
|
|
tx, err = trans.BeginTx(context.Background(), true)
|
|
require.NoError(t, err)
|
|
defer tx.Rollback()
|
|
err = schemar.DropDatabase(tx, dax.QualifiedDatabaseID{OrganizationID: orgID, DatabaseID: "yoooo"})
|
|
requireCode(t, err, dax.ErrDatabaseIDDoesNotExist)
|
|
})
|
|
}
|
|
|
|
func requireCode(t *testing.T, err error, code errors.Code) {
|
|
if !errors.Is(err, code) {
|
|
t.Fatalf("Error '%v' does not have code %s.", err, code)
|
|
}
|
|
}
|