// Copyright 2024 Dolthub, Inc. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. package id import ( "bytes" "fmt" "strings" "unsafe" ) // Id uses one of two formats. Which format is being used is marked by the upper Section bit being either 0 or 1. // Often, an ID contains information that will commonly be accessed by the item, so the first format is tailored for // efficient retrieval of specific segments. If an item is larger than the size limit (255, size is stored as an uint8), // then we use the second format, which inserts a separator between items. This allows Id to hold any data in case // the need arises in the future, but in practice we'll only see the first format (since data will usually be // identifiers or smaller embedded IDs). Id IDs will be accessed far more often than they'll be created, hence the // focus on efficient retrieval rather than simplicity of storage. // // First format (upper bit is 0): // The first byte is the section // The second byte contains the number of segments N (up to 255 segments) // The next N bytes contain the length of each respective segment (up to 255 bytes) // The remaining bytes are the original string data, stored contiguously // Second format (upper bit is 1): // The first byte is the section // The remaining bytes are the original string data, stored with the separator between each segment const ( // idSeparator marks the different data sections in an Id. This is the null byte since that byte is invalid in // all identifiers, so we can guarantee that it's safe to use as a separator. This is used when an individual data // segment is larger than 254 bytes. idSeparator = "\x00" // formatMask is the upper bit that determines whether we're using the first or second format. formatMask = uint8(0x80) // Null is an empty, invalid ID. Null Id = "" // NullAccessMethod is an empty, invalid ID. This is exactly equivalent to Null. NullAccessMethod AccessMethod = "" // NullCast is an empty, invalid ID. This is exactly equivalent to Null. NullCast Cast = "" // NullCheck is an empty, invalid ID. This is exactly equivalent to Null. NullCheck Check = "" // NullCollation is an empty, invalid ID. This is exactly equivalent to Null. NullCollation Collation = "" // NullColumnDefault is an empty, invalid ID. This is exactly equivalent to Null. NullColumnDefault ColumnDefault = "" // NullDatabase is an empty, invalid ID. This is exactly equivalent to Null. NullDatabase Database = "" // NullEnumLabel is an empty, invalid ID. This is exactly equivalent to Null. NullEnumLabel EnumLabel = "" // NullExtension is an empty, invalid ID. This is exactly equivalent to Null. NullExtension Extension = "" // NullForeignKey is an empty, invalid ID. This is exactly equivalent to Null. NullForeignKey ForeignKey = "" // NullFunction is an empty, invalid ID. This is exactly equivalent to Null. NullFunction Function = "" // NullIndex is an empty, invalid ID. This is exactly equivalent to Null. NullIndex Index = "" // NullNamespace is an empty, invalid ID. This is exactly equivalent to Null. NullNamespace Namespace = "" // NullProcedure is an empty, invalid ID. This is exactly equivalent to Null. NullProcedure Procedure = "" // NullSequence is an empty, invalid ID. This is exactly equivalent to Null. NullSequence Sequence = "" // NullTable is an empty, invalid ID. This is exactly equivalent to Null. NullTable Table = "" // NullTrigger is an empty, invalid ID. This is exactly equivalent to Null. NullTrigger Trigger = "" // NullType is an empty, invalid ID. This is exactly equivalent to Null. NullType Type = "" // NullView is an empty, invalid ID. This is exactly equivalent to Null. NullView View = "" ) // Id is an ID that is used within Doltgres. This ID is never exposed to clients through any normal means, and // exists solely for internal operations to be able to identify specific items. This functions as an internal // replacement for Postgres' OIDs. type Id string // NewId constructs an Id using the given section and data. In general, you should prefer to use the `NewIDTYPE` that // matches the Section that's being created, and then convert that to an Id for returning or storage. You almost never // want to call this function directly. func NewId(section Section, data ...string) Id { if section == Section_Null { // It's easier if there's only one canonical way to represent a null ID, so we'll return our constant instead of // creating a new string return Null } if len(data) > 255 { return newIdSecondFormat(section, data) } buf := bytes.Buffer{} buf.WriteByte(uint8(section)) buf.WriteByte(uint8(len(data))) for _, segment := range data { segmentLength := len(segment) if segmentLength > 255 { return newIdSecondFormat(section, data) } buf.WriteByte(uint8(segmentLength)) } for _, segment := range data { buf.WriteString(segment) } return Id(buf.Bytes()) } // newIdSecondFormat constructs an Id using the given section and data. This always returns the second format (using the // separator). func newIdSecondFormat(section Section, data []string) Id { buf := bytes.Buffer{} buf.WriteByte(uint8(section) | formatMask) for i, segment := range data { if i > 0 { buf.WriteString(idSeparator) } buf.WriteString(segment) } return Id(buf.Bytes()) } // IsValid returns whether the Id is valid. func (id Id) IsValid() bool { // We don't allow setting the section to Section_Null, so we can do a simple length check return len(id) > 0 } // Section returns the Section for this Id. func (id Id) Section() Section { if len(id) == 0 { return Section_Null } return Section(id[0] & (^formatMask)) } // Data returns the original data used to create this Id. func (id Id) Data() []string { if len(id) <= 1 { return nil } if id[0]&formatMask == formatMask { // Second format return strings.Split(string(id[1:]), idSeparator) } else { // First format segmentCount := int(id[1]) data := id[2+segmentCount:] // We skip 2 for the section and count bytes, then the number of segment counts segments := make([]string, segmentCount) start := 0 for i := 0; i < segmentCount; i++ { length := int(id[2+i]) segments[i] = string(data[start : start+length]) start += length } return segments } } // SegmentCount returns the number of segments that were in the original data. func (id Id) SegmentCount() int { if len(id) <= 1 { return 0 } if id[0]&formatMask == formatMask { // Second format return len(id.Data()) } else { // First format return int(id[1]) } } // Segment returns the segment from the given index. An empty string is returned for an index not contained by the ID. func (id Id) Segment(index int) string { if index < 0 || len(id) <= 1 { return "" } if id[0]&formatMask == formatMask { // Second format data := id.Data() if index >= len(data) { return "" } return data[index] } else { // First format segmentCount := int(id[1]) data := id[2+segmentCount:] // We skip 2 for the section and count bytes, then the number of segment counts if index >= segmentCount { return "" } start := 0 currentLength := 0 for i := 0; i <= index; i++ { start += currentLength currentLength = int(id[2+i]) } return string(data[start : start+currentLength]) } } // String returns a display-suitable version of the ID. Although the ID is implemented as a string, it should not be // treated as a string except for the purposes of storage and retrieval. func (id Id) String() string { data := id.Data() if len(data) == 0 { return fmt.Sprintf(`{%s:[]}`, id.Section().String()) } return fmt.Sprintf(`{%s:["%s"]}`, id.Section().String(), strings.Join(data, `","`)) } // CaseString returns a quoted string that may be used to represent this ID in a switch-case. func (id Id) CaseString() string { if len(id) == 0 { return `""` } if id[0]&formatMask == formatMask { // Second format data := strings.ReplaceAll(string(id[1:]), "\x00", `\x00`) data = strings.ReplaceAll(data, `"`, `\x22`) return fmt.Sprintf(`"\x%02x%s"`, id[0], data) } else { // First format sb := strings.Builder{} sb.Grow(len(id) + 32) sb.WriteRune('"') count := int(id[1]) sb.WriteString(fmt.Sprintf(`\x%02x\x%02x`, id[0], count)) for i := 0; i < count; i++ { sb.WriteString(fmt.Sprintf(`\x%02x`, id[2+i])) } sb.WriteString(strings.ReplaceAll(string(id[2+count:]), `"`, `\x22`)) sb.WriteRune('"') return sb.String() } } // UnderlyingBytes returns the underlying bytes for the ID. These must not be modified, as this is intended solely for // efficient usage of operations that require byte slices. func (id Id) UnderlyingBytes() []byte { return unsafe.Slice(unsafe.StringData(string(id)), len(id)) } // usesSecondFormat returns whether the separator is used, which is the second format. func (id Id) usesSecondFormat() bool { return len(id) > 0 && id[0]&formatMask == formatMask }