| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215 |
- package executor
- import (
- "fmt"
- "strings"
- "sync"
- "time"
- "github.com/danfragoso/pizzasql-next/pkg/lexer"
- "github.com/danfragoso/pizzasql-next/pkg/parser"
- "github.com/danfragoso/pizzasql-next/pkg/pgerror"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // defaultColumnValue resolves a DEFAULT declaration into the representation the
- // schema catalog persists. A constant is evaluated once and stored as its value.
- // The whitelist of clock-reading defaults is stored as SQL text instead, so it
- // is evaluated on every write rather than frozen at CREATE/ALTER time.
- //
- // Every other non-constant expression is rejected: the catalog would have to
- // store its SQL text and a write would have to evaluate it, and silently
- // freezing the DDL-time value is worse than refusing the declaration.
- func defaultColumnValue(e *Executor, column string, expr parser.Expr) (interface{}, string, error) {
- if isConstantExpr(expr) {
- value, err := e.evalExpr(expr, nil)
- if err != nil {
- return nil, "", fmt.Errorf("evaluate DEFAULT for column %q: %w", column, err)
- }
- return value, "", nil
- }
- if isWhitelistedDefaultExpr(expr) {
- return nil, parser.FormatExpr(expr), nil
- }
- return nil, "", pgerror.Unsupported(
- "DEFAULT for column %q is not a constant expression: %s; PizzaSQL evaluates a non-constant default at write time only for CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, datetime('now'), date('now'), and time('now'). Omit the default and supply the value on INSERT",
- column, parser.FormatExpr(expr),
- )
- }
- // isConstantExpr reports whether expr is a literal, a unary sign applied to a
- // literal, a parenthesized constant, or a cast of a constant. Everything else
- // (column references including CURRENT_TIMESTAMP, function calls, subqueries,
- // arithmetic) is non-constant.
- func isConstantExpr(expr parser.Expr) bool {
- switch ex := expr.(type) {
- case *parser.LiteralExpr:
- return true
- case *parser.UnaryExpr:
- if ex.Op != lexer.TokenMinus && ex.Op != lexer.TokenPlus {
- return false
- }
- return isConstantExpr(ex.Operand)
- case *parser.ParenExpr:
- return isConstantExpr(ex.Expr)
- case *parser.CastExpr:
- return isConstantExpr(ex.Expr)
- default:
- return false
- }
- }
- // isWhitelistedDefaultExpr reports whether expr is one of the clock-reading
- // defaults PizzaSQL evaluates per write. The list is deliberately small: these
- // are the forms that SQLite applications declare, and each maps to a value the
- // engine already produces for the equivalent expression.
- func isWhitelistedDefaultExpr(expr parser.Expr) bool {
- // SQLite applications commonly wrap a default in parentheses, for example
- // DEFAULT (datetime('now')). The wrapper does not change the expression.
- if paren, ok := expr.(*parser.ParenExpr); ok {
- return isWhitelistedDefaultExpr(paren.Expr)
- }
- switch ex := expr.(type) {
- case *parser.ColumnRef:
- if ex.Table != "" {
- return false
- }
- switch strings.ToLower(ex.Column) {
- case "current_timestamp", "current_date", "current_time":
- return true
- }
- case *parser.FunctionCall:
- if len(ex.Args) != 1 {
- return false
- }
- switch strings.ToLower(ex.Name) {
- case "datetime", "date", "time":
- default:
- return false
- }
- lit, ok := ex.Args[0].(*parser.LiteralExpr)
- return ok && lit.Type == lexer.TokenString && strings.EqualFold(lit.Value, "now")
- }
- return false
- }
- // defaultExprCache holds parsed default expressions. The stored form is SQL
- // text validated at DDL time, so parsing it again per statement uses the same
- // parser path and keeps the catalog independent of the AST.
- var defaultExprCache sync.Map // map[string]parser.Expr
- func parseDefaultExpr(text string) (parser.Expr, error) {
- if cached, ok := defaultExprCache.Load(text); ok {
- if expr, ok := cached.(parser.Expr); ok {
- return expr, nil
- }
- }
- expr, err := parser.ParseExpr(text)
- if err != nil {
- return nil, fmt.Errorf("invalid stored DEFAULT expression %q: %w", text, err)
- }
- defaultExprCache.Store(text, expr)
- return expr, nil
- }
- // evalDefaultExpressionAt evaluates a persisted default with an explicit clock.
- // The value shapes match what the same expression returns in a query, so a
- // defaulted timestamp column compares equal to CURRENT_TIMESTAMP:
- // CURRENT_TIMESTAMP and datetime('now') yield "2006-01-02 15:04:05" UTC,
- // CURRENT_DATE and date('now') yield "2006-01-02", and CURRENT_TIME and
- // time('now') yield "15:04:05". That space-separated UTC text is what the
- // engine already stores and what the timestamptz OID path emits, so clients
- // that decode timestamptz accept it.
- func evalDefaultExpressionAt(text string, now time.Time) (interface{}, error) {
- expr, err := parseDefaultExpr(text)
- if err != nil {
- return nil, err
- }
- for {
- paren, ok := expr.(*parser.ParenExpr)
- if !ok {
- break
- }
- expr = paren.Expr
- }
- if !isWhitelistedDefaultExpr(expr) {
- return nil, pgerror.Unsupported(
- "stored DEFAULT expression %q is not supported; PizzaSQL only evaluates CURRENT_TIMESTAMP, CURRENT_DATE, CURRENT_TIME, datetime('now'), date('now'), and time('now') at write time",
- text,
- )
- }
- switch ex := expr.(type) {
- case *parser.ColumnRef:
- value, _ := sqliteCurrentTimeValueAt(ex.Column, now)
- return value, nil
- case *parser.FunctionCall:
- keyword := map[string]string{
- "datetime": "current_timestamp",
- "date": "current_date",
- "time": "current_time",
- }[strings.ToLower(ex.Name)]
- value, _ := sqliteCurrentTimeValueAt(keyword, now)
- return value, nil
- }
- return nil, pgerror.Unsupported("stored DEFAULT expression %q is not supported", text)
- }
- // statementDefaults resolves every default in the schema for the statement.
- // Expression defaults are evaluated once, so a multi-row INSERT stamps all its
- // rows with one value while a later statement reads the clock again. Constant
- // catalog defaults are included too: the conflict-matching and RETURNING paths
- // inspect the candidate row before storage fills missing columns, so a key
- // column that comes from a DEFAULT must already be present in it.
- func (e *Executor) statementDefaults(schema *storage.Schema) (map[string]interface{}, error) {
- // One instant per statement: two clock-default columns in the same row must
- // not disagree because the loop sampled the clock twice. The clock is read
- // lazily so a schema without expression defaults never samples it.
- var now time.Time
- sampledClock := false
- var defaults map[string]interface{}
- add := func(name string, value interface{}) {
- if defaults == nil {
- defaults = make(map[string]interface{})
- }
- defaults[name] = value
- }
- for _, column := range schema.Columns {
- if column.DefaultExpr == "" {
- if column.Default != nil {
- add(column.Name, column.Default)
- }
- continue
- }
- if !sampledClock {
- now = e.statementNow()
- sampledClock = true
- }
- value, err := evalDefaultExpressionAt(column.DefaultExpr, now)
- if err != nil {
- return nil, fmt.Errorf("evaluate DEFAULT for column %q: %w", column.Name, err)
- }
- add(column.Name, value)
- }
- return defaults, nil
- }
- // statementNow returns the instant one statement's expression defaults use.
- // The executor samples it once per INSERT/UPDATE; the storage layer samples it
- // once per ALTER TABLE statement. Tests inject e.clock to make it deterministic.
- func (e *Executor) statementNow() time.Time {
- if e.clock != nil {
- return e.clock().UTC()
- }
- return time.Now().UTC()
- }
- // applyRowDefaults fills the defaulted columns a statement omitted. An explicit
- // NULL is already present in the row, so it wins over the default.
- func applyRowDefaults(row storage.Row, defaults map[string]interface{}) {
- for name, value := range defaults {
- if _, present := lookupRowValue(row, name); present {
- continue
- }
- row[name] = value
- }
- }
|