| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399 |
- package executor
- import (
- "fmt"
- "sort"
- "strconv"
- "strings"
- "github.com/danfragoso/pizzasql-next/pkg/parser"
- "github.com/danfragoso/pizzasql-next/pkg/storage"
- )
- // SQLite catalog-introspection support for xorm.io/xorm v0.8.0 and
- // github.com/glebarez/sqlite v1.11.0. These drivers read the virtual
- // sqlite_master / sqlite_schema table and the index_list / index_info /
- // table_xinfo pragmas. Rows are synthesized from the durable PizzaSQL schema;
- // nothing is persisted.
- var sqliteCatalogTables = map[string]bool{
- "sqlite_master": true,
- "sqlite_schema": true,
- }
- func sqliteCatalogSchema(tableName string) *storage.Schema {
- return &storage.Schema{
- Name: tableName,
- Columns: []storage.Column{
- {Name: "type", Type: "TEXT", Nullable: true},
- {Name: "name", Type: "TEXT", Nullable: true},
- {Name: "tbl_name", Type: "TEXT", Nullable: true},
- {Name: "rootpage", Type: "INTEGER", Nullable: true},
- {Name: "sql", Type: "TEXT", Nullable: true},
- },
- }
- }
- func isSQLiteCatalogTable(name string) bool {
- return sqliteCatalogTables[strings.ToLower(name)]
- }
- // sqliteCatalogSelect answers a single-table SELECT against sqlite_master /
- // sqlite_schema. handled is false for any other shape so the caller falls
- // through to the normal SELECT path.
- func (e *Executor) sqliteCatalogSelect(stmt *parser.SelectStmt) (*Result, bool, error) {
- if stmt == nil || stmt.Compound != nil || len(stmt.From) != 1 {
- return nil, false, nil
- }
- ref := stmt.From[0]
- if ref.Subquery != nil || ref.Join != nil || !isSQLiteCatalogTable(ref.Name) {
- return nil, false, nil
- }
- rows, err := e.sqliteCatalogRows()
- if err != nil {
- return nil, true, err
- }
- if ref.Alias != "" {
- for i := range rows {
- rows[i] = e.addTableAlias(rows[i], ref.Alias)
- }
- }
- result, err := e.executeSelectOnRows(stmt, rows, sqliteCatalogSchema(ref.Name))
- if err != nil {
- return nil, true, err
- }
- return result, true, nil
- }
- // sqliteCatalogRows materializes the sqlite_master row set: one "table" row per
- // user table (with recreated CREATE TABLE SQL) and one "index" row per index
- // (with recreated CREATE INDEX SQL), deterministically ordered.
- func (e *Executor) sqliteCatalogRows() ([]storage.Row, error) {
- var rows []storage.Row
- tables, err := e.schema.ListTables()
- if err != nil {
- return nil, err
- }
- tableNames := append([]string(nil), tables...)
- sort.Strings(tableNames)
- for _, name := range tableNames {
- schema, err := e.schema.GetSchema(name)
- if err != nil {
- return nil, fmt.Errorf("sqlite_catalog: resolve table %q: %w", name, err)
- }
- rows = append(rows, storage.Row{
- "type": "table",
- "name": schema.Name,
- "tbl_name": schema.Name,
- "rootpage": int64(0),
- "sql": recreateCreateTableSQL(schema),
- })
- }
- indexes, err := e.schema.ListIndexes()
- if err != nil {
- return nil, err
- }
- indexNames := append([]string(nil), indexes...)
- sort.Strings(indexNames)
- for _, name := range indexNames {
- idx, err := e.schema.GetIndex(name)
- if err != nil {
- return nil, fmt.Errorf("sqlite_catalog: resolve index %q: %w", name, err)
- }
- rows = append(rows, storage.Row{
- "type": "index",
- "name": idx.Name,
- "tbl_name": idx.Table,
- "rootpage": int64(0),
- "sql": recreateCreateIndexSQL(idx),
- })
- }
- return rows, nil
- }
- // executeSelectOnRows runs the shared SELECT tail (WHERE, GROUP BY, aggregates,
- // ORDER BY/LIMIT/OFFSET, projection, DISTINCT) over an in-memory row set.
- func (e *Executor) executeSelectOnRows(stmt *parser.SelectStmt, rows []storage.Row, schema *storage.Schema) (*Result, error) {
- if stmt.Where != nil {
- filtered := make([]storage.Row, 0, len(rows))
- for _, row := range rows {
- val, err := e.evalExpr(stmt.Where, row)
- if err != nil {
- return nil, err
- }
- if toBool(val) {
- filtered = append(filtered, row)
- }
- }
- rows = filtered
- }
- if len(stmt.GroupBy) > 0 {
- return e.executeGroupBy(stmt, rows, schema)
- }
- if e.hasAggregates(stmt.Columns) {
- return e.executeAggregateSelect(stmt, rows, schema)
- }
- rows = e.orderAndLimitRows(rows, stmt.OrderBy, stmt.Limit, stmt.Offset, stmt.Columns)
- result := NewResult("SELECT")
- for i, col := range stmt.Columns {
- switch {
- case col.Alias != "":
- result.AddColumn(col.Alias)
- case col.Star:
- for _, c := range schema.Columns {
- result.AddColumn(c.Name)
- }
- default:
- if ref, ok := col.Expr.(*parser.ColumnRef); ok {
- result.AddColumn(ref.Column)
- } else {
- result.AddColumn(fmt.Sprintf("column%d", i+1))
- }
- }
- }
- for _, row := range rows {
- values := make([]interface{}, 0, len(stmt.Columns))
- for _, col := range stmt.Columns {
- if col.Star {
- for _, c := range schema.Columns {
- values = append(values, row[c.Name])
- }
- } else {
- val, err := e.evalExpr(col.Expr, row)
- if err != nil {
- return nil, err
- }
- values = append(values, val)
- }
- }
- result.AddRow(values...)
- }
- if stmt.Distinct {
- result.Rows = e.applyDistinct(result.Rows)
- }
- return result, nil
- }
- // sqliteCatalogPragma answers the introspection pragmas (index_list, index_info,
- // table_xinfo). handled is false for everything else so the built-in handler runs.
- func (e *Executor) sqliteCatalogPragma(stmt *parser.PragmaStmt) (*Result, bool, error) {
- if stmt == nil {
- return nil, false, nil
- }
- switch strings.ToLower(stmt.Name) {
- case "index_list", "index_info", "table_xinfo":
- res, err := e.executeCatalogPragma(stmt)
- return res, true, err
- default:
- return nil, false, nil
- }
- }
- func (e *Executor) executeCatalogPragma(stmt *parser.PragmaStmt) (*Result, error) {
- switch strings.ToLower(stmt.Name) {
- case "index_list":
- return e.pragmaIndexList(stmt.Arg)
- case "index_info":
- return e.pragmaIndexInfo(stmt.Arg)
- case "table_xinfo":
- return e.pragmaTableXInfo(stmt.Arg)
- default:
- return nil, fmt.Errorf("unknown pragma: %s", stmt.Name)
- }
- }
- // pragmaIndexList returns PRAGMA index_list(table): seq, name, unique, origin,
- // partial. PizzaSQL only creates indexes via CREATE INDEX, so origin is "c".
- func (e *Executor) pragmaIndexList(table string) (*Result, error) {
- if table == "" {
- return nil, fmt.Errorf("index_list requires a table name")
- }
- indexes, err := e.schema.ListTableIndexes(table)
- if err != nil {
- return nil, err
- }
- sort.Slice(indexes, func(i, j int) bool { return indexes[i].Name < indexes[j].Name })
- result := NewResult("PRAGMA")
- for _, c := range []string{"seq", "name", "unique", "origin", "partial"} {
- result.AddColumn(c)
- }
- for i, idx := range indexes {
- unique := int64(0)
- if idx.Unique {
- unique = 1
- }
- result.AddRow(int64(i), idx.Name, unique, "c", int64(0))
- }
- return result, nil
- }
- // pragmaIndexInfo returns PRAGMA index_info(index): seqno, cid, name.
- func (e *Executor) pragmaIndexInfo(name string) (*Result, error) {
- if name == "" {
- return nil, fmt.Errorf("index_info requires an index name")
- }
- idx, err := e.schema.GetIndex(name)
- if err != nil {
- return nil, err
- }
- schema, err := e.schema.GetSchema(idx.Table)
- if err != nil {
- return nil, err
- }
- result := NewResult("PRAGMA")
- for _, c := range []string{"seqno", "cid", "name"} {
- result.AddColumn(c)
- }
- for seqno, ic := range idx.Columns {
- cid := int64(-1)
- for i, c := range schema.Columns {
- if strings.EqualFold(c.Name, ic.Name) {
- cid = int64(i)
- break
- }
- }
- result.AddRow(int64(seqno), cid, ic.Name)
- }
- return result, nil
- }
- // pragmaTableXInfo returns PRAGMA table_xinfo(table): table_info columns plus a
- // trailing hidden flag (always 0).
- func (e *Executor) pragmaTableXInfo(table string) (*Result, error) {
- if table == "" {
- return nil, fmt.Errorf("table_xinfo requires a table name")
- }
- schema, err := e.schema.GetSchema(table)
- if err != nil {
- return nil, err
- }
- result := NewResult("PRAGMA")
- for _, c := range []string{"cid", "name", "type", "notnull", "dflt_value", "pk", "hidden"} {
- result.AddColumn(c)
- }
- for i, col := range schema.Columns {
- notnull := int64(0)
- if !col.Nullable {
- notnull = 1
- }
- pk := int64(0)
- if col.PrimaryKey {
- pk = 1
- }
- // Hidden flag: 0 normal, 2 generated VIRTUAL, 3 generated STORED.
- hidden := int64(0)
- if col.GeneratedExpr != "" {
- if col.GeneratedStored {
- hidden = 3
- } else {
- hidden = 2
- }
- }
- result.AddRow(int64(i), col.Name, col.Type, notnull, col.Default, pk, hidden)
- }
- return result, nil
- }
- // recreateCreateTableSQL rebuilds CREATE TABLE from the durable schema. The
- // implicit _rowid_ (and its aliases) are stripped, and identifiers are quoted so
- // xorm's IsColumnExist / GORM's HasColumn LIKE patterns match.
- func recreateCreateTableSQL(s *storage.Schema) string {
- cols := make([]string, 0, len(s.Columns))
- for _, col := range s.Columns {
- // Strip only the engine-injected hidden rowid column, never user
- // columns that happen to be named oid/rowid/_rowid_.
- if s.PrimaryKey == "_rowid_" && col.Name == "_rowid_" {
- continue
- }
- cols = append(cols, recreateColumnDef(s, col))
- }
- return fmt.Sprintf("CREATE TABLE %s (%s)", quoteIdent(s.Name), strings.Join(cols, ", "))
- }
- func recreateColumnDef(s *storage.Schema, col storage.Column) string {
- var b strings.Builder
- b.WriteString(quoteIdent(col.Name))
- b.WriteString(" ")
- b.WriteString(col.Type)
- if col.PrimaryKey {
- b.WriteString(" PRIMARY KEY")
- }
- if col.PrimaryKey && s.AutoIncrement {
- b.WriteString(" AUTOINCREMENT")
- }
- if !col.Nullable {
- b.WriteString(" NOT NULL")
- }
- if col.Default != nil {
- b.WriteString(" DEFAULT ")
- b.WriteString(sqlLiteral(col.Default))
- }
- if col.GeneratedExpr != "" {
- b.WriteString(" GENERATED ALWAYS AS (")
- b.WriteString(col.GeneratedExpr)
- b.WriteString(")")
- if col.GeneratedStored {
- b.WriteString(" STORED")
- } else {
- b.WriteString(" VIRTUAL")
- }
- }
- return b.String()
- }
- func recreateCreateIndexSQL(idx *storage.Index) string {
- unique := ""
- if idx.Unique {
- unique = "UNIQUE "
- }
- cols := make([]string, 0, len(idx.Columns))
- for _, c := range idx.Columns {
- col := quoteIdent(c.Name)
- if c.Desc {
- col += " DESC"
- }
- cols = append(cols, col)
- }
- return fmt.Sprintf("CREATE %sINDEX %s ON %s (%s)", unique, quoteIdent(idx.Name), quoteIdent(idx.Table), strings.Join(cols, ", "))
- }
- // quoteIdent backtick-quotes an identifier, doubling embedded backticks.
- func quoteIdent(name string) string {
- return "`" + strings.ReplaceAll(name, "`", "``") + "`"
- }
- func sqlLiteral(v interface{}) string {
- switch t := v.(type) {
- case nil:
- return "NULL"
- case string:
- return "'" + strings.ReplaceAll(t, "'", "''") + "'"
- case bool:
- if t {
- return "1"
- }
- return "0"
- case int:
- return strconv.Itoa(t)
- case int64:
- return strconv.FormatInt(t, 10)
- case float64:
- return strconv.FormatFloat(t, 'g', -1, 64)
- default:
- return "'" + strings.ReplaceAll(fmt.Sprintf("%v", v), "'", "''") + "'"
- }
- }
|