- mssql: write computed columns (AS (expr) PERSISTED) and identity; read computed definition and identity - sqlite: write GENERATED ALWAYS AS (expr) STORED; read generated columns via table_xinfo and parse the expression - drizzle: generatedAlwaysAs / generated(Always|ByDefault)AsIdentity, read and write - typeorm: asExpression/generatedType and identity decorators, read and write; decorator scan is now quote-aware - bun, gorm readers: read generated and identity markers - readmes updated
117 lines
2.4 KiB
Go
117 lines
2.4 KiB
Go
package sqlite
|
|
|
|
import (
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
var generatedAsRegex = regexp.MustCompile(`(?is)\bAS\s*\(`)
|
|
|
|
// parseGeneratedExpression extracts the expression of a generated column from a CREATE
|
|
// TABLE statement, e.g. `full TEXT GENERATED ALWAYS AS (a || b) STORED` yields `a || b`.
|
|
// It returns "" when the column or its expression cannot be found.
|
|
func parseGeneratedExpression(createSQL, columnName string) string {
|
|
open := strings.Index(createSQL, "(")
|
|
if open < 0 {
|
|
return ""
|
|
}
|
|
|
|
for _, def := range splitTopLevel(createSQL[open+1:]) {
|
|
if !strings.EqualFold(firstIdentifier(def), columnName) {
|
|
continue
|
|
}
|
|
loc := generatedAsRegex.FindStringIndex(def)
|
|
if loc == nil {
|
|
return ""
|
|
}
|
|
return balancedParens(def[loc[1]-1:])
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// splitTopLevel splits a table body on commas that are outside parentheses and quotes,
|
|
// stopping at the parenthesis that closes the body.
|
|
func splitTopLevel(body string) []string {
|
|
var parts []string
|
|
depth := 0
|
|
var quote byte
|
|
start := 0
|
|
for i := 0; i < len(body); i++ {
|
|
ch := body[i]
|
|
if quote != 0 {
|
|
if ch == quote {
|
|
quote = 0
|
|
}
|
|
continue
|
|
}
|
|
switch ch {
|
|
case '\'', '"', '`':
|
|
quote = ch
|
|
case '[':
|
|
quote = ']'
|
|
case '(':
|
|
depth++
|
|
case ')':
|
|
if depth == 0 {
|
|
return append(parts, body[start:i])
|
|
}
|
|
depth--
|
|
case ',':
|
|
if depth == 0 {
|
|
parts = append(parts, body[start:i])
|
|
start = i + 1
|
|
}
|
|
}
|
|
}
|
|
return append(parts, body[start:])
|
|
}
|
|
|
|
// firstIdentifier returns the first (possibly quoted) identifier of a column definition.
|
|
func firstIdentifier(def string) string {
|
|
def = strings.TrimSpace(def)
|
|
if def == "" {
|
|
return ""
|
|
}
|
|
switch def[0] {
|
|
case '"', '\'', '`':
|
|
if end := strings.IndexByte(def[1:], def[0]); end >= 0 {
|
|
return def[1 : 1+end]
|
|
}
|
|
case '[':
|
|
if end := strings.IndexByte(def, ']'); end >= 0 {
|
|
return def[1:end]
|
|
}
|
|
}
|
|
if end := strings.IndexAny(def, " \t\r\n"); end >= 0 {
|
|
return def[:end]
|
|
}
|
|
return def
|
|
}
|
|
|
|
// balancedParens returns the text inside the parenthesis group that starts at s[0].
|
|
func balancedParens(s string) string {
|
|
depth := 0
|
|
var quote byte
|
|
for i := 0; i < len(s); i++ {
|
|
ch := s[i]
|
|
if quote != 0 {
|
|
if ch == quote {
|
|
quote = 0
|
|
}
|
|
continue
|
|
}
|
|
switch ch {
|
|
case '\'', '"':
|
|
quote = ch
|
|
case '(':
|
|
depth++
|
|
case ')':
|
|
depth--
|
|
if depth == 0 {
|
|
return strings.TrimSpace(s[1:i])
|
|
}
|
|
}
|
|
}
|
|
return ""
|
|
}
|