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
365 lines
10 KiB
Go
365 lines
10 KiB
Go
// Package postgresql generates the PostgreSQL dialect seed under
|
|
// internal/engine/postgresql/dialect from a live server: pg_catalog's
|
|
// functions, the relations of pg_catalog and information_schema, and one
|
|
// directory per contrib extension holding the types and functions CREATE
|
|
// EXTENSION adds. types.jsonl and operators.jsonl at the top of the dialect
|
|
// are written by hand and are not this package's business.
|
|
//
|
|
// The server is named by POSTGRESQL_SERVER_URI and has to be the major
|
|
// release in Major, since each release adds to its catalogs.
|
|
package postgresql
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
"github.com/sqlc-dev/sqlc/internal/goldeneye/dialect"
|
|
)
|
|
|
|
// Engine is the name of the engine directory the dialect lives under.
|
|
const Engine = "postgresql"
|
|
|
|
// Major is the PostgreSQL major release the dialect is generated from.
|
|
// Bumping it is a deliberate change: every release adds functions and
|
|
// catalog columns, so regenerate and review the dialect after changing it.
|
|
const Major = 16
|
|
|
|
// https://dba.stackexchange.com/questions/255412/how-to-select-functions-that-belong-in-a-given-extension-in-postgresql
|
|
//
|
|
// Extension functions are added to the public schema
|
|
const extensionFuncs = `
|
|
WITH extension_funcs AS (
|
|
SELECT p.oid
|
|
FROM pg_catalog.pg_extension AS e
|
|
INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
|
|
INNER JOIN pg_catalog.pg_proc AS p ON (p.oid = d.objid)
|
|
INNER JOIN pg_catalog.pg_namespace AS ne ON (ne.oid = e.extnamespace)
|
|
INNER JOIN pg_catalog.pg_namespace AS np ON (np.oid = p.pronamespace)
|
|
WHERE d.deptype = 'e' AND e.extname = $1
|
|
)
|
|
SELECT p.proname as name,
|
|
format_type(p.prorettype, NULL),
|
|
array(select format_type(unnest(p.proargtypes), NULL)),
|
|
p.proargnames,
|
|
p.proargnames[p.pronargs-p.pronargdefaults+1:p.pronargs],
|
|
p.proargmodes::text[]
|
|
FROM pg_catalog.pg_proc p
|
|
JOIN extension_funcs ef ON ef.oid = p.oid
|
|
WHERE pg_function_is_visible(p.oid)
|
|
-- simply order all columns to keep subsequent runs stable
|
|
ORDER BY 1, 2, 3, 4, 5;
|
|
`
|
|
|
|
// The types an extension defines, in the order they were declared so that a
|
|
// type appears after the types it is built on. Array types are created
|
|
// implicitly rather than by the extension's script, so they carry an internal
|
|
// dependency and never show up here.
|
|
const extensionTypes = `
|
|
SELECT t.typname, t.typcategory
|
|
FROM pg_catalog.pg_extension AS e
|
|
INNER JOIN pg_catalog.pg_depend AS d ON (d.refobjid = e.oid)
|
|
INNER JOIN pg_catalog.pg_type AS t ON (t.oid = d.objid)
|
|
WHERE d.deptype = 'e'
|
|
AND d.classid = 'pg_catalog.pg_type'::regclass
|
|
AND e.extname = $1
|
|
AND t.typtype <> 'p'
|
|
ORDER BY t.oid;
|
|
`
|
|
|
|
// Locate returns the server to generate from, named by POSTGRESQL_SERVER_URI.
|
|
func Locate() (string, error) {
|
|
if url := os.Getenv("POSTGRESQL_SERVER_URI"); url != "" {
|
|
return url, nil
|
|
}
|
|
return "", errors.New("POSTGRESQL_SERVER_URI is not set")
|
|
}
|
|
|
|
// Version reports the release a server is.
|
|
func Version(ctx context.Context, url string) (string, error) {
|
|
conn, err := pgx.Connect(ctx, url)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer conn.Close(ctx)
|
|
var version string
|
|
if err := conn.QueryRow(ctx, "SELECT version()").Scan(&version); err != nil {
|
|
return "", err
|
|
}
|
|
return version, nil
|
|
}
|
|
|
|
// checkVersion refuses a server of another major release than the dialect
|
|
// is generated from, whose catalogs would differ from the committed ones
|
|
// without anything being wrong.
|
|
func checkVersion(ctx context.Context, conn *pgx.Conn) error {
|
|
var num string
|
|
if err := conn.QueryRow(ctx, "SELECT current_setting('server_version_num')").Scan(&num); err != nil {
|
|
return err
|
|
}
|
|
n, err := strconv.Atoi(num)
|
|
if err != nil {
|
|
return fmt.Errorf("server_version_num %q: %w", num, err)
|
|
}
|
|
if major := n / 10000; major != Major {
|
|
return fmt.Errorf("the dialect is generated from PostgreSQL %d, but the server is PostgreSQL %d", Major, major)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func clean(arg string) string {
|
|
arg = strings.TrimSpace(arg)
|
|
arg = strings.Replace(arg, "\"any\"", "any", -1)
|
|
arg = strings.Replace(arg, "\"char\"", "char", -1)
|
|
arg = strings.Replace(arg, "\"timestamp\"", "char", -1)
|
|
return arg
|
|
}
|
|
|
|
// encodeFunctions encodes procs as JSONL, one function per line, in the form
|
|
// the seed package reads. Both the analysis core and the catalog the legacy
|
|
// compiler builds load the result.
|
|
func encodeFunctions(procs []Proc) ([]byte, error) {
|
|
funcs := make([]dialect.Function, 0, len(procs))
|
|
for _, proc := range procs {
|
|
fn := dialect.Function{Name: proc.Name, Returns: proc.ReturnTypeName()}
|
|
for _, arg := range proc.Args() {
|
|
a := dialect.Arg{Name: arg.Name, Type: arg.TypeName(), HasDefault: arg.HasDefault}
|
|
if arg.Mode != "" && arg.Mode != "i" {
|
|
a.Mode = arg.Mode
|
|
}
|
|
fn.Args = append(fn.Args, a)
|
|
}
|
|
funcs = append(funcs, fn)
|
|
}
|
|
return dialect.JSONL(funcs)
|
|
}
|
|
|
|
// readExtensionTypes reads the types an extension defines.
|
|
func readExtensionTypes(ctx context.Context, conn *pgx.Conn, extension string) ([]dialect.Type, error) {
|
|
rows, err := conn.Query(ctx, extensionTypes, extension)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
var types []dialect.Type
|
|
for rows.Next() {
|
|
var t dialect.Type
|
|
if err := rows.Scan(&t.Name, &t.Category); err != nil {
|
|
return nil, err
|
|
}
|
|
types = append(types, t)
|
|
}
|
|
return types, rows.Err()
|
|
}
|
|
|
|
// writeRelations appends a schema's relations to out as JSONL, one relation
|
|
// per line, in the form the seed package reads.
|
|
func writeRelations(out *bytes.Buffer, schemaName string, relations []Relation) error {
|
|
enc := json.NewEncoder(out)
|
|
for _, relation := range relations {
|
|
rec := dialect.Relation{
|
|
Catalog: relation.Catalog,
|
|
Schema: schemaName,
|
|
Name: relation.Name,
|
|
}
|
|
for _, col := range relation.Columns {
|
|
c := dialect.Column{
|
|
Name: col.Name,
|
|
Type: col.Type,
|
|
NotNull: col.IsNotNull,
|
|
Array: col.IsArray,
|
|
}
|
|
if col.Length != nil {
|
|
c.Length = *col.Length
|
|
}
|
|
rec.Columns = append(rec.Columns, c)
|
|
}
|
|
if err := enc.Encode(rec); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// preserveLegacyCatalogBehavior maintain previous ordering and filtering
|
|
// that was manually done to the generated file pg_catalog.go.
|
|
// Some of the test depend on this ordering - in particular, function lookups
|
|
// where there might be multiple matching functions (due to type overloads)
|
|
// Until sqlc supports "smarter" looking up of these functions,
|
|
// preserveLegacyCatalogBehavior ensures there are no accidental test breakages
|
|
func preserveLegacyCatalogBehavior(allProcs []Proc) []Proc {
|
|
// Preserve the legacy sort order of the end-to-end tests
|
|
sort.SliceStable(allProcs, func(i, j int) bool {
|
|
fnA := allProcs[i]
|
|
fnB := allProcs[j]
|
|
|
|
if fnA.Name == "lower" && fnB.Name == "lower" && len(fnA.ArgTypes) == 1 && fnA.ArgTypes[0] == "text" {
|
|
return true
|
|
}
|
|
|
|
if fnA.Name == "generate_series" && fnB.Name == "generate_series" && len(fnA.ArgTypes) == 2 && fnA.ArgTypes[0] == "numeric" {
|
|
return true
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
procs := make([]Proc, 0, len(allProcs))
|
|
for _, p := range allProcs {
|
|
// Skip generating pg_catalog.concat to preserve legacy behavior
|
|
if p.Name == "concat" {
|
|
continue
|
|
}
|
|
|
|
procs = append(procs, p)
|
|
}
|
|
|
|
return procs
|
|
}
|
|
|
|
// Generate reads the dialect from the server. It creates every contrib
|
|
// extension it describes, so the server's contrib package has to be
|
|
// installed.
|
|
func Generate(ctx context.Context, url string) (dialect.Files, error) {
|
|
conn, err := pgx.Connect(ctx, url)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer conn.Close(ctx)
|
|
if err := checkVersion(ctx, conn); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
files := dialect.Files{}
|
|
|
|
// The two schemas sqlc knows PostgreSQL by. pg_catalog's functions are
|
|
// the dialect's standard library; the relations of both share one file,
|
|
// keyed by schema.
|
|
procs, err := readProcs(ctx, conn, "pg_catalog", extensions)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if files[dialect.FunctionsFile], err = encodeFunctions(preserveLegacyCatalogBehavior(procs)); err != nil {
|
|
return nil, err
|
|
}
|
|
var relations bytes.Buffer
|
|
for _, schema := range []string{"pg_catalog", "information_schema"} {
|
|
rels, err := readRelations(ctx, conn, schema)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := writeRelations(&relations, schema, rels); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
files[dialect.RelationsFile] = relations.Bytes()
|
|
|
|
// Each extension is a directory of its own under the dialect, holding the
|
|
// functions CREATE EXTENSION adds to the catalog.
|
|
for _, extension := range extensions {
|
|
if _, err := conn.Exec(ctx, fmt.Sprintf("CREATE EXTENSION IF NOT EXISTS %q", extension)); err != nil {
|
|
return nil, fmt.Errorf("error creating %s: %s", extension, err)
|
|
}
|
|
|
|
rows, err := conn.Query(ctx, extensionFuncs, extension)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
procs, err := scanProcs(rows)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
types, err := readExtensionTypes(ctx, conn, extension)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(procs) == 0 && len(types) == 0 {
|
|
continue
|
|
}
|
|
|
|
// Preserve the legacy sort order of the end-to-end tests
|
|
sort.SliceStable(procs, func(i, j int) bool {
|
|
fnA := procs[i]
|
|
fnB := procs[j]
|
|
|
|
if extension == "pgcrypto" {
|
|
if fnA.Name == "digest" && fnB.Name == "digest" && len(fnA.ArgTypes) == 2 && fnA.ArgTypes[0] == "text" {
|
|
return true
|
|
}
|
|
}
|
|
|
|
return false
|
|
})
|
|
|
|
extensionDir := path.Join(dialect.ExtensionsDir, extension)
|
|
if len(types) > 0 {
|
|
if files[path.Join(extensionDir, dialect.TypesFile)], err = dialect.JSONL(types); err != nil {
|
|
return nil, fmt.Errorf("error generating extension %s: %w", extension, err)
|
|
}
|
|
}
|
|
if files[path.Join(extensionDir, dialect.FunctionsFile)], err = encodeFunctions(procs); err != nil {
|
|
return nil, fmt.Errorf("error generating extension %s: %w", extension, err)
|
|
}
|
|
}
|
|
|
|
return files, nil
|
|
}
|
|
|
|
// https://www.postgresql.org/docs/current/contrib.html
|
|
var extensions = []string{
|
|
"adminpack",
|
|
"amcheck",
|
|
// "auth_delay",
|
|
// "auto_explain",
|
|
// "bloom",
|
|
"btree_gin",
|
|
"btree_gist",
|
|
"citext",
|
|
"cube",
|
|
"dblink",
|
|
// "dict_int",
|
|
// "dict_xsyn",
|
|
"earthdistance",
|
|
"file_fdw",
|
|
"fuzzystrmatch",
|
|
"hstore",
|
|
"intagg",
|
|
"intarray",
|
|
"isn",
|
|
"lo",
|
|
"ltree",
|
|
"pageinspect",
|
|
// "passwordcheck",
|
|
"pg_buffercache",
|
|
"pg_freespacemap",
|
|
"pg_prewarm",
|
|
"pg_stat_statements",
|
|
"pg_trgm",
|
|
"pg_visibility",
|
|
"pgcrypto",
|
|
"pgrowlocks",
|
|
"pgstattuple",
|
|
"postgres_fdw",
|
|
"seg",
|
|
// "sepgsql",
|
|
// "spi",
|
|
"sslinfo",
|
|
"tablefunc",
|
|
"tcn",
|
|
// "test_decoding",
|
|
// "tsm_system_rows",
|
|
// "tsm_system_time",
|
|
"unaccent",
|
|
"uuid-ossp",
|
|
"xml2",
|
|
}
|