Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions internal/cmd/shim.go
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@ func pluginQueries(r *compiler.Result) []*plugin.Query {
Params: params,
Filename: q.Metadata.Filename,
InsertIntoTable: iit,
SourceTables: q.SourceTables,
})
}
return out
Expand Down
1 change: 1 addition & 0 deletions internal/compiler/parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ func (c *Compiler) parseQuery(stmt ast.Node, src string, o opts.Parser) (*Query,
Columns: anlys.Columns,
SQL: trimmed,
InsertIntoTable: anlys.Table,
SourceTables: sourceTableNames(raw.Stmt),
}, nil
}

Expand Down
7 changes: 7 additions & 0 deletions internal/compiler/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,13 @@ type Query struct {
Columns []*Column
Params []Parameter

// SourceTables lists the base tables the query reads from, including tables
// that appear only in joins, subqueries, or common table expression bodies.
// Names are schema-qualified when a schema is present, deduplicated, and
// sorted. Common table expression names and the target relations of write
// statements are excluded.
SourceTables []string

// Needed for CopyFrom
InsertIntoTable *ast.TableName

Expand Down
90 changes: 90 additions & 0 deletions internal/compiler/source_tables.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
package compiler

import (
"sort"
"strings"

"github.com/sqlc-dev/sqlc/internal/sql/ast"
"github.com/sqlc-dev/sqlc/internal/sql/astutils"
)

// sourceTableNames returns the sorted, deduplicated names of the base tables a
// statement reads from. It covers every table referenced in a FROM, a JOIN, or
// a subquery in any clause, including the bodies of common table expressions.
// The names of common table expressions and the target relations of INSERT,
// UPDATE, DELETE, and TRUNCATE statements are not reads and are excluded.
func sourceTableNames(root ast.Node) []string {
cteNames := map[string]struct{}{}
writeTargets := map[*ast.RangeVar]struct{}{}

collect := astutils.VisitorFunc(func(node ast.Node) {
switch n := node.(type) {
case *ast.CommonTableExpr:
if n.Ctename != nil {
cteNames[*n.Ctename] = struct{}{}
}
case *ast.InsertStmt:
if n.Relation != nil {
markRangeVars(writeTargets, n.Relation)
}
case *ast.UpdateStmt:
if n.Relations != nil {
markRangeVars(writeTargets, n.Relations)
}
case *ast.DeleteStmt:
if n.Relations != nil {
markRangeVars(writeTargets, n.Relations)
}
case *ast.TruncateStmt:
if n.Relations != nil {
markRangeVars(writeTargets, n.Relations)
}
}
})
astutils.Walk(collect, root)

seen := map[string]struct{}{}
names := []string{}
for _, rv := range rangeVars(root) {
if _, ok := writeTargets[rv]; ok {
continue
}
table, err := ParseTableName(rv)
if err != nil {
continue
}
Comment thread
Prateeks16 marked this conversation as resolved.
if _, ok := cteNames[table.Name]; ok {
continue
}
name := qualifiedName(table)
if _, ok := seen[name]; ok {
continue
}
seen[name] = struct{}{}
names = append(names, name)
}
Comment thread
Prateeks16 marked this conversation as resolved.
sort.Strings(names)
return names
Comment thread
Prateeks16 marked this conversation as resolved.
}

// qualifiedName joins a table's catalog, schema, and name with dots, omitting
// the parts that are empty. A table referenced without a schema is reported by
// its bare name; one referenced with a schema keeps the schema so tables of the
// same name in different schemas stay distinct.
func qualifiedName(tn *ast.TableName) string {
parts := make([]string, 0, 3)
if tn.Catalog != "" {
parts = append(parts, tn.Catalog)
}
if tn.Schema != "" {
parts = append(parts, tn.Schema)
}
parts = append(parts, tn.Name)
return strings.Join(parts, ".")
}

func markRangeVars(set map[*ast.RangeVar]struct{}, node ast.Node) {
for _, rv := range rangeVars(node) {
set[rv] = struct{}{}
}
}
65 changes: 65 additions & 0 deletions internal/compiler/source_tables_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package compiler

import (
"reflect"
"strings"
"testing"

"github.com/sqlc-dev/sqlc/internal/engine/postgresql"
)

func TestSourceTableNames(t *testing.T) {
for _, tc := range []struct {
name string
sql string
want []string
}{
{
name: "cte, join and subquery dependencies",
sql: `WITH filtered_accounts AS (
SELECT account_id FROM accounts WHERE accounts.space_id = $1
AND NOT EXISTS (
SELECT 1 FROM account_tags t WHERE t.account_id = accounts.account_id
)
)
SELECT acc.* FROM accounts acc
JOIN filtered_accounts fa ON acc.account_id = fa.account_id
LEFT JOIN transactions t ON t.debit_account_id = acc.account_id`,
want: []string{"account_tags", "accounts", "transactions"},
},
{
name: "deduplicated across aliases",
sql: `SELECT a1.account_id FROM accounts a1 JOIN accounts a2 ON a1.space_id = a2.space_id`,
want: []string{"accounts"},
},
{
name: "insert excludes write target, includes read",
sql: `INSERT INTO audit_log (account_id) SELECT account_id FROM accounts`,
want: []string{"accounts"},
},
{
name: "schema-qualified tables stay distinct",
sql: `SELECT 1 FROM audit.accounts JOIN accounts ON true`,
want: []string{"accounts", "audit.accounts"},
},
{
name: "no base tables",
sql: `SELECT 1`,
want: []string{},
},
} {
t.Run(tc.name, func(t *testing.T) {
stmts, err := postgresql.NewParser().Parse(strings.NewReader(tc.sql + ";"))
if err != nil {
t.Fatalf("parse: %v", err)
}
if len(stmts) != 1 {
t.Fatalf("expected 1 statement, got %d", len(stmts))
}
got := sourceTableNames(stmts[0].Raw.Stmt)
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("sourceTableNames\n got: %v\n want: %v", got, tc.want)
}
})
}
}
16 changes: 12 additions & 4 deletions internal/endtoend/testdata/codegen_json/gen/codegen.json
Original file line number Diff line number Diff line change
Expand Up @@ -65079,7 +65079,10 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": [
"authors"
]
},
{
"text": "SELECT id, name, bio FROM authors\nORDER BY name",
Expand Down Expand Up @@ -65168,7 +65171,10 @@
"params": [],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": [
"authors"
]
},
{
"text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio",
Expand Down Expand Up @@ -65320,7 +65326,8 @@
"catalog": "",
"schema": "",
"name": "authors"
}
},
"source_tables": []
},
{
"text": "DELETE FROM authors\nWHERE id = $1",
Expand Down Expand Up @@ -65360,7 +65367,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": []
}
],
"sqlc_version": "v1.31.1",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65081,7 +65081,10 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": [
"authors"
]
},
{
"text": "SELECT id, name, bio FROM authors\nORDER BY name",
Expand Down Expand Up @@ -65170,7 +65173,10 @@
"params": [],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": [
"authors"
]
},
{
"text": "INSERT INTO authors (\n name, bio\n) VALUES (\n $1, $2\n)\nRETURNING id, name, bio",
Expand Down Expand Up @@ -65322,7 +65328,8 @@
"catalog": "",
"schema": "",
"name": "authors"
}
},
"source_tables": []
},
{
"text": "DELETE FROM authors\nWHERE id = $1",
Expand Down Expand Up @@ -65362,7 +65369,8 @@
],
"comments": [],
"filename": "query.sql",
"insert_into_table": null
"insert_into_table": null,
"source_tables": []
}
],
"sqlc_version": "v1.31.1",
Expand Down
91 changes: 51 additions & 40 deletions internal/plugin/codegen.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading