Skip to content

Commit 6c188fc

Browse files
committed
core/seed: move the system catalogs into relations.jsonl
pg_catalog's 139 tables and information_schema's 69 were the last of PostgreSQL's catalog held as generated Go, and the analysis core had no notion of a relation it did not read from the user's schema — so a query against information_schema.columns failed to analyze. They are now records in a dialect's relations.jsonl, keyed by schema: sql_class is what the core stores them in, and "relations" is the friendly name for it the way "functions" is for sql_proc. sqlc-pg-gen writes the file, the core seeds classes and attributes from it, and the two schemas the legacy compiler builds are read back from the same records — which takes pg_catalog.go and information_schema.go out of the tree entirely. Relations are seeded on demand rather than up front. A dialect's system catalogs are thousands of columns that most queries never name, and loading them for every invocation cost 40ms; the catalog now loads them the first time a query names a schema it does not have. Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_011MnoUabwBWW9gaEn2Nj7eG
1 parent 0b3636c commit 6c188fc

13 files changed

Lines changed: 577 additions & 14594 deletions

File tree

internal/core/catalog.go

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,36 @@ type Catalog struct {
2121
// dialectOID is the dialect this catalog was seeded with. A catalog is
2222
// built for one dialect, so dialect-wide lookups need no other input.
2323
dialectOID int64
24+
25+
// deferred holds the parts of a seed that are only worth loading if a
26+
// query asks for them. They run at most once, when a namespace lookup
27+
// misses.
28+
deferred []func(*Catalog) error
29+
deferredDone bool
30+
}
31+
32+
// SeedLater registers a part of the seed to run the first time the catalog is
33+
// asked for a namespace it does not have. A dialect's system catalogs run to
34+
// thousands of columns that most queries never reference, so loading them is
35+
// left until one does.
36+
func (c *Catalog) SeedLater(fn func(*Catalog) error) {
37+
c.deferred = append(c.deferred, fn)
38+
}
39+
40+
// runDeferred runs the deferred seeds, reporting whether it had any to run.
41+
func (c *Catalog) runDeferred() (bool, error) {
42+
if c.deferredDone || len(c.deferred) == 0 {
43+
return false, nil
44+
}
45+
// Marked done up front: a deferred seed looks namespaces up itself, and
46+
// must not set itself running again.
47+
c.deferredDone = true
48+
for _, fn := range c.deferred {
49+
if err := fn(c); err != nil {
50+
return false, err
51+
}
52+
}
53+
return true, nil
2454
}
2555

2656
type Option func(*Catalog) error

internal/core/namespace.go

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,10 +15,20 @@ func (c *Catalog) CreateNamespace(name string) (int64, error) {
1515

1616
func (c *Catalog) NamespaceOID(name string) (int64, error) {
1717
oid, err := c.q.NamespaceOID(context.Background(), name)
18-
if err != nil {
19-
return 0, fmt.Errorf("namespace %q: %w", name, err)
18+
if err == nil {
19+
return oid, nil
2020
}
21-
return oid, nil
21+
// A namespace the catalog does not have may be one a deferred seed brings
22+
// in — a dialect's system catalogs, which most queries never name and
23+
// which are therefore not loaded until one does.
24+
if ran, derr := c.runDeferred(); derr != nil {
25+
return 0, derr
26+
} else if ran {
27+
if oid, err := c.q.NamespaceOID(context.Background(), name); err == nil {
28+
return oid, nil
29+
}
30+
}
31+
return 0, fmt.Errorf("namespace %q: %w", name, err)
2232
}
2333

2434
type NamespaceInfo struct {

internal/core/seed/seed.go

Lines changed: 159 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,15 @@
1010
// operators.jsonl operator overloads beyond the ones the rules generate
1111
// casts.jsonl casts beyond the ones the rules generate
1212
// functions.jsonl the functions it ships with
13+
// relations.jsonl the system tables and views it ships with
1314
//
1415
// The lists are JSONL — one record per line — and are applied as they are
1516
// read, so a dialect whose function list runs to thousands of entries is never
1617
// held in memory as a whole. Any of the lists may be left out.
18+
//
19+
// Relations are the exception to "applied as they are read": they are loaded
20+
// the first time a query names a schema the catalog does not yet have, since
21+
// most queries never touch a system catalog.
1722
package seed
1823

1924
import (
@@ -38,6 +43,7 @@ const (
3843
OperatorsFile = "operators.jsonl"
3944
CastsFile = "casts.jsonl"
4045
FunctionsFile = "functions.jsonl"
46+
RelationsFile = "relations.jsonl"
4147
)
4248

4349
// Settings is dialect.json: what the dialect is called and the rules that
@@ -105,6 +111,28 @@ type Function struct {
105111
Nullable bool `json:"nullable,omitempty"`
106112
}
107113

114+
// Relation is a table or view the dialect ships with, such as one of
115+
// PostgreSQL's system catalogs. Kind is 'r' for a table or 'v' for a view, and
116+
// defaults to a table.
117+
type Relation struct {
118+
// Catalog is the database the relation belongs to, which PostgreSQL
119+
// reports for its own schemas and which the legacy catalog carries.
120+
Catalog string `json:"catalog,omitempty"`
121+
Schema string `json:"schema"`
122+
Name string `json:"name"`
123+
Kind string `json:"kind,omitempty"`
124+
Columns []Column `json:"columns"`
125+
}
126+
127+
// Column is one of a relation's columns.
128+
type Column struct {
129+
Name string `json:"name"`
130+
Type string `json:"type"`
131+
NotNull bool `json:"not_null,omitempty"`
132+
Array bool `json:"array,omitempty"`
133+
Length int `json:"length,omitempty"`
134+
}
135+
108136
// Arg is one of a function's parameters. Mode is 'i'n, 'o'ut, 'b'oth, 't'able
109137
// or 'v'ariadic, and defaults to in.
110138
type Arg struct {
@@ -132,10 +160,11 @@ func apply(cat *core.Catalog, fsys fs.FS) error {
132160
return err
133161
}
134162
b := &builder{
135-
cat: cat,
136-
settings: settings,
137-
oids: map[string]int64{},
138-
seenCasts: map[[2]int64]bool{},
163+
cat: cat,
164+
settings: settings,
165+
oids: map[string]int64{},
166+
namespaces: map[string]int64{},
167+
seenCasts: map[[2]int64]bool{},
139168
}
140169
if b.dialectOID, err = cat.CreateDialect(settings.Dialect); err != nil {
141170
return err
@@ -160,7 +189,16 @@ func apply(cat *core.Catalog, fsys fs.FS) error {
160189
if err := b.categoryCasts(); err != nil {
161190
return err
162191
}
163-
return stream(fsys, FunctionsFile, b.addFunction)
192+
if err := stream(fsys, FunctionsFile, b.addFunction); err != nil {
193+
return err
194+
}
195+
196+
// A dialect's system catalogs are thousands of columns that a query only
197+
// occasionally names, so they wait until one does.
198+
cat.SeedLater(func(*core.Catalog) error {
199+
return stream(fsys, RelationsFile, b.addRelation)
200+
})
201+
return nil
164202
}
165203

166204
func loadSettings(fsys fs.FS) (Settings, error) {
@@ -245,6 +283,44 @@ func Functions(fsys fs.FS, dir string) ([]*catalog.Function, error) {
245283
return out, nil
246284
}
247285

286+
// Relations streams the relations a dialect ships with in the named schema
287+
// into the form the engine catalogs use.
288+
func Relations(fsys fs.FS, dir, schema string) ([]*catalog.Table, error) {
289+
sub, err := fs.Sub(fsys, dir)
290+
if err != nil {
291+
return nil, fmt.Errorf("seed: %s: %w", dir, err)
292+
}
293+
var out []*catalog.Table
294+
err = stream(sub, RelationsFile, func(rel Relation) error {
295+
if rel.Schema != schema {
296+
return nil
297+
}
298+
table := &catalog.Table{
299+
Rel: &ast.TableName{Catalog: rel.Catalog, Schema: rel.Schema, Name: rel.Name},
300+
Columns: make([]*catalog.Column, 0, len(rel.Columns)),
301+
}
302+
for _, col := range rel.Columns {
303+
column := &catalog.Column{
304+
Name: col.Name,
305+
Type: ast.TypeName{Name: col.Type},
306+
IsNotNull: col.NotNull,
307+
IsArray: col.Array,
308+
}
309+
if col.Length > 0 {
310+
length := col.Length
311+
column.Length = &length
312+
}
313+
table.Columns = append(table.Columns, column)
314+
}
315+
out = append(out, table)
316+
return nil
317+
})
318+
if err != nil {
319+
return nil, err
320+
}
321+
return out, nil
322+
}
323+
248324
// argModes maps the mode a record carries to the one the catalog uses. The
249325
// letters are PostgreSQL's, which is where the generated function lists come
250326
// from.
@@ -285,6 +361,10 @@ type builder struct {
285361
oids map[string]int64
286362
categories []categorized
287363

364+
// namespaces maps a schema name to its OID, so that a run of relations in
365+
// the same schema resolves it once.
366+
namespaces map[string]int64
367+
288368
// seenCasts records the pairs already registered. The alias rules and the
289369
// category rules overlap, and a cast pair is unique in the catalog.
290370
seenCasts map[[2]int64]bool
@@ -509,6 +589,80 @@ func (b *builder) addFunction(fn Function) error {
509589
return nil
510590
}
511591

592+
func (b *builder) addRelation(rel Relation) error {
593+
if rel.Name == "" {
594+
return errors.New("relation has no name")
595+
}
596+
nsOID, err := b.namespace(rel.Schema)
597+
if err != nil {
598+
return err
599+
}
600+
kind := rel.Kind
601+
if kind == "" {
602+
kind = "r"
603+
}
604+
classOID, err := b.cat.CreateClass(nsOID, rel.Name, kind)
605+
if err != nil {
606+
return err
607+
}
608+
for i, col := range rel.Columns {
609+
name := col.Type
610+
if col.Array {
611+
name += core.ArraySuffix
612+
}
613+
typeOID, err := b.columnType(name)
614+
if err != nil {
615+
return fmt.Errorf("relation %q column %q: %w", rel.Name, col.Name, err)
616+
}
617+
if err := b.cat.CreateAttributeSpec(core.AttributeSpec{
618+
ClassOID: classOID,
619+
Name: col.Name,
620+
TypeOID: typeOID,
621+
Num: i + 1,
622+
NotNull: col.NotNull,
623+
DeclType: col.Type,
624+
TypeLength: col.Length,
625+
}); err != nil {
626+
return fmt.Errorf("relation %q column %q: %w", rel.Name, col.Name, err)
627+
}
628+
}
629+
return nil
630+
}
631+
632+
// namespace resolves the schema a relation belongs to, creating it the first
633+
// time it is named. A relation with no schema belongs to the default one.
634+
func (b *builder) namespace(schema string) (int64, error) {
635+
if schema == "" {
636+
schema = "public"
637+
}
638+
if oid, ok := b.namespaces[schema]; ok {
639+
return oid, nil
640+
}
641+
oid, err := b.cat.NamespaceOID(schema)
642+
if err != nil {
643+
if oid, err = b.cat.CreateNamespace(schema); err != nil {
644+
return 0, err
645+
}
646+
}
647+
b.namespaces[schema] = oid
648+
return oid, nil
649+
}
650+
651+
// columnType resolves a column's type, which unlike a function signature may
652+
// name an array.
653+
func (b *builder) columnType(name string) (int64, error) {
654+
key := strings.ToLower(name)
655+
if oid, ok := b.oids[key]; ok {
656+
return oid, nil
657+
}
658+
oid, err := b.cat.ResolveTypeName(key)
659+
if err != nil {
660+
return 0, err
661+
}
662+
b.oids[key] = oid
663+
return oid, nil
664+
}
665+
512666
// funcType resolves the type a function signature names. Signatures reference
513667
// pseudo types ("any", "record") and types no dialect bothers to list, so an
514668
// unknown name is registered rather than rejected.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"command": "analyze",
3+
"args": ["--dialect", "postgresql", "--schema", "schema.sql", "query.sql"],
4+
"contexts": ["base"]
5+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
-- name: ListColumns :many
2+
SELECT table_name, column_name, data_type
3+
FROM information_schema.columns
4+
WHERE table_schema = $1;
5+
6+
-- name: CountRelations :one
7+
SELECT count(*) AS total FROM pg_catalog.pg_class WHERE relkind = $1;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
CREATE TABLE authors (
2+
id BIGSERIAL PRIMARY KEY,
3+
name text NOT NULL
4+
);
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
[
2+
{
3+
"name": "ListColumns",
4+
"cmd": ":many",
5+
"columns": [
6+
{
7+
"name": "table_name",
8+
"data_type": "sql_identifier",
9+
"not_null": false,
10+
"is_array": false,
11+
"table": "columns"
12+
},
13+
{
14+
"name": "column_name",
15+
"data_type": "sql_identifier",
16+
"not_null": false,
17+
"is_array": false,
18+
"table": "columns"
19+
},
20+
{
21+
"name": "data_type",
22+
"data_type": "character_data",
23+
"not_null": false,
24+
"is_array": false,
25+
"table": "columns"
26+
}
27+
],
28+
"params": [
29+
{
30+
"number": 1,
31+
"column": {
32+
"name": "table_schema",
33+
"data_type": "sql_identifier",
34+
"not_null": false,
35+
"is_array": false,
36+
"table": "columns"
37+
}
38+
}
39+
]
40+
},
41+
{
42+
"name": "CountRelations",
43+
"cmd": ":one",
44+
"columns": [
45+
{
46+
"name": "total",
47+
"data_type": "bigint",
48+
"not_null": true,
49+
"is_array": false
50+
}
51+
],
52+
"params": [
53+
{
54+
"number": 1,
55+
"column": {
56+
"name": "relkind",
57+
"data_type": "char",
58+
"not_null": true,
59+
"is_array": false,
60+
"table": "pg_class"
61+
}
62+
}
63+
]
64+
}
65+
]

0 commit comments

Comments
 (0)