package dax import ( "crypto/rand" "fmt" "regexp" "strconv" "strings" "time" "github.com/featurebasedb/featurebase/v3/errors" "github.com/featurebasedb/featurebase/v3/pql" uuid "github.com/satori/go.uuid" ) //////////////////////////////////////////////////////////////////////////////// // // Table // // The types defined below are used to standardize on tables and fields. Prior // to introducing these types, the only way we could identify a table was by // name, which wasn't even a defined type. Rather, we passed `string` values // throughout the code. // // OrganizationID - carried over from ControlPlane; currently uuid // DatabaseID - carried over from ControlPlane; currently uuid // Database - base Database struct // DatabaseKey - a string representation of OrganizationID and DatabaseID // TableID - internally stored as a uint64; presented as a hex string. // TableName - human-friendly string table name // Table - base Table struct; includes a TableID and a TableName // QualifiedDatabase - OrganizationID plus a Database // QualifiedDatabaseID - combination of OrganizationID and DatabaseID // QualifiedTable - QualifiedDatabaseID plus a Table // QualifiedTableID - QualifiedDatabaseID plus a TableID // TableKey - a string representation of OrganizationID, DatabaseID, and // TableID, which is safe to use as a FeatureBase index name. // // Example: // OrganizationID - "29-ae44-41" // DatabaseID - "75-d1a2-4f" // TableID - 123456789 (hex string: "499602d2") // TableName - foo // Table - {ID:"499602d2", Name: "foo", Fields: ... } // QualifierDatabaseID - {Org: "29-ae44-41", DB: "75-d1a2-4f"} // QualifiedTable - {Org: "29-ae44-41", DB: "75-d1a2-4f", Table: *tbl} // QualifiedTableID - {Org: "29-ae44-41", DB: "75-d1a2-4f", TableID: "499602d2"} // TableKey - "tbl__29-ae44-41__75-d1a2-4f__499602d2" // //////////////////////////////////////////////////////////////////////////////// // TableKeyDelimiter is used to delimit the qualifier elements in the TableKey. // While it might make more sense to use a pipe ("|") here, we instead use a // double underscore because underscore is one of the few characters allowed by // the FeatureBase index name restrictions, and we double it in a lame attempt // to distinquish it from FeatureBase index names which contain a single // underscore. const TableKeyDelimiter = "__" // PrefixDatabase is used as a prefix to DatabaseKey strings because FeatureBase // indexes must start with an alpha (a-z) character. Because the string // representation of a uuid (i.e. the OrganizationID value) can start with a // numeric value, we can't have OrganizationId (or any of the other ID values // which make up the DatabaseKey) be at the beginning of the DatabaseKey. const PrefixDatabase = "db" // PrefixTable is used as a prefix to TableKey strings because FeatureBase // indexes must start with an alpha (a-z) character. Because the string // representation of a uuid (i.e. the OrganizationID value) can start with a // numeric value, we can't have OrganizationId (or any of the other ID values // which make up the TableKey) be at the beginning of the TableKey. const PrefixTable = "tbl" // Base types. const ( BaseTypeBool = "bool" // BaseTypeDecimal = "decimal" // BaseTypeID = "id" // non-keyed mutex BaseTypeIDSet = "idset" // non-keyed set BaseTypeIDSetQ = "idsetq" // non-keyed set timequantum BaseTypeInt = "int" // BaseTypeString = "string" // keyed mutex BaseTypeStringSet = "stringset" // keyed set BaseTypeStringSetQ = "stringsetq" // keyed set timequantum BaseTypeTimestamp = "timestamp" // DefaultPartitionN = 256 PrimaryKeyFieldName = FieldName("_id") ) // Schema contains a list of Tables. type Schema struct { Tables []*Table } // Table returns the table with the provided name. If a table with that name // does not exist, the returned boolean will be false. func (s *Schema) Table(name TableName) (*Table, bool) { for _, tbl := range s.Tables { if tbl.Name == name { return tbl, true } } return nil, false } // OrganizationID is the unique organization identifier, currently generated by // the Control Plane in a FeatureBase cloud implementation. In that // implementation, its value is a uuid as a string, but there's nothing // enforcing that; the value could be any string. type OrganizationID string // DatabaseID is the unique database identifier, currently generated by the // Control Plane in a FeatureBase cloud implementation. In that implementation, // its value is a uuid as a string, but there's nothing enforcing that; the // value could be any string. type DatabaseID string // DatabaseIDs is a sortable slice of DatabaseID. type DatabaseIDs []DatabaseID func (s DatabaseIDs) Len() int { return len(s) } func (s DatabaseIDs) Less(i, j int) bool { return s[i] < s[j] } func (s DatabaseIDs) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // DatabaseKey is a globally unique identifier for a database; it is effectively the // compound key: (org, database). This is (hopefully) the value that will // be used when interfacing with services which are unaware of qualifiers. type DatabaseKey string // QualifiedDatabaseID returns the QualifiedDatabaseID based on the key. If // DatabaseKey can't be parsed into a valid (i.e. complete) QualifiedDatabaseID, // then blank values are used where necessary. func (dk DatabaseKey) QualifiedDatabaseID() QualifiedDatabaseID { qdbid, err := QualifiedDatabaseIDFromKey(string(dk)) if err != nil { return NewQualifiedDatabaseID("", DatabaseID(dk)) } return qdbid } // DatabaseName is a human-friendly string. type DatabaseName string // Database represents a database and its configuration. type Database struct { ID DatabaseID `json:"id"` Name DatabaseName `json:"name"` Options DatabaseOptions `json:"options"` // Tables []*Table `json:"tables"` Description string `json:"description,omitempty"` Owner string `json:"owner,omitempty"` CreatedAt int64 `json:"createdAt,omitempty"` UpdatedAt int64 `json:"updatedAt,omitempty"` UpdatedBy string `json:"updatedBy,omitempty"` } // CreateID generates a unique identifier for Database. If Database has already // been assigned an ID, then this no-ops. The reason for this is that the cloud // implementation of FeatureBase may allocate an ID before calling // CreateDatabase on the controller. func (d *Database) CreateID() (DatabaseID, error) { if d.ID != "" { return d.ID, nil } id, err := uuid.NewV4() if err != nil { return "", errors.Wrap(err, "generating uuid") } d.ID = DatabaseID(id.String()) return d.ID, nil } // DatabaseOptions are used to configure a database. type DatabaseOptions struct { WorkersMin int `json:"workers-min"` WorkersMax int `json:"workers-max"` } // DatabaseOption is a string key representing a database option. type DatabaseOption string const ( DatabaseOptionWorkersMin = "workers-min" DatabaseOptionWorkersMax = "workers-max" ) // Set sets the specified option to the provided value. func (opts *DatabaseOptions) Set(option string, value string) error { opt := strings.ToLower(option) switch opt { case DatabaseOptionWorkersMin: min, err := strconv.Atoi(value) if err != nil { return errors.Wrapf(err, "converting value to int: %s", value) } opts.WorkersMin = min // We don't currently expose WorkersMax because we aren't yet detecting // how to scale between a range, so for now we just keep it set to the // same value as WorkersMin. opts.WorkersMax = min default: return errors.Errorf("unsupported database option: %s", option) } return nil } // QualifiedDatabase is a Database along with its OrganizationID. type QualifiedDatabase struct { OrganizationID OrganizationID `json:"org-id"` Database } // NewQualifiedDatabase returns the db as a QualifiedDatabase with the provided // OrganizationID. func NewQualifiedDatabase(orgID OrganizationID, db *Database) *QualifiedDatabase { return &QualifiedDatabase{ OrganizationID: orgID, Database: *db, } } type QualifiedDatabases []*QualifiedDatabase // Key returns the string-encoded (delimited by DatabaseKeyDelimiter) globally // unique DatabaseKey. func (qdb QualifiedDatabase) Key() DatabaseKey { return qdb.QualifiedID().Key() } // String returns a human-friendly version of the QualifiedDatabase. It is only // used for display purposes; it is not used as any kind of key. func (qdb QualifiedDatabase) String() string { return fmt.Sprintf("%s (%s)", qdb.QualifiedID(), qdb.Name) } // QualifiedID returns the QualifiedDatabaseID for the database. func (qdb *QualifiedDatabase) QualifiedID() QualifiedDatabaseID { return QualifiedDatabaseID{ OrganizationID: qdb.OrganizationID, DatabaseID: qdb.ID, } } // QualifiedDatabaseID is a DatabaseID along with its OrganizationID. type QualifiedDatabaseID struct { OrganizationID OrganizationID `json:"org-id"` DatabaseID DatabaseID `json:"db-id"` } // NewQualifiedDatabaseID is a helper function used to create a // QualifiedDatabaseID from the provided arguments. func NewQualifiedDatabaseID(orgID OrganizationID, dbID DatabaseID) QualifiedDatabaseID { return QualifiedDatabaseID{ OrganizationID: orgID, DatabaseID: dbID, } } // String returns a human-friendly version of the QualifiedDatabaseID. It is only // used for display purposes; it is not used as any kind of key. For that, see // the QualifiedDatabaseID.Key() method and the DatabaseKey type. func (qdbid QualifiedDatabaseID) String() string { return fmt.Sprintf("[%s:%s]", qdbid.OrganizationID, qdbid.DatabaseID) } // Key returns the string-encoded (delimited by TableKeyDelimiter) globally // unique DatabaseKey. The key has a prefix because FeatureBase index name // restrictions require the name to start with a non-numeric value, and since a // uuid can contain a number as its first character, we have to prefix it with // something. func (qdbid QualifiedDatabaseID) Key() DatabaseKey { if qdbid.DatabaseID == "" { panic("QualifiedDatabaseID.Key called without an ID set") } return DatabaseKey(fmt.Sprintf("%s%s%s%s%s", PrefixDatabase, TableKeyDelimiter, qdbid.OrganizationID, TableKeyDelimiter, qdbid.DatabaseID)) } // QualifiedDatabaseIDs is a list of QualifiedDatabaseID. type QualifiedDatabaseIDs []QualifiedDatabaseID func (s QualifiedDatabaseIDs) Len() int { return len(s) } func (s QualifiedDatabaseIDs) Less(i, j int) bool { if s[i].OrganizationID != s[j].OrganizationID { return s[i].OrganizationID < s[j].OrganizationID } return s[i].DatabaseID < s[j].DatabaseID } func (s QualifiedDatabaseIDs) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // TableKeyer is an interface implemented by any type which can produce, and be // represented by, a TableKey. In the case of a QualifiedTable, its TableKey // might be something like `tbl__org__db__tableid`, while a general pilosa // implemenation might represent a table as a basic table name `foo`. type TableKeyer interface { Key() TableKey } // StringTableKeyer is a helper type which can wrap a string, making it a // TableKeyer. This is useful for certain calls to Execute() which take a string // index name. type StringTableKeyer string func (s StringTableKeyer) Key() TableKey { return TableKey(s) } // TableKey is a globally unique identifier for a table; it is effectively the // compound key: (org, database, table). This is (hopefully) the value that will // be used when interfacing with services which are unaware of table qualifiers. // For example, the FeatureBase server has no notion of organization or // database; its top level type is index/indexName/table. So in this case, until // and unless we introduce table qualifiers into FeatureBase, we will use // TableKey as the value for index.Name. type TableKey string func (tk TableKey) Key() TableKey { return tk } // QualifiedTableID returns the QualifiedTableID based on the key. If TableKey // can't be parsed into a valid (i.e. complete) QualifiedTableID, then blank // values are used where necessary. func (tk TableKey) QualifiedTableID() QualifiedTableID { qtid, err := QualifiedTableIDFromKey(string(tk)) if err != nil { return NewQualifiedTableID( NewQualifiedDatabaseID("", ""), TableID(tk), ) } return qtid } // TableKeys is a sortable slice of TableKey. type TableKeys []TableKey func (s TableKeys) Len() int { return len(s) } func (s TableKeys) Less(i, j int) bool { return s[i] < s[j] } func (s TableKeys) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // TableID is a table identifier. It is unique within the scope of a // QualifiedDatabaseID. Coupled with a QualifiedDatabaseID, it makes up a // QualifiedTableID and, when encoded as a string, a TableKey. type TableID string // TableIDs is a sortable slice of TableID. type TableIDs []TableID func (s TableIDs) Len() int { return len(s) } func (s TableIDs) Less(i, j int) bool { return s[i] < s[j] } func (s TableIDs) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // TableName is a human-friendly string. While it is not used as a primary key, // uniqueness is generally enforced within the scope of a QualifiedDatabaseID. type TableName string // TableNames is a sortable slice of TableName. type TableNames []TableName func (s TableNames) Len() int { return len(s) } func (s TableNames) Less(i, j int) bool { return s[i] < s[j] } func (s TableNames) Swap(i, j int) { s[i], s[j] = s[j], s[i] } // Table represents a table and its configuration. type Table struct { ID TableID `json:"id,omitempty"` Name TableName `json:"name,omitempty"` Fields []*Field `json:"fields"` PartitionN int `json:"partitionN"` Description string `json:"description,omitempty"` Owner string `json:"owner,omitempty"` UpdatedBy string `json:"updatedBy,omitempty"` } func (t *Table) Key() TableKey { return TableKey(t.ID) } // CreateID generates a unique identifier for Table. If Table has already been // assigned an ID, then an error is returned. func (t *Table) CreateID() (TableID, error) { if t.ID != "" { return "", errors.Errorf("CreateID called on table %+v that already has ID", t) } // stub is prepended to the Table.ID as a way to make IDs somewhat // human-readable for debugging purposes. If the table name is changed after // its ID has been created, this could be confusing (because the stub // portion of the ID will still resemble the initial table name). // // In order to avoid creating an ID with a double underscore, we remove all // underscores from the original table name (because that's what we use in // TableKey as a delimiter). In addition to that, we remove any other // characters which are not valid as a pilosa index name. stub := regexp.MustCompile(`[^a-z0-9-]+`).ReplaceAllString(strings.ToLower(string(t.Name)), "") if len(stub) > 10 { stub = stub[:10] } rn := make([]byte, 8) if _, err := rand.Read(rn); err != nil { return "", errors.Wrap(err, "getting random data") } t.ID = TableID(fmt.Sprintf("%s_%x", stub, rn)) return t.ID, nil } // NewTable returns a new instance of table with a pseudo-random ID which is // assumed to be unique within the scope of a QualifiedDatabaseID. func NewTable(name TableName) *Table { return &Table{ Name: name, Fields: make([]*Field, 0), } } // StringKeys returns true if the table's primary key is either a string or a // concatenation of fields. func (t *Table) StringKeys() bool { for _, fld := range t.Fields { if fld.IsPrimaryKey() { if fld.Type == BaseTypeString { return true } break } } return false } // HasValidPrimaryKey returns false if the table does not contain a primary key // field (which is required), or if the primary key field is not a valid type. func (t *Table) HasValidPrimaryKey() bool { for _, fld := range t.Fields { if !fld.IsPrimaryKey() { continue } if fld.Type == BaseTypeID || fld.Type == BaseTypeString { return true } } return false } // FieldNames returns the list of field names associated with the table. func (t *Table) FieldNames() []FieldName { ret := make([]FieldName, 0, len(t.Fields)) for _, f := range t.Fields { ret = append(ret, f.Name) } return ret } // Field returns the field with the provided name. If a field with that name // does not exist, the returned boolean will be false. func (t *Table) Field(name FieldName) (*Field, bool) { for _, fld := range t.Fields { if fld.Name == name { return fld, true } } return nil, false } // RemoveField removes the given field by name. It returns true if the field was // removed. func (t *Table) RemoveField(name FieldName) bool { for i, fld := range t.Fields { if fld.Name == name { t.Fields = append(t.Fields[:i], t.Fields[i+1:]...) return true } } return false } // CreateSQL returns the SQL CREATE TABLE string necessary to create the table. func (t *Table) CreateSQL() string { sql := fmt.Sprintf("CREATE TABLE %s (", t.Name) cols := []string{} for _, fld := range t.Fields { cols = append(cols, fld.CreateSQL()) } sql += strings.Join(cols, ", ") sql += fmt.Sprintf(") KEYPARTITIONS %d", t.PartitionN) return sql } // Tables is a sortable slice of Table. type Tables []*Table func (o Tables) Len() int { return len(o) } func (o Tables) Less(i, j int) bool { return o[i].Name < o[j].Name } func (o Tables) Swap(i, j int) { o[i], o[j] = o[j], o[i] } //////////////////////////////////////////////// // QualifiedTableID is a globally unique table identifier. It is a // sub-set of a QualifiedTable (i.e. it's just the identification // portion). Most things will take a Name or an ID and do the right // thing™. type QualifiedTableID struct { QualifiedDatabaseID ID TableID `json:"id"` Name TableName `json:"name"` } // NewQualifiedTableID is a helper function used to create a QualifiedTableID // from the provided arguments. func NewQualifiedTableID(qdbid QualifiedDatabaseID, tid TableID) QualifiedTableID { return QualifiedTableID{ QualifiedDatabaseID: qdbid, ID: tid, } } // QualifiedTableIDFromKey decodes a string key into a QualifiedTableID. The key // is assumed to have been encoded using the QualifiedTableID.Key() method. func QualifiedTableIDFromKey(key string) (QualifiedTableID, error) { parts := strings.Split(key, TableKeyDelimiter) switch len(parts) { case 4: // prefix|orgID|dbID|tblID return NewQualifiedTableID( NewQualifiedDatabaseID( OrganizationID(parts[1]), DatabaseID(parts[2]), ), TableID(parts[3]), ), nil default: return QualifiedTableID{}, errors.Errorf("invalid key: %s", key) } } // QualifiedDatabaseIDFromKey decodes a string key into a QualifiedDatabaseID. // The key is assumed to have been encoded using the QualifiedDatabaseID.Key() // method. func QualifiedDatabaseIDFromKey(key string) (QualifiedDatabaseID, error) { parts := strings.Split(key, TableKeyDelimiter) switch len(parts) { case 3: // prefix|orgID|dbID return NewQualifiedDatabaseID( OrganizationID(parts[1]), DatabaseID(parts[2]), ), nil default: return QualifiedDatabaseID{}, errors.Errorf("invalid key: %s", key) } } // String returns a human-friendly version of the QualifiedDatabaseID. It is // only used for display purposes; it is not used as any kind of key. For that, // see the QualifiedDatabaseID.Key() method. func (qtid QualifiedTableID) String() string { if qtid.ID == "" { return fmt.Sprintf("%s%s", qtid.QualifiedDatabaseID, qtid.Name) } return fmt.Sprintf("%s%s", qtid.QualifiedDatabaseID, qtid.ID) } // Key returns the string-encoded (delimited by TableKeyDelimiter) globally // unique TableKey. The key has a prefix because FeatureBase index name // restrictions require the name to start with a non-numeric value, and since a // uuid can contain a number as its first character, we have to prefix it with // something. func (qtid QualifiedTableID) Key() TableKey { if qtid.ID == "" { panic("QualifiedTableID.Key called without an ID set") } return TableKey(fmt.Sprintf("%s%s%s%s%s%s%s", PrefixTable, TableKeyDelimiter, qtid.OrganizationID, TableKeyDelimiter, qtid.DatabaseID, TableKeyDelimiter, qtid.ID)) } // Equals returns true if `other` is the same as qtid. Note: the `Name` value is // ignored in this comparison; only `QualifiedDatabaseID` and `ID` are // considered. func (qtid QualifiedTableID) Equals(other QualifiedTableID) bool { if qtid.QualifiedDatabaseID == other.QualifiedDatabaseID && qtid.ID == other.ID { return true } return false } // Qualifier returns the QualifiedDatabaseID (qdbid) portion of the // QualifiedTableID (qtid). func (qtid QualifiedTableID) Qualifier() QualifiedDatabaseID { return QualifiedDatabaseID{ OrganizationID: qtid.OrganizationID, DatabaseID: qtid.DatabaseID, } } //////////////////////////////////////////////// // QualifiedTable wraps Table and includes a QualifiedDatabaseID. type QualifiedTable struct { QualifiedDatabaseID Table } // NewQualifiedTable returns the tbl as a QualifiedTable with the provided // QualifiedDatabaseID. func NewQualifiedTable(qdbid QualifiedDatabaseID, tbl *Table) *QualifiedTable { return &QualifiedTable{ QualifiedDatabaseID: qdbid, Table: *tbl, } } // Key returns the string-encoded (delimited by TableKeyDelimiter) globally // unique TableKey. func (qt QualifiedTable) Key() TableKey { return qt.QualifiedID().Key() } // String returns a human-friendly version of the QualifiedTable. It is only // used for display purposes; it is not used as any kind of key. func (qt QualifiedTable) String() string { return fmt.Sprintf("%s (%s)", qt.QualifiedID(), qt.Name) } // Qualifier returns the QualifiedDatabaseID portion of the QualifiedTable. func (qt *QualifiedTable) Qualifier() QualifiedDatabaseID { return qt.QualifiedDatabaseID } // QualifiedID returns the QualifiedTableID for the table. func (qt *QualifiedTable) QualifiedID() QualifiedTableID { return QualifiedTableID{ QualifiedDatabaseID: qt.QualifiedDatabaseID, ID: qt.ID, Name: qt.Name, } } // QualifiedTables is a sortable slice of QualifiedTable. type QualifiedTables []*QualifiedTable func (o QualifiedTables) Len() int { return len(o) } func (o QualifiedTables) Less(i, j int) bool { return o[i].ID < o[j].ID } func (o QualifiedTables) Swap(i, j int) { o[i], o[j] = o[j], o[i] } // FieldName is a typed string used for field names. type FieldName string // BaseType is a typed string used for field types. type BaseType string // BaseTypeFromString converts a string to one of the defined BaseTypes. If the // string does not match a BaseType, then an error is returned. func BaseTypeFromString(s string) (BaseType, error) { lowered := strings.ToLower(s) switch lowered { case BaseTypeBool, BaseTypeDecimal, BaseTypeID, BaseTypeIDSet, BaseTypeIDSetQ, BaseTypeInt, BaseTypeString, BaseTypeStringSet, BaseTypeStringSetQ, BaseTypeTimestamp: return BaseType(lowered), nil default: return "", errors.Errorf("invalid field type: %s", s) } } // SplitFieldType splits a string into a BaseType and, when applicable, a slice // of qualifiers for that type. For example, the string `decimal(2)` would be // split into BaseType `decimal` and []interface{}{int64(2)}. func SplitFieldType(s string) (BaseType, []interface{}, error) { var base string var paren string parts := strings.Split(s, "(") base = parts[0] if len(parts) > 1 { parenParts := strings.Split(parts[1], ")") if len(parenParts) != 2 { return "", nil, errors.Errorf("invalid type qualifier: %s", s) } paren = parenParts[0] } baseType, err := BaseTypeFromString(base) if err != nil { return "", nil, err } // Handle the string found in parenthesis. args := []interface{}{} switch baseType { case BaseTypeDecimal: scale, err := strconv.ParseInt(paren, 10, 64) if err != nil { return "", nil, errors.Wrapf(err, "parsing int from string: %s", paren) } args = append(args, scale) } return baseType, args, nil } // Field represents a field and its configuration. type Field struct { Name FieldName `json:"name"` Type BaseType `json:"type"` Options FieldOptions `json:"options"` CreatedAt int64 `json:"createdAt,omitempty"` } // String returns the field name as a string. func (f *Field) String() string { return string(f.Name) } // FullType returns the field type along with its parenthetical (when // applicable). func (f *Field) FullType() string { switch f.Type { case BaseTypeDecimal: return fmt.Sprintf("%s(%d)", f.Type, f.Options.Scale) default: return string(f.Type) } } // StringKeys returns true if the field uses string keys. func (f *Field) StringKeys() bool { switch f.Type { case BaseTypeString, BaseTypeStringSet, BaseTypeStringSetQ: return true } return false } // IsPrimaryKey returns true if the field is the primary key field (of either // type ID or STRING). func (f *Field) IsPrimaryKey() bool { return f.Name == PrimaryKeyFieldName } // CreateSQL returns the SQL representation of the field to be used in a CREATE // TABLE statement. func (f *Field) CreateSQL() string { sql := fmt.Sprintf("%s %s", f.Name, f.Type) // Apply constraints to all non-primarykey fields. if !f.IsPrimaryKey() { sql += f.constraints() } return sql } func (f *Field) constraints() string { sql := "" // Apply constraints. switch f.Type { case BaseTypeInt: sql += fmt.Sprintf(" MIN %d MAX %d", f.Options.Min.ToInt64(0), f.Options.Max.ToInt64(0)) case BaseTypeID, BaseTypeString: if f.Options.CacheType != "" { sql += fmt.Sprintf(" CACHETYPE %s SIZE %d", f.Options.CacheType, f.Options.CacheSize) } case BaseTypeIDSet, BaseTypeStringSet: if f.Options.CacheType != "" { sql += fmt.Sprintf(" CACHETYPE %s SIZE %d", f.Options.CacheType, f.Options.CacheSize) } if f.Options.TimeQuantum != "" { sql += fmt.Sprintf(" TIMEQUANTUM '%s'", f.Options.TimeQuantum) if f.Options.TTL > 0 { sql += fmt.Sprintf(" TTL '%s'", f.Options.TTL) } } case BaseTypeTimestamp: if f.Options.TimeUnit != "" { sql += fmt.Sprintf(" TIMEUNIT '%s'", f.Options.TimeUnit) } } return sql } // FieldOptions represents options to set when initializing a field. type FieldOptions struct { Min pql.Decimal `json:"min,omitempty"` Max pql.Decimal `json:"max,omitempty"` Scale int64 `json:"scale,omitempty"` NoStandardView bool `json:"no-standard-view,omitempty"` // TODO: we should remove this CacheType string `json:"cache-type,omitempty"` CacheSize uint32 `json:"cache-size,omitempty"` TimeUnit string `json:"time-unit,omitempty"` Epoch time.Time `json:"epoch,omitempty"` TimeQuantum TimeQuantum `json:"time-quantum,omitempty"` TTL time.Duration `json:"ttl,omitempty"` ForeignIndex string `json:"foreign-index,omitempty"` TrackExistence bool `json:"track-existence"` }