Skip to content

Commit a0c474f

Browse files
committed
compiler/sqlite: validate qualified column references across scopes
Signed-off-by: Amirhossein Akhlaghpour <[email protected]>
1 parent 67e865b commit a0c474f

File tree

6 files changed

+311
-0
lines changed

6 files changed

+311
-0
lines changed

internal/compiler/analyze.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,12 @@ func (c *Compiler) _analyzeQuery(raw *ast.RawStmt, query string, failfast bool)
181181
return nil, err
182182
}
183183

184+
if c.conf.Engine == config.EngineSQLite {
185+
if err := validateSQLiteQualifiedColumnRefs(raw.Stmt); err != nil {
186+
return nil, check(err)
187+
}
188+
}
189+
184190
params, err := c.resolveCatalogRefs(qc, rvs, refs, namedParams, embeds)
185191
if err := check(err); err != nil {
186192
return nil, err

internal/compiler/validator.go

Lines changed: 262 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,262 @@
1+
package compiler
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/sqlc-dev/sqlc/internal/sql/ast"
8+
"github.com/sqlc-dev/sqlc/internal/sql/sqlerr"
9+
)
10+
11+
/*
12+
This file implements SQLite-specific validation for qualified column references.
13+
14+
Problem:
15+
SQLite allows invalid correlated references to pass parsing, e.g.
16+
17+
SELECT *
18+
FROM locations l
19+
WHERE EXISTS (
20+
SELECT 1
21+
FROM projects p
22+
WHERE p.id = location.project_id -- invalid: "location" not in scope
23+
)
24+
25+
SQLite itself errors at runtime, but sqlc historically accepted this query.
26+
This validator rejects such queries during compilation, matching SQLite behavior.
27+
*/
28+
29+
// scope represents the set of visible table names and aliases at a SELECT level.
30+
// parent enables correlated subqueries to see outer query tables.
31+
type scope struct {
32+
parent *scope
33+
names map[string]struct{}
34+
}
35+
36+
func newScope(parent *scope) *scope {
37+
return &scope{
38+
parent: parent,
39+
names: map[string]struct{}{},
40+
}
41+
}
42+
43+
// add registers a visible table name or alias.
44+
func (s *scope) add(name string) {
45+
if name == "" {
46+
return
47+
}
48+
s.names[name] = struct{}{}
49+
}
50+
51+
// has checks whether a name is visible in this scope or any parent scope.
52+
func (s *scope) has(name string) bool {
53+
for cur := s; cur != nil; cur = cur.parent {
54+
if _, ok := cur.names[name]; ok {
55+
return true
56+
}
57+
}
58+
return false
59+
}
60+
61+
// qualifierFromColumnRef extracts the table/alias portion of a qualified ref.
62+
//
63+
// Examples:
64+
//
65+
// a.b -> "a"
66+
// s.a.b -> "a" (schema.table.column)
67+
// b -> "" (unqualified)
68+
func qualifierFromColumnRef(ref *ast.ColumnRef) (string, bool) {
69+
if ref == nil || ref.Fields == nil {
70+
return "", false
71+
}
72+
items := stringSlice(ref.Fields)
73+
switch len(items) {
74+
case 2:
75+
return items[0], true
76+
case 3:
77+
return items[1], true
78+
default:
79+
return "", false
80+
}
81+
}
82+
83+
// addFromItemToScope records tables and aliases introduced by FROM/JOIN items.
84+
func addFromItemToScope(sc *scope, n ast.Node) {
85+
switch t := n.(type) {
86+
case *ast.RangeVar:
87+
if t.Relname != nil {
88+
sc.add(*t.Relname)
89+
}
90+
if t.Alias != nil && t.Alias.Aliasname != nil {
91+
sc.add(*t.Alias.Aliasname)
92+
}
93+
94+
case *ast.JoinExpr:
95+
addFromItemToScope(sc, t.Larg)
96+
addFromItemToScope(sc, t.Rarg)
97+
98+
case *ast.RangeSubselect:
99+
if t.Alias != nil && t.Alias.Aliasname != nil {
100+
sc.add(*t.Alias.Aliasname)
101+
}
102+
103+
case *ast.RangeFunction:
104+
if t.Alias != nil && t.Alias.Aliasname != nil {
105+
sc.add(*t.Alias.Aliasname)
106+
}
107+
}
108+
}
109+
110+
// validateSQLiteQualifiedColumnRefs is the public entry point.
111+
// It validates that qualified column references only use visible tables/aliases.
112+
func validateSQLiteQualifiedColumnRefs(root ast.Node) error {
113+
return validateNodeSQLite(root, nil)
114+
}
115+
116+
// validateNodeSQLite validates a SELECT node and establishes a new scope.
117+
// Nested SELECTs receive the current scope as their parent.
118+
func validateNodeSQLite(node ast.Node, parent *scope) error {
119+
switch n := node.(type) {
120+
case *ast.SelectStmt:
121+
sc := newScope(parent)
122+
123+
// Collect visible names from FROM clause
124+
if n.FromClause != nil {
125+
for _, item := range n.FromClause.Items {
126+
addFromItemToScope(sc, item)
127+
}
128+
}
129+
130+
// Walk this SELECT subtree with the new scope
131+
return walkSQLite(n, sc)
132+
133+
default:
134+
// Only SELECTs introduce scopes; other nodes are irrelevant here.
135+
return nil
136+
}
137+
}
138+
139+
// walkSQLite recursively walks an AST node, validating ColumnRef qualifiers.
140+
func walkSQLite(node ast.Node, sc *scope) error {
141+
if node == nil {
142+
return nil
143+
}
144+
145+
// Pre-order validation: check ColumnRef immediately
146+
if ref, ok := node.(*ast.ColumnRef); ok {
147+
if qual, ok := qualifierFromColumnRef(ref); ok && !sc.has(qual) {
148+
return &sqlerr.Error{
149+
Code: "42703",
150+
Message: fmt.Sprintf("table alias %q does not exist", qual),
151+
Location: ref.Location,
152+
}
153+
}
154+
}
155+
156+
// Explicit handling of subquery boundaries
157+
switch n := node.(type) {
158+
case *ast.SubLink:
159+
if n.Subselect != nil {
160+
return validateNodeSQLite(n.Subselect, sc)
161+
}
162+
return nil
163+
164+
case *ast.RangeSubselect:
165+
if n.Subquery != nil {
166+
return validateNodeSQLite(n.Subquery, sc)
167+
}
168+
return nil
169+
}
170+
171+
// Generic recursion for all other node types
172+
return walkSQLiteReflect(node, sc)
173+
}
174+
175+
// walkSQLiteReflect traverses AST nodes via reflection.
176+
// This avoids dependency on astutils.Walk, whose signature varies.
177+
func walkSQLiteReflect(node ast.Node, sc *scope) error {
178+
v := reflect.ValueOf(node)
179+
if v.Kind() == reflect.Pointer {
180+
if v.IsNil() {
181+
return nil
182+
}
183+
v = v.Elem()
184+
}
185+
if v.Kind() != reflect.Struct {
186+
return nil
187+
}
188+
189+
t := v.Type()
190+
for i := 0; i < v.NumField(); i++ {
191+
// Skip unexported fields
192+
if t.Field(i).PkgPath != "" {
193+
continue
194+
}
195+
196+
f := v.Field(i)
197+
if !f.IsValid() {
198+
continue
199+
}
200+
201+
// Dereference pointers
202+
for f.Kind() == reflect.Pointer {
203+
if f.IsNil() {
204+
goto next
205+
}
206+
f = f.Elem()
207+
}
208+
209+
// Handle ast.List
210+
if f.Type() == reflect.TypeOf(ast.List{}) {
211+
list := f.Addr().Interface().(*ast.List)
212+
for _, n := range list.Items {
213+
if err := walkSQLite(n, sc); err != nil {
214+
return err
215+
}
216+
}
217+
continue
218+
}
219+
220+
// Handle *ast.List
221+
if f.CanAddr() {
222+
if pl, ok := f.Addr().Interface().(**ast.List); ok && *pl != nil {
223+
for _, n := range (*pl).Items {
224+
if err := walkSQLite(n, sc); err != nil {
225+
return err
226+
}
227+
}
228+
continue
229+
}
230+
}
231+
232+
// Handle single ast.Node
233+
if f.CanInterface() {
234+
if n, ok := f.Interface().(ast.Node); ok {
235+
if err := walkSQLite(n, sc); err != nil {
236+
return err
237+
}
238+
continue
239+
}
240+
}
241+
242+
// Handle slices of ast.Node
243+
if f.Kind() == reflect.Slice {
244+
for j := 0; j < f.Len(); j++ {
245+
elem := f.Index(j)
246+
if elem.Kind() == reflect.Pointer && elem.IsNil() {
247+
continue
248+
}
249+
if elem.CanInterface() {
250+
if n, ok := elem.Interface().(ast.Node); ok {
251+
if err := walkSQLite(n, sc); err != nil {
252+
return err
253+
}
254+
}
255+
}
256+
}
257+
}
258+
259+
next:
260+
}
261+
return nil
262+
}
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- name: GetByPublicID :one
2+
SELECT *
3+
FROM locations l
4+
WHERE l.public_id = ?
5+
AND EXISTS (
6+
SELECT 1
7+
FROM projects p
8+
WHERE p.id = location.project_id
9+
);
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
CREATE TABLE organizations (
2+
id INTEGER PRIMARY KEY
3+
);
4+
5+
CREATE TABLE organization_members (
6+
id INTEGER PRIMARY KEY,
7+
organization_id INTEGER NOT NULL,
8+
account_id INTEGER NOT NULL
9+
);
10+
11+
CREATE TABLE projects (
12+
id INTEGER PRIMARY KEY,
13+
organization_id INTEGER NOT NULL
14+
);
15+
16+
CREATE TABLE locations (
17+
id INTEGER PRIMARY KEY,
18+
public_id TEXT UNIQUE NOT NULL,
19+
project_id INTEGER NOT NULL
20+
) STRICT;
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
version: "2"
2+
3+
sql:
4+
- engine: "sqlite"
5+
schema: "schema.sql"
6+
queries: "query.sql"
7+
rules:
8+
- sqlc/db-prepare
9+
gen:
10+
go:
11+
package: "db"
12+
out: "generated"
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
# package db
2+
query.sql:8:18: table alias "location" does not exist

0 commit comments

Comments
 (0)