featurebase/dax/controller/sqldb/schemar.go
David Kagan 24a45bc30d
Cloud 1475 (#2371)
* working on incorporating regex logic

* Implemented a validation check for database name with given rules in doc within controller

* fixing tests to pass

* reflecting changes to match docs

* fixed tests further, hopefully

* for sure fixed integration tests, and moved validation check

* integration tests passed, go test now will pass

* fixed name size to 230 due to previous commit acknowledgement

* fixed field test negative validations

* added missing comma
2023-04-07 15:08:07 -04:00

471 lines
14 KiB
Go

package sqldb
import (
"encoding/json"
"fmt"
"strconv"
"github.com/pkg/errors"
featurebase "github.com/featurebasedb/featurebase/v3"
"github.com/featurebasedb/featurebase/v3/dax"
"github.com/featurebasedb/featurebase/v3/dax/controller/schemar"
"github.com/featurebasedb/featurebase/v3/dax/models"
"github.com/featurebasedb/featurebase/v3/logger"
)
func NewSchemar(log logger.Logger) schemar.Schemar {
if log == nil {
log = logger.NopLogger
}
return &Schemar{
log: log,
}
}
// Schemar is the SQL implementation of the schemar that stores all
// the information in a SQL database.
type Schemar struct {
log logger.Logger
}
func (s *Schemar) CreateDatabase(tx dax.Transaction, qdb *dax.QualifiedDatabase) error {
// Ensure the database id is not blank.
if qdb.ID == "" {
return schemar.NewErrDatabaseIDInvalid(qdb.ID)
}
// Sanitizing database name
if err := featurebase.ValidateName(string(qdb.Name)); err != nil {
return schemar.NewErrDatabaseNameInvalid(qdb.Name)
}
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
if exists, err := dt.C.Where("id = ?", qdb.Database.ID).Exists(&models.Database{}); err != nil {
return errors.Wrap(err, "checking database existence")
} else if exists {
return dax.NewErrDatabaseIDExists(qdb.QualifiedID())
}
// Check if org exists, if not, create org.
org := &models.Organization{ID: string(qdb.OrganizationID)}
if exists, err := dt.C.Where("id = ?", qdb.OrganizationID).Exists(org); err != nil {
return errors.Wrap(err, "checking for org")
} else if !exists {
if err := dt.C.Create(org); err != nil {
return errors.Wrap(err, "creating organization")
}
}
// Check if database name exists in org, if does, throw error.
if exists, err := dt.C.Where("name = ? AND organization_id = ?", qdb.Name, org.ID).Exists(&models.Database{}); err != nil {
return errors.Wrap(err, "checking database name")
} else if exists {
return dax.NewErrDatabaseNameExists(qdb.Name)
}
db := toModelDatabase(qdb)
if err := dt.C.Create(db); err != nil {
return errors.Wrap(err, "creating database object")
}
return nil
}
func (s *Schemar) DropDatabase(tx dax.Transaction, qdb dax.QualifiedDatabaseID) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
db := &models.Database{}
err := dt.C.RawQuery("DELETE from databases where id = ? RETURNING id", qdb.DatabaseID).First(db)
if isNoRowsError(err) {
return dax.NewErrDatabaseIDDoesNotExist(qdb)
}
return errors.Wrap(err, "deleting database")
}
func (s *Schemar) DatabaseByName(tx dax.Transaction, orgID dax.OrganizationID, dbname dax.DatabaseName) (*dax.QualifiedDatabase, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
db := &models.Database{}
err := dt.C.Where("organization_id = ? and name = ?", orgID, dbname).First(db)
if isNoRowsError(err) {
return nil, dax.NewErrDatabaseNameDoesNotExist(dbname)
} else if err != nil {
return nil, errors.Wrap(err, "finding database")
}
return toQualifiedDatabase(db), nil
}
func toModelDatabase(qdb *dax.QualifiedDatabase) *models.Database {
db := qdb.Database
return &models.Database{
ID: string(db.ID),
Name: db.Name,
WorkersMin: db.Options.WorkersMin,
WorkersMax: db.Options.WorkersMax,
Description: db.Description,
Owner: db.Owner,
UpdatedBy: db.UpdatedBy,
// Tables: []*models.Table{},
OrganizationID: string(qdb.OrganizationID),
// CreatedAt: time.Unix(db.CreatedAt, 0),
// UpdatedAt: time.Unix(db.UpdatedAt),
}
}
func toQualifiedDatabase(db *models.Database) *dax.QualifiedDatabase {
return &dax.QualifiedDatabase{
OrganizationID: dax.OrganizationID(db.OrganizationID),
Database: dax.Database{
ID: dax.DatabaseID(db.ID),
Name: dax.DatabaseName(db.Name),
Options: dax.DatabaseOptions{
WorkersMin: db.WorkersMin,
WorkersMax: db.WorkersMax,
},
Description: db.Description,
Owner: db.Owner,
CreatedAt: db.CreatedAt.Unix(), // TODO is this right, or UnixNano, or...?
UpdatedAt: db.UpdatedAt.Unix(),
UpdatedBy: db.UpdatedBy,
}}
}
func (s *Schemar) DatabaseByID(tx dax.Transaction, qdb dax.QualifiedDatabaseID) (*dax.QualifiedDatabase, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
db := &models.Database{}
err := dt.C.Find(db, string(qdb.DatabaseID))
if isNoRowsError(err) {
return nil, dax.NewErrDatabaseIDDoesNotExist(qdb)
} else if err != nil {
return nil, errors.Wrap(err, "finding DB")
}
return toQualifiedDatabase(db), nil
}
func (s *Schemar) SetDatabaseOption(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, option string, value string) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
var val int64
var err error
switch option {
case dax.DatabaseOptionWorkersMin:
option = "workers_min" // convert to table column name
val, err = strconv.ParseInt(value, 0, 64)
if err != nil {
return errors.Wrap(err, "parsing workers min value")
}
case dax.DatabaseOptionWorkersMax:
val, err = strconv.ParseInt(value, 0, 64)
option = "workers_max" // convert to table column name
if err != nil {
return errors.Wrap(err, "parsing workers max value")
}
default:
return errors.Errorf("unsupported database option: %s", option)
}
db := &models.Database{}
err = dt.C.RawQuery(fmt.Sprintf("UPDATE databases set %s = ? WHERE id = ? RETURNING id", option), val, qdbid.DatabaseID).First(db)
if isNoRowsError(err) {
return dax.NewErrDatabaseIDDoesNotExist(qdbid)
} else if err != nil {
return errors.Wrap(err, "updating option")
}
return errors.Wrap(err, "updating database option")
}
// Databases returns a list of databases. If the list of DatabaseIDs is
// empty, all databases will be returned. If greater than zero DatabaseIDs
// are passed in the second argument, only databases matching those IDs will
// be returned.
func (s *Schemar) Databases(tx dax.Transaction, orgID dax.OrganizationID, dbIDs ...dax.DatabaseID) ([]*dax.QualifiedDatabase, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
s.log.Debugf("Schemar: Databases: orgID: %s dbIDs: %v", orgID, dbIDs)
dbs := []*models.Database{}
q := dt.C.Q()
if orgID != "" {
q = q.Where("organization_id = ?", orgID)
}
if len(dbIDs) > 0 {
ifaceIDs := make([]interface{}, len(dbIDs))
for i, dbID := range dbIDs {
ifaceIDs[i] = dbID
}
q = q.Where("id in (?)", ifaceIDs...)
}
err := q.Order("created_at asc").All(&dbs)
if err != nil {
return nil, errors.Wrap(err, "finding databases")
}
ret := make([]*dax.QualifiedDatabase, len(dbs))
for i, db := range dbs {
ret[i] = toQualifiedDatabase(db)
}
s.log.Debugf("Schemar: Databases: returning %+v", ret)
return ret, nil
}
func (s *Schemar) CreateTable(tx dax.Transaction, qtbl *dax.QualifiedTable) error {
// Ensure the table id is not blank.
if qtbl.ID == "" {
return schemar.NewErrTableIDInvalid(qtbl.ID)
}
// Ensure the table name is not blank.
if qtbl.Name == "" {
return schemar.NewErrTableNameInvalid(qtbl.Name)
}
// Ensure that a primary key field is present and valid.
if !qtbl.HasValidPrimaryKey() {
return schemar.NewErrInvalidPrimaryKey()
}
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
// Check to see if table name exists for a database ID, and if so, throw error
if exists, err := dt.C.Where("name = ? AND database_id = ?", qtbl.Name, qtbl.DatabaseID).Exists(&models.Table{}); err != nil {
return errors.Wrap(err, "checking if table name exists")
} else if exists {
return dax.NewErrTableNameExists(qtbl.Name)
}
tbl := toModelTable(qtbl)
err := dt.C.Eager().Create(tbl)
if isViolatesUniqueConstraint(err) {
return dax.NewErrTableIDExists(qtbl.QualifiedID())
}
return errors.Wrap(err, "creating database object")
}
func toModelTable(qtbl *dax.QualifiedTable) *models.Table {
columns := make([]models.Column, len(qtbl.Fields))
for i, fld := range qtbl.Fields {
columns[i] = toModelColumn(qtbl.Key(), fld)
}
return &models.Table{
ID: string(qtbl.Key()),
Name: qtbl.Name,
Owner: qtbl.Owner,
OrganizationID: qtbl.OrganizationID,
Columns: columns,
UpdatedBy: qtbl.UpdatedBy,
DatabaseID: string(qtbl.QualifiedDatabaseID.DatabaseID),
Description: qtbl.Description,
PartitionN: qtbl.PartitionN,
}
}
func toModelColumn(tk dax.TableKey, fld *dax.Field) models.Column {
optBytes, err := json.Marshal(fld.Options)
if err != nil {
panic(err)
}
return models.Column{
Name: fld.Name,
Type: fld.Type,
TableID: string(tk),
Constraints: "TODO: unimplemented",
Options: string(optBytes),
}
}
func toField(col models.Column) *dax.Field {
opts := dax.FieldOptions{}
err := json.Unmarshal([]byte(col.Options), &opts)
if err != nil {
panic(err)
}
return &dax.Field{
Name: col.Name,
Type: col.Type,
Options: opts,
}
}
func toQualifiedTable(mtbl *models.Table) *dax.QualifiedTable {
fields := make([]*dax.Field, len(mtbl.Columns))
for i, col := range mtbl.Columns {
fields[i] = toField(col)
}
return &dax.QualifiedTable{
QualifiedDatabaseID: dax.QualifiedDatabaseID{
OrganizationID: mtbl.OrganizationID,
DatabaseID: dax.DatabaseID(mtbl.DatabaseID),
},
Table: dax.Table{
ID: dax.TableKey(mtbl.ID).QualifiedTableID().ID,
Name: mtbl.Name,
Fields: fields,
PartitionN: mtbl.PartitionN,
Description: mtbl.Description,
Owner: mtbl.Owner,
UpdatedBy: mtbl.UpdatedBy,
},
}
}
func (s *Schemar) DropTable(tx dax.Transaction, qtid dax.QualifiedTableID) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
err := dt.C.Destroy(&models.Table{ID: string(qtid.Key())})
return errors.Wrap(err, "destroying table")
}
func (s *Schemar) CreateField(tx dax.Transaction, qtid dax.QualifiedTableID, field *dax.Field) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
if field.Name == "" {
return schemar.NewErrFieldNameInvalid(field.Name)
}
// we could probably make this a single query with an INSERT WHERE
// (subselect), but then would have to construct the whole insert
// by hand which would be annoying and error prone to keep up to
// date
cols := &models.Columns{}
err := dt.C.Where("name = ? and table_id = ?", field.Name, qtid.Key()).All(cols)
if err != nil {
return errors.Wrap(err, "looking up field")
}
if len(*cols) > 0 {
return dax.NewErrFieldExists(field.Name)
}
col := toModelColumn(qtid.Key(), field)
err = dt.C.Create(&col)
return errors.Wrap(err, "creating column")
}
func (s *Schemar) DropField(tx dax.Transaction, qtid dax.QualifiedTableID, fieldName dax.FieldName) error {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
col := &models.Column{}
err := dt.C.Where("table_id = ? and name = ?", qtid.Key(), fieldName).First(col)
if err != nil {
if isNoRowsError(err) {
return dax.NewErrFieldDoesNotExist(fieldName)
}
return errors.Wrap(err, "querying for field")
}
err = dt.C.Destroy(col)
return errors.Wrap(err, "destroying col")
}
func (s *Schemar) Table(tx dax.Transaction, qtid dax.QualifiedTableID) (*dax.QualifiedTable, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
tbl := &models.Table{}
if qtid.ID != "" {
if err := dt.C.Eager().Find(tbl, qtid.Key()); err != nil {
if isNoRowsError(err) {
return nil, dax.NewErrTableIDDoesNotExist(qtid)
}
return nil, errors.Wrap(err, "finding table by ID")
}
} else {
if err := dt.C.Eager().Where("database_id = ? and name = ?", qtid.DatabaseID, qtid.Name).First(tbl); err != nil {
if isNoRowsError(err) {
return nil, dax.NewErrTableNameDoesNotExist(qtid.Name)
}
return nil, errors.Wrap(err, "finding table by name")
}
}
return toQualifiedTable(tbl), nil
}
// Tables returns a list of tables in the given database. If tableIDs
// are given, only tables with matching IDs are returned.
func (s *Schemar) Tables(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, tableIDs ...dax.TableID) ([]*dax.QualifiedTable, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return nil, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
query := dt.C.Where("database_id = ?", qdbid.DatabaseID)
if len(tableIDs) > 0 {
ifaceIDs := make([]interface{}, len(tableIDs))
for i, tableID := range tableIDs {
ifaceIDs[i] = dax.QualifiedTableID{QualifiedDatabaseID: qdbid, ID: tableID}.Key()
}
query = query.Where("id in (?)", ifaceIDs)
}
tables := []*models.Table{}
err := query.Eager().Order("name asc").All(&tables)
if err != nil {
return nil, errors.Wrap(err, "querying all tables")
}
ret := make([]*dax.QualifiedTable, len(tables))
for i, tab := range tables {
ret[i] = toQualifiedTable(tab)
}
return ret, nil
}
// TableID is a reverse-lookup method to get the TableID for a given
// qualified TableName.
func (s *Schemar) TableID(tx dax.Transaction, qdbid dax.QualifiedDatabaseID, tableName dax.TableName) (dax.QualifiedTableID, error) {
dt, ok := tx.(*DaxTransaction)
if !ok {
return dax.QualifiedTableID{}, dax.NewErrInvalidTransaction("*sqldb.DaxTransaction")
}
tbl := &models.Table{}
if err := dt.C.Where("database_id = ? and name = ?", qdbid.DatabaseID, tableName).First(tbl); err != nil {
if isNoRowsError(err) {
return dax.QualifiedTableID{}, dax.NewErrTableNameDoesNotExist(tableName)
}
return dax.QualifiedTableID{}, errors.Wrapf(err, "looking up table by name '%s', dbid: '%s'", tableName, qdbid.DatabaseID)
}
return dax.TableKey(tbl.ID).QualifiedTableID(), nil
}