Files
upstream-mirror 0f4df23290
Some checks failed
kotlin / test (push) Has been skipped
python / test (push) Has been skipped
typescript / test (push) Has been skipped
go / build darwin/amd64 (push) Has been cancelled
go / build linux/amd64 (push) Has been cancelled
go / build windows/amd64 (push) Has been cancelled
go / build darwin/arm64 (push) Has been cancelled
go / build linux/arm64 (push) Has been cancelled
go / build windows/arm64 (push) Has been cancelled
go / test (push) Has been cancelled
go / vuln_check (push) Has been cancelled
snapshot of https://github.com/sqlc-dev/sqlc.git (main, 2026-09-16; blobs > 131072B omitted)
2026-09-16 02:17:34 +05:00

234 lines
5.4 KiB
Go

package analyzer
import (
"context"
"fmt"
"strings"
"sync"
"github.com/ncruces/go-sqlite3"
core "github.com/sqlc-dev/sqlc/internal/analysis"
"github.com/sqlc-dev/sqlc/internal/config"
"github.com/sqlc-dev/sqlc/internal/shfmt"
"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/named"
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
"github.com/sqlc-dev/sqlc/internal/sqlcdebug"
_ "github.com/sqlc-dev/sqlc/internal/sqlite3ext"
)
var debugDatabases = sqlcdebug.New("databases")
type Analyzer struct {
db config.Database
conn *sqlite3.Conn
replacer *shfmt.Replacer
mu sync.Mutex
}
func New(db config.Database) *Analyzer {
return &Analyzer{
db: db,
replacer: shfmt.NewReplacer(nil),
}
}
func (a *Analyzer) Analyze(ctx context.Context, n ast.Node, query string, migrations []string, ps *named.ParamSet) (*core.Analysis, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.conn == nil {
var uri string
applyMigrations := a.db.Managed
if a.db.Managed {
// For managed databases, create an in-memory database
uri = ":memory:"
} else if debugDatabases.Value() == "managed" {
return nil, fmt.Errorf("database: connections disabled via SQLCDEBUG=databases=managed")
} else {
uri = a.replacer.Replace(a.db.URI)
// For in-memory databases, we need to apply migrations since the database starts empty
if isInMemoryDatabase(uri) {
applyMigrations = true
}
}
conn, err := sqlite3.Open(uri)
if err != nil {
return nil, fmt.Errorf("failed to open sqlite database: %w", err)
}
a.conn = conn
// Apply migrations for managed or in-memory databases
if applyMigrations {
for _, m := range migrations {
if len(strings.TrimSpace(m)) == 0 {
continue
}
if err := a.conn.Exec(m); err != nil {
a.conn.Close()
a.conn = nil
return nil, fmt.Errorf("migration failed: %s: %w", m, err)
}
}
}
}
// Prepare the statement to get column and parameter information
stmt, _, err := a.conn.Prepare(query)
if err != nil {
return nil, a.extractSqlErr(n, err)
}
defer stmt.Close()
var result core.Analysis
// Get column information
colCount := stmt.ColumnCount()
for i := range colCount {
name := stmt.ColumnName(i)
declType := stmt.ColumnDeclType(i)
dbName := stmt.ColumnDatabaseName(i)
tableName := stmt.ColumnTableName(i)
originName := stmt.ColumnOriginName(i)
// Determine if column is NOT NULL
var notNull bool
var dataType string
if originName != "" {
declType, _, notNull, _, _, _ = a.conn.TableColumnMetadata(
dbName, tableName, originName)
}
// Normalize the data type
dataType = normalizeType(declType)
col := &core.Column{
Name: name,
OriginalName: originName,
DataType: dataType,
NotNull: notNull,
}
if tableName != "" {
col.Table = &core.Identifier{
Schema: dbName,
Name: tableName,
}
}
result.Columns = append(result.Columns, col)
}
// Get parameter information
bindCount := stmt.BindCount()
for i := 1; i <= bindCount; i++ {
paramName := stmt.BindName(i)
// SQLite doesn't provide parameter types from prepared statements
// We use "any" as the default type
name := ""
if paramName != "" {
// Remove the prefix (?, :, @, $) from parameter names
name = strings.TrimLeft(paramName, "?:@$")
}
if ps != nil {
if n, ok := ps.NameFor(i); ok {
name = n
}
}
result.Params = append(result.Params, &core.Parameter{
Number: int32(i),
Column: &core.Column{
Name: name,
DataType: "any",
NotNull: false,
},
})
}
return &result, nil
}
func (a *Analyzer) extractSqlErr(n ast.Node, err error) error {
if err == nil {
return nil
}
// Try to extract SQLite error details
var sqliteErr *sqlite3.Error
if e, ok := err.(*sqlite3.Error); ok {
sqliteErr = e
}
if sqliteErr != nil {
return &sqlerr.Error{
Code: fmt.Sprintf("%d", sqliteErr.Code()),
Message: sqliteErr.Error(),
Location: n.Pos(),
}
}
return &sqlerr.Error{
Message: err.Error(),
Location: n.Pos(),
}
}
func (a *Analyzer) Close(_ context.Context) error {
a.mu.Lock()
defer a.mu.Unlock()
if a.conn != nil {
err := a.conn.Close()
a.conn = nil
return err
}
return nil
}
// isInMemoryDatabase checks if a SQLite URI refers to an in-memory database
func isInMemoryDatabase(uri string) bool {
if uri == ":memory:" || uri == "" {
return true
}
// Check for file URI with mode=memory parameter
// e.g., "file:test?mode=memory&cache=shared"
if strings.Contains(uri, "mode=memory") {
return true
}
return false
}
// normalizeType converts SQLite type declarations to standard type names
func normalizeType(declType string) string {
if declType == "" {
return "any"
}
// Convert to lowercase for comparison
lower := strings.ToLower(declType)
// SQLite type affinity rules (https://www.sqlite.org/datatype3.html)
switch {
case strings.Contains(lower, "int"):
return "integer"
case strings.Contains(lower, "char"),
strings.Contains(lower, "clob"),
strings.Contains(lower, "text"):
return "text"
case strings.Contains(lower, "blob"):
return "blob"
case strings.Contains(lower, "real"),
strings.Contains(lower, "floa"),
strings.Contains(lower, "doub"):
return "real"
case strings.Contains(lower, "bool"):
return "boolean"
case strings.Contains(lower, "date"),
strings.Contains(lower, "time"):
return "datetime"
default:
// Return as-is for numeric or other types
return lower
}
}