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.NewTransactor(conf, logger.StderrLogger) require.NoError(t, err, "connecting") defer sqldb.DropDatabase(trans) require.NoError(t, trans.Start()) 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 where table name 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.ErrTableNameExists) }) 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("Create database with database name that already exists", func(t *testing.T) { tx, err = trans.BeginTx(context.Background(), true) require.NoError(t, err) defer tx.Rollback() err = schemar.CreateDatabase(tx, &dax.QualifiedDatabase{ OrganizationID: orgID, Database: dax.Database{ID: dbID2, Name: dbName}}) requireCode(t, err, dax.ErrDatabaseNameExists) }) 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) { t.Helper() if !errors.Is(err, code) { t.Fatalf("Error '%v' does not have code %s.", err, code) } }