| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574 |
- package pgserver
- import (
- "strconv"
- "strings"
- "github.com/danfragoso/pizzasql-next/pkg/lexer"
- "github.com/danfragoso/pizzasql-next/pkg/parser"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // textParameterOID is the type a parameter falls back to when the SQL gives no
- // usable context. PostgreSQL's Describe never reports OID 0 for a parameter:
- // drivers such as pgx reject the statement client-side when the described
- // parameter count or type disagrees with the arguments they were given.
- const textParameterOID = 25
- // markerPrefix identifies the string literal substituted for a $n placeholder
- // while a statement is parsed for type inference. The parser has no parameter
- // node, so each placeholder becomes a uniquely named string literal whose
- // position in the AST supplies the surrounding comparison or assignment
- // context.
- const markerPrefix = "__pizzasql_parameter_"
- // parameterReference records one $n placeholder found by the quote- and
- // comment-aware scan that Bind uses. number is 0 when the digits do not fit in
- // an int, which Bind reports as a missing parameter.
- type parameterReference struct {
- number int
- start int
- end int
- }
- // negotiateParameterOIDs builds the ParameterDescription type list for query.
- // The list length is the highest placeholder number (or the client's declared
- // count when that is larger), client-declared non-zero OIDs win, and every
- // remaining entry is inferred from the SQL or falls back to TEXT.
- func (c *Connection) negotiateParameterOIDs(query string, declared []int32) []int32 {
- references := scanParameters(query)
- count := len(declared)
- for _, reference := range references {
- if reference.number > count {
- count = reference.number
- }
- }
- oids := make([]int32, count)
- copy(oids, declared)
- if count == 0 {
- return oids
- }
- inferred := c.inferParameterTypes(query, references)
- for i := range oids {
- if oids[i] != 0 {
- continue
- }
- oids[i] = textParameterOID
- if typeName, ok := inferred[i+1]; ok {
- oids[i] = c.getOIDForType(typeName)
- }
- }
- return oids
- }
- // inferParameterTypes returns the type name of every parameter whose SQL
- // context names one. Inference is best effort: a statement the parser does not
- // accept, or a column the catalog cannot resolve, leaves that parameter out so
- // the caller falls back to TEXT.
- func (c *Connection) inferParameterTypes(query string, references []parameterReference) map[int]string {
- if len(references) == 0 {
- return nil
- }
- marked, markers := substituteParameterMarkers(query, references)
- stmt, err := parser.New(lexer.New(marked)).Parse()
- if err != nil {
- return nil
- }
- inferrer := ¶meterInferrer{
- conn: c,
- markers: markers,
- types: make(map[int]string),
- }
- inferrer.walkStatement(nil, stmt)
- return inferrer.types
- }
- // scanParameters locates $n placeholders outside string literals, quoted
- // identifiers, and comments. Bind and parameter inference share it, so Describe
- // sees exactly the placeholders Bind replaces.
- func scanParameters(query string) []parameterReference {
- var references []parameterReference
- var quote byte
- for i := 0; i < len(query); {
- ch := query[i]
- if quote != 0 {
- i++
- if ch == quote {
- if i < len(query) && query[i] == quote {
- i++
- } else {
- quote = 0
- }
- }
- continue
- }
- if i+1 < len(query) && ch == '-' && query[i+1] == '-' {
- end := strings.IndexByte(query[i:], '\n')
- if end < 0 {
- break
- }
- i += end + 1
- continue
- }
- if i+1 < len(query) && ch == '/' && query[i+1] == '*' {
- end := strings.Index(query[i+2:], "*/")
- if end < 0 {
- break
- }
- i += end + 4
- continue
- }
- // Brackets are ordinary syntax: a subscript such as tags[$1] binds $1.
- if ch == '\'' || ch == '"' || ch == '`' {
- quote = ch
- i++
- continue
- }
- if ch == '$' && i+1 < len(query) && query[i+1] >= '0' && query[i+1] <= '9' {
- end := i + 1
- for end < len(query) && query[end] >= '0' && query[end] <= '9' {
- end++
- }
- number, _ := strconv.Atoi(query[i+1 : end])
- references = append(references, parameterReference{number: number, start: i, end: end})
- i = end
- continue
- }
- i++
- }
- return references
- }
- // substituteParameterMarkers replaces each placeholder with a distinct string
- // literal and reports marker text -> parameter number.
- func substituteParameterMarkers(query string, references []parameterReference) (string, map[string]int) {
- var marked strings.Builder
- markers := make(map[string]int, len(references))
- last := 0
- for i, reference := range references {
- marker := markerPrefix + strconv.Itoa(i) + "__"
- markers[marker] = reference.number
- marked.WriteString(query[last:reference.start])
- marked.WriteString("'")
- marked.WriteString(marker)
- marked.WriteString("'")
- last = reference.end
- }
- marked.WriteString(query[last:])
- return marked.String(), markers
- }
- // parameterInferrer walks one parsed statement and records the type name each
- // parameter occurrence is compared with or assigned to. The first resolved
- // occurrence of a parameter wins.
- type parameterInferrer struct {
- conn *Connection
- markers map[string]int
- types map[int]string
- }
- func (in *parameterInferrer) note(marker, typeName string) {
- number, ok := in.markers[marker]
- if !ok || typeName == "" {
- return
- }
- if _, recorded := in.types[number]; recorded {
- return
- }
- in.types[number] = typeName
- }
- // parameterScope is one query level's table set. Column types come from the
- // storage catalog through the connection's session, so DDL staged by the
- // current transaction is visible.
- type parameterScope struct {
- parent *parameterScope
- tables []parameterTable
- }
- type parameterTable struct {
- name string
- alias string
- schema *storage.Schema
- }
- func (c *Connection) parameterTableSchema(name string) *storage.Schema {
- if name == "" {
- return nil
- }
- if c.executor != nil {
- if schema, err := c.executor.Session().GetSchema(name); err == nil {
- return schema
- }
- return nil
- }
- if c.schema != nil {
- if schema, err := c.schema.GetSchema(name); err == nil {
- return schema
- }
- }
- return nil
- }
- func (c *Connection) newParameterScope(parent *parameterScope, refs []parser.TableRef) *parameterScope {
- scope := ¶meterScope{parent: parent}
- for _, ref := range refs {
- scope.addTable(c, ref)
- }
- return scope
- }
- func (s *parameterScope) addTable(c *Connection, ref parser.TableRef) {
- if ref.Subquery == nil && ref.Function == nil {
- if schema := c.parameterTableSchema(ref.Name); schema != nil {
- s.tables = append(s.tables, parameterTable{name: ref.Name, alias: ref.Alias, schema: schema})
- }
- }
- if ref.Join != nil && ref.Join.Table != nil {
- s.addTable(c, *ref.Join.Table)
- }
- }
- // columnType resolves a column reference to its catalog type name. An
- // unqualified name is only used when exactly one table in the level has it; a
- // qualified name that names a table without the column stops the search rather
- // than falling through to an outer query.
- func (s *parameterScope) columnType(ref *parser.ColumnRef) (string, bool) {
- for scope := s; scope != nil; scope = scope.parent {
- var found string
- matches, matchedTable := 0, false
- for _, table := range scope.tables {
- if ref.Table != "" && !strings.EqualFold(table.name, ref.Table) && !strings.EqualFold(table.alias, ref.Table) {
- continue
- }
- matchedTable = true
- for _, column := range table.schema.Columns {
- if !strings.EqualFold(column.Name, ref.Column) {
- continue
- }
- matches++
- found = column.Type
- }
- }
- if matches > 0 {
- return found, matches == 1
- }
- if ref.Table != "" && matchedTable {
- return "", false
- }
- }
- return "", false
- }
- func tableRefName(ref *parser.TableRef) string {
- if ref == nil {
- return ""
- }
- return ref.Name
- }
- func schemaColumnType(schema *storage.Schema, name string) string {
- if schema == nil {
- return ""
- }
- for _, column := range schema.Columns {
- if strings.EqualFold(column.Name, name) {
- return column.Type
- }
- }
- return ""
- }
- func (in *parameterInferrer) walkStatement(scope *parameterScope, stmt parser.Statement) {
- switch s := stmt.(type) {
- case *parser.SelectStmt:
- in.walkSelect(scope, s)
- case *parser.CompoundSelect:
- in.walkSelect(scope, s.Left)
- in.walkSelect(scope, s.Right)
- in.walkExpr(scope, s.Limit, "")
- in.walkExpr(scope, s.Offset, "")
- case *parser.InsertStmt:
- in.walkInsert(scope, s)
- case *parser.UpdateStmt:
- in.walkUpdate(scope, s)
- case *parser.DeleteStmt:
- in.walkDelete(scope, s)
- }
- }
- func (in *parameterInferrer) walkSelect(parent *parameterScope, stmt *parser.SelectStmt) {
- if stmt == nil {
- return
- }
- for _, cte := range stmt.With {
- in.walkSelect(parent, cte.Query)
- }
- scope := in.conn.newParameterScope(parent, stmt.From)
- for _, column := range stmt.Columns {
- in.walkExpr(scope, column.Expr, "")
- }
- in.walkExpr(scope, stmt.Where, "")
- for _, expr := range stmt.GroupBy {
- in.walkExpr(scope, expr, "")
- }
- in.walkExpr(scope, stmt.Having, "")
- for _, item := range stmt.OrderBy {
- in.walkExpr(scope, item.Expr, "")
- }
- in.walkExpr(scope, stmt.Limit, "")
- in.walkExpr(scope, stmt.Offset, "")
- if stmt.Compound != nil {
- in.walkStatement(parent, stmt.Compound)
- }
- }
- func (in *parameterInferrer) walkInsert(parent *parameterScope, stmt *parser.InsertStmt) {
- var target []parser.TableRef
- if stmt.Table != nil {
- target = append(target, *stmt.Table)
- }
- schema := in.conn.parameterTableSchema(tableRefName(stmt.Table))
- scope := in.conn.newParameterScope(parent, target)
- // Without an explicit column list, VALUES positions map to the table's
- // columns in declaration order.
- columnTypes := make([]string, 0, len(stmt.Columns))
- if len(stmt.Columns) == 0 {
- if schema != nil {
- for _, column := range schema.Columns {
- // Generated columns are not writable and are skipped when
- // positional values map to the table's columns.
- if column.GeneratedExpr != "" {
- continue
- }
- columnTypes = append(columnTypes, column.Type)
- }
- }
- } else {
- for _, name := range stmt.Columns {
- columnTypes = append(columnTypes, schemaColumnType(schema, name))
- }
- }
- for _, row := range stmt.Values {
- for i, value := range row {
- expected := ""
- if i < len(columnTypes) {
- expected = columnTypes[i]
- }
- in.walkExpr(scope, value, expected)
- }
- }
- in.walkSelect(parent, stmt.Select)
- for _, assignment := range stmt.ConflictUpdate {
- in.walkExpr(scope, assignment.Value, schemaColumnType(schema, assignment.Column))
- }
- for _, column := range stmt.Returning {
- in.walkExpr(scope, column.Expr, "")
- }
- }
- func (in *parameterInferrer) walkUpdate(parent *parameterScope, stmt *parser.UpdateStmt) {
- var target []parser.TableRef
- if stmt.Table != nil {
- target = append(target, *stmt.Table)
- }
- schema := in.conn.parameterTableSchema(tableRefName(stmt.Table))
- scope := in.conn.newParameterScope(parent, append(target, stmt.From...))
- for _, assignment := range stmt.Set {
- in.walkExpr(scope, assignment.Value, schemaColumnType(schema, assignment.Column))
- }
- in.walkExpr(scope, stmt.Where, "")
- for _, column := range stmt.Returning {
- in.walkExpr(scope, column.Expr, "")
- }
- }
- func (in *parameterInferrer) walkDelete(parent *parameterScope, stmt *parser.DeleteStmt) {
- var target []parser.TableRef
- if stmt.Table != nil {
- target = append(target, *stmt.Table)
- }
- scope := in.conn.newParameterScope(parent, target)
- in.walkExpr(scope, stmt.Where, "")
- for _, column := range stmt.Returning {
- in.walkExpr(scope, column.Expr, "")
- }
- }
- // walkExpr visits expr with the type expected from its surrounding context.
- // Only the operands of comparison and containment operators, assignment
- // targets, and ANY/ALL operands supply a context; everything else recurses
- // without one.
- func (in *parameterInferrer) walkExpr(scope *parameterScope, expr parser.Expr, expected string) {
- switch e := expr.(type) {
- case nil:
- return
- case *parser.ParenExpr:
- in.walkExpr(scope, e.Expr, expected)
- case *parser.LiteralExpr:
- if e.Type == lexer.TokenString {
- in.note(e.Value, expected)
- }
- case *parser.CastExpr:
- if literal, ok := markerLiteral(e.Expr); ok {
- in.note(literal, e.Type.SQLTypeName())
- return
- }
- in.walkExpr(scope, e.Expr, "")
- case *parser.BinaryExpr:
- if isParameterTypeOperator(e.Op) {
- leftType, leftOK := in.exprType(scope, e.Left)
- rightType, rightOK := in.exprType(scope, e.Right)
- if leftOK {
- in.walkExpr(scope, e.Right, leftType)
- } else {
- in.walkExpr(scope, e.Right, "")
- }
- if rightOK {
- in.walkExpr(scope, e.Left, rightType)
- } else {
- in.walkExpr(scope, e.Left, "")
- }
- return
- }
- in.walkExpr(scope, e.Left, "")
- in.walkExpr(scope, e.Right, "")
- case *parser.AnyAllExpr:
- if columnType, ok := in.exprType(scope, e.Left); ok {
- in.walkExpr(scope, e.Array, parameterArrayType(columnType))
- } else {
- in.walkExpr(scope, e.Array, "")
- }
- in.walkExpr(scope, e.Left, "")
- case *parser.InExpr:
- columnType, ok := in.exprType(scope, e.Left)
- for _, value := range e.Values {
- if ok {
- in.walkExpr(scope, value, columnType)
- } else {
- in.walkExpr(scope, value, "")
- }
- }
- in.walkExpr(scope, e.Left, "")
- in.walkSelect(scope, e.Subquery)
- case *parser.BetweenExpr:
- columnType, _ := in.exprType(scope, e.Left)
- in.walkExpr(scope, e.Low, columnType)
- in.walkExpr(scope, e.High, columnType)
- in.walkExpr(scope, e.Left, "")
- case *parser.IsDistinctExpr:
- leftType, leftOK := in.exprType(scope, e.Left)
- rightType, rightOK := in.exprType(scope, e.Right)
- if leftOK {
- in.walkExpr(scope, e.Right, leftType)
- } else {
- in.walkExpr(scope, e.Right, "")
- }
- if rightOK {
- in.walkExpr(scope, e.Left, rightType)
- } else {
- in.walkExpr(scope, e.Left, "")
- }
- case *parser.IsNullExpr:
- in.walkExpr(scope, e.Left, "")
- case *parser.LikeExpr:
- in.walkExpr(scope, e.Left, "")
- in.walkExpr(scope, e.Pattern, "TEXT")
- in.walkExpr(scope, e.Escape, "TEXT")
- case *parser.UnaryExpr:
- in.walkExpr(scope, e.Operand, "")
- case *parser.FunctionCall:
- for _, arg := range e.Args {
- in.walkExpr(scope, arg, "")
- }
- case *parser.WindowExpr:
- if e.Func != nil {
- for _, arg := range e.Func.Args {
- in.walkExpr(scope, arg, "")
- }
- }
- for _, expr := range e.PartitionBy {
- in.walkExpr(scope, expr, "")
- }
- for _, item := range e.OrderBy {
- in.walkExpr(scope, item.Expr, "")
- }
- case *parser.CaseExpr:
- in.walkExpr(scope, e.Operand, "")
- for _, when := range e.Whens {
- in.walkExpr(scope, when.Condition, "")
- in.walkExpr(scope, when.Result, expected)
- }
- in.walkExpr(scope, e.Else, expected)
- case *parser.ArrayExpr:
- for _, element := range e.Elements {
- in.walkExpr(scope, element, "")
- }
- case *parser.SubscriptExpr:
- in.walkExpr(scope, e.Expr, "")
- // An array subscript is an integer position regardless of the array's
- // element type.
- in.walkExpr(scope, e.Index, "INTEGER")
- case *parser.SubqueryExpr:
- in.walkSelect(scope, e.Query)
- case *parser.ExistsExpr:
- in.walkSelect(scope, e.Subquery)
- }
- }
- // exprType reports the type of an expression that names one directly: a column
- // from the catalog or an explicit cast.
- func (in *parameterInferrer) exprType(scope *parameterScope, expr parser.Expr) (string, bool) {
- switch e := expr.(type) {
- case *parser.ParenExpr:
- return in.exprType(scope, e.Expr)
- case *parser.ColumnRef:
- if scope == nil {
- return "", false
- }
- return scope.columnType(e)
- case *parser.CastExpr:
- return e.Type.SQLTypeName(), true
- }
- return "", false
- }
- // markerLiteral reports the parameter marker text when expr is a marker,
- // looking through parentheses.
- func markerLiteral(expr parser.Expr) (string, bool) {
- for {
- paren, ok := expr.(*parser.ParenExpr)
- if !ok {
- break
- }
- expr = paren.Expr
- }
- literal, ok := expr.(*parser.LiteralExpr)
- if !ok || literal.Type != lexer.TokenString || !strings.HasPrefix(literal.Value, markerPrefix) {
- return "", false
- }
- return literal.Value, true
- }
- // isParameterTypeOperator reports whether an operator's operands share a type,
- // so one side can supply the other side's parameter type.
- func isParameterTypeOperator(op lexer.TokenType) bool {
- switch op {
- case lexer.TokenEq, lexer.TokenNeq, lexer.TokenLt, lexer.TokenLte,
- lexer.TokenGt, lexer.TokenGte, lexer.TokenAtArrow, lexer.TokenLtAt:
- return true
- }
- return false
- }
- // parameterArrayType returns the array type of a column's element type. An
- // array column keeps its declared array type: ANY/ALL over an array column is
- // the engine's supported form.
- func parameterArrayType(columnType string) string {
- if _, isArray := arrayElementTypeName(columnType); isArray {
- return columnType
- }
- return columnType + "[]"
- }
|