a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
386 lines
11 KiB
Go
386 lines
11 KiB
Go
package sql
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
func TestExtractTables_BasicSelect(t *testing.T) {
|
|
refs := ExtractTables(`SELECT * FROM users WHERE id = 1`)
|
|
if len(refs) != 1 {
|
|
t.Fatalf("expected 1 ref, got %d: %+v", len(refs), refs)
|
|
}
|
|
if refs[0].Table != "users" || refs[0].Op != "select" {
|
|
t.Errorf("got %+v", refs[0])
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_JoinClauses(t *testing.T) {
|
|
refs := ExtractTables(`SELECT u.name FROM users u JOIN orders o ON u.id = o.user_id`)
|
|
if len(refs) != 2 {
|
|
t.Fatalf("expected 2 refs, got %d: %+v", len(refs), refs)
|
|
}
|
|
got := map[string]bool{}
|
|
for _, r := range refs {
|
|
got[r.Table] = true
|
|
}
|
|
if !got["users"] || !got["orders"] {
|
|
t.Errorf("missing refs: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_InsertUpdateDelete(t *testing.T) {
|
|
cases := []struct {
|
|
query string
|
|
op string
|
|
table string
|
|
}{
|
|
{`INSERT INTO users (id, name) VALUES (1, 'a')`, "insert", "users"},
|
|
{`UPDATE accounts SET balance = 0 WHERE id = 1`, "update", "accounts"},
|
|
{`DELETE FROM sessions WHERE expired = true`, "delete", "sessions"},
|
|
{`TRUNCATE TABLE logs`, "truncate", "logs"},
|
|
{`TRUNCATE logs2`, "truncate", "logs2"},
|
|
}
|
|
for _, c := range cases {
|
|
refs := ExtractTables(c.query)
|
|
if len(refs) != 1 {
|
|
t.Fatalf("%q → %d refs, want 1", c.query, len(refs))
|
|
}
|
|
if refs[0].Op != c.op || refs[0].Table != c.table {
|
|
t.Errorf("%q → %+v, want op=%q table=%q", c.query, refs[0], c.op, c.table)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_QuotedIdentifiers(t *testing.T) {
|
|
cases := []string{
|
|
`SELECT * FROM "users" WHERE id = 1`, // ANSI
|
|
"SELECT * FROM `users` WHERE id = 1", // MySQL backticks
|
|
`SELECT * FROM [users] WHERE id = 1`, // T-SQL brackets
|
|
}
|
|
for _, q := range cases {
|
|
refs := ExtractTables(q)
|
|
if len(refs) != 1 || refs[0].Table != "users" {
|
|
t.Errorf("%q → %+v, want table=users", q, refs)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_SchemaQualified(t *testing.T) {
|
|
refs := ExtractTables(`SELECT * FROM public.users JOIN auth.sessions ON id = session_id`)
|
|
if len(refs) != 2 {
|
|
t.Fatalf("expected 2 refs, got %d", len(refs))
|
|
}
|
|
got := map[string]string{} // table → schema
|
|
for _, r := range refs {
|
|
got[r.Table] = r.Schema
|
|
}
|
|
if got["users"] != "public" {
|
|
t.Errorf("users schema = %q, want public", got["users"])
|
|
}
|
|
if got["sessions"] != "auth" {
|
|
t.Errorf("sessions schema = %q, want auth", got["sessions"])
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_DeduplicatesSameOpAndTable(t *testing.T) {
|
|
refs := ExtractTables(`SELECT * FROM users JOIN users u2 ON 1=1`)
|
|
// Both users references share op=select, schema="" — should dedupe.
|
|
if len(refs) != 1 {
|
|
t.Errorf("expected dedup to single users ref, got %d", len(refs))
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_MixedOps(t *testing.T) {
|
|
q := `WITH x AS (SELECT * FROM source)
|
|
INSERT INTO target SELECT * FROM x`
|
|
refs := ExtractTables(q)
|
|
// `source` (select) + `target` (insert) + `x` (select) = 3
|
|
if len(refs) != 3 {
|
|
t.Errorf("expected 3 refs, got %d: %+v", len(refs), refs)
|
|
}
|
|
}
|
|
|
|
func TestExtractTables_Empty(t *testing.T) {
|
|
if r := ExtractTables(""); len(r) != 0 {
|
|
t.Errorf("empty query should yield no refs")
|
|
}
|
|
if r := ExtractTables("SELECT 1"); len(r) != 0 {
|
|
t.Errorf("no-table query should yield no refs")
|
|
}
|
|
}
|
|
|
|
func TestStripQuoting(t *testing.T) {
|
|
cases := []struct{ in, want string }{
|
|
{`"users"`, "users"},
|
|
{"`users`", "users"},
|
|
{"[users]", "users"},
|
|
{"users", "users"},
|
|
{`"`, `"`},
|
|
}
|
|
for _, c := range cases {
|
|
if got := stripQuoting(c.in); got != c.want {
|
|
t.Errorf("stripQuoting(%q) = %q, want %q", c.in, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSplitSchemaTable(t *testing.T) {
|
|
cases := []struct {
|
|
in string
|
|
schema, table string
|
|
}{
|
|
{"users", "", "users"},
|
|
{"public.users", "public", "users"},
|
|
{"db.public.users", "public", "users"}, // database segment dropped
|
|
{`"public"."users"`, "public", "users"},
|
|
}
|
|
for _, c := range cases {
|
|
s, t2 := splitSchemaTable(c.in)
|
|
if s != c.schema || t2 != c.table {
|
|
t.Errorf("splitSchemaTable(%q) = (%q,%q), want (%q,%q)", c.in, s, t2, c.schema, c.table)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractCreateTables_Basic(t *testing.T) {
|
|
src := `CREATE TABLE users (
|
|
id BIGINT PRIMARY KEY,
|
|
email TEXT UNIQUE
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS sessions (
|
|
user_id BIGINT REFERENCES users(id),
|
|
token TEXT
|
|
);
|
|
`
|
|
refs := ExtractCreateTables(src)
|
|
if len(refs) != 2 {
|
|
t.Fatalf("expected 2 refs, got %d: %+v", len(refs), refs)
|
|
}
|
|
got := map[string]bool{}
|
|
for _, r := range refs {
|
|
got[r.Table] = true
|
|
if r.Op != "create" {
|
|
t.Errorf("op = %q, want create", r.Op)
|
|
}
|
|
}
|
|
if !got["users"] || !got["sessions"] {
|
|
t.Errorf("missing tables: %v", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractCreateTables_Variants(t *testing.T) {
|
|
src := `
|
|
CREATE TEMPORARY TABLE t1 (id INT);
|
|
CREATE TEMP TABLE t2 (id INT);
|
|
CREATE GLOBAL TEMPORARY TABLE t3 (id INT);
|
|
CREATE UNLOGGED TABLE t4 (id INT);
|
|
CREATE TABLE IF NOT EXISTS t5 (id INT);
|
|
CREATE TABLE "quoted_name" (id INT);
|
|
CREATE TABLE auth.tokens (id INT);
|
|
`
|
|
refs := ExtractCreateTables(src)
|
|
if len(refs) != 7 {
|
|
t.Fatalf("expected 7 refs, got %d: %+v", len(refs), refs)
|
|
}
|
|
gotSchemas := map[string]string{}
|
|
for _, r := range refs {
|
|
gotSchemas[r.Table] = r.Schema
|
|
}
|
|
if gotSchemas["tokens"] != "auth" {
|
|
t.Errorf("auth.tokens schema = %q", gotSchemas["tokens"])
|
|
}
|
|
if gotSchemas["quoted_name"] != "" {
|
|
t.Errorf("quoted-name should have no schema, got %q", gotSchemas["quoted_name"])
|
|
}
|
|
}
|
|
|
|
func TestExtractCreateTables_DedupesSameSchemaTable(t *testing.T) {
|
|
src := `
|
|
CREATE TABLE users (id INT);
|
|
CREATE TABLE users (id INT);
|
|
`
|
|
refs := ExtractCreateTables(src)
|
|
if len(refs) != 1 {
|
|
t.Errorf("expected 1 deduped ref, got %d", len(refs))
|
|
}
|
|
}
|
|
|
|
func TestExtractCreateTables_IgnoresAlterAndDrop(t *testing.T) {
|
|
src := `
|
|
ALTER TABLE users ADD COLUMN name TEXT;
|
|
DROP TABLE sessions;
|
|
`
|
|
if got := ExtractCreateTables(src); len(got) != 0 {
|
|
t.Errorf("alter/drop should not produce CREATE refs, got %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestIsMigrationPath(t *testing.T) {
|
|
cases := []struct {
|
|
path string
|
|
want bool
|
|
}{
|
|
{"db/migrate/001_create_users.sql", true},
|
|
{"db/migrations/2024_01_init.sql", true},
|
|
{"migrations/v1.sql", true},
|
|
{"internal/db/migrations/init.sql", true},
|
|
{"pkg/queries/select.sql", false},
|
|
{"main.go", false},
|
|
{"docs/migration_guide.md", false}, // not .sql
|
|
}
|
|
for _, c := range cases {
|
|
if got := IsMigrationPath(c.path); got != c.want {
|
|
t.Errorf("IsMigrationPath(%q) = %v, want %v", c.path, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestMigrationNodeID(t *testing.T) {
|
|
if got := MigrationNodeID("db/migrate/001_init.sql"); got != "migration::db/migrate/001_init.sql" {
|
|
t.Errorf("got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestExtractColumns_InsertWrites(t *testing.T) {
|
|
got := ExtractColumns("INSERT INTO users (id, email, name) VALUES ($1, $2, $3)")
|
|
wantCols := map[string]bool{"id": false, "email": false, "name": false}
|
|
for _, c := range got {
|
|
if c.Op != "write" {
|
|
t.Errorf("expected write op, got %q for %q", c.Op, c.Column)
|
|
}
|
|
if c.Table != "users" {
|
|
t.Errorf("expected table users, got %q", c.Table)
|
|
}
|
|
if _, ok := wantCols[c.Column]; ok {
|
|
wantCols[c.Column] = true
|
|
}
|
|
}
|
|
for col, found := range wantCols {
|
|
if !found {
|
|
t.Errorf("expected column %q in result, got %v", col, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractColumns_UpdateWrites(t *testing.T) {
|
|
got := ExtractColumns("UPDATE users SET email = $1, updated_at = NOW() WHERE id = $2")
|
|
wantCols := map[string]bool{"email": false, "updated_at": false}
|
|
for _, c := range got {
|
|
if c.Op != "write" {
|
|
continue
|
|
}
|
|
if _, ok := wantCols[c.Column]; ok {
|
|
wantCols[c.Column] = true
|
|
}
|
|
}
|
|
for col, found := range wantCols {
|
|
if !found {
|
|
t.Errorf("expected SET column %q to surface as write; got %v", col, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractColumns_SingleTableSelectReads(t *testing.T) {
|
|
got := ExtractColumns("SELECT id, email, name FROM users WHERE id = $1")
|
|
want := map[string]bool{"id": false, "email": false, "name": false}
|
|
for _, c := range got {
|
|
if c.Op != "read" {
|
|
continue
|
|
}
|
|
if c.Table != "users" {
|
|
continue
|
|
}
|
|
if _, ok := want[c.Column]; ok {
|
|
want[c.Column] = true
|
|
}
|
|
}
|
|
for col, found := range want {
|
|
if !found {
|
|
t.Errorf("expected read column %q; got %v", col, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractColumns_StarSelectNoColumns(t *testing.T) {
|
|
got := ExtractColumns("SELECT * FROM users")
|
|
for _, c := range got {
|
|
if c.Op == "read" {
|
|
t.Errorf("SELECT * should not produce read columns; got %v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestExtractColumns_JoinSelectReturnsNoColumns(t *testing.T) {
|
|
got := ExtractColumns("SELECT u.id, o.id FROM users u JOIN orders o ON o.user_id = u.id")
|
|
for _, c := range got {
|
|
if c.Op == "read" {
|
|
t.Errorf("multi-table SELECT should not emit columns in v1; got %v", got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestColumnNodeID(t *testing.T) {
|
|
cases := []struct {
|
|
dialect, schema, table, column, want string
|
|
}{
|
|
{"postgres", "public", "users", "email", "col::postgres::public.users.email"},
|
|
{"", "", "users", "email", "col::generic::users.email"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := ColumnNodeID(c.dialect, c.schema, c.table, c.column); got != c.want {
|
|
t.Errorf("ColumnNodeID(%q,%q,%q,%q) = %q, want %q",
|
|
c.dialect, c.schema, c.table, c.column, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTableNodeID(t *testing.T) {
|
|
cases := []struct {
|
|
dialect, schema, table, want string
|
|
}{
|
|
{"postgres", "public", "users", "db::postgres::public.users"},
|
|
{"", "", "users", "db::generic::users"},
|
|
{"mysql", "", "orders", "db::mysql::orders"},
|
|
}
|
|
for _, c := range cases {
|
|
if got := TableNodeID(c.dialect, c.schema, c.table); got != c.want {
|
|
t.Errorf("TableNodeID(%q,%q,%q) = %q, want %q",
|
|
c.dialect, c.schema, c.table, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestProjectionColumns(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
query string
|
|
want []string
|
|
}{
|
|
{"plain columns", "SELECT id, name FROM users", []string{"id", "name"}},
|
|
{"table-qualified", "SELECT u.id, u.name FROM users u", []string{"id", "name"}},
|
|
{"alias wins", "SELECT total AS order_total, id FROM orders", []string{"order_total", "id"}},
|
|
{"expression alias", "SELECT count(*) AS n, max(x) AS m FROM t", []string{"n", "m"}},
|
|
{"cast inner AS ignored", "SELECT cast(zip AS text) AS zip_code FROM t", []string{"zip_code"}},
|
|
{"unaliased expr dropped", "SELECT count(*), id FROM t", []string{"id"}},
|
|
{"star yields nothing", "SELECT * FROM t", nil},
|
|
{"final select of CTE", "WITH a AS (SELECT p FROM raw) SELECT x, y FROM a", []string{"x", "y"}},
|
|
{"no from clause", "SELECT 1 AS id, 2 AS qty", []string{"id", "qty"}},
|
|
{"empty", "", nil},
|
|
{"dedup", "SELECT id, id FROM t", []string{"id"}},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
got := ProjectionColumns(c.query)
|
|
if len(got) != len(c.want) {
|
|
t.Fatalf("ProjectionColumns(%q) = %v, want %v", c.query, got, c.want)
|
|
}
|
|
for i := range got {
|
|
if got[i] != c.want[i] {
|
|
t.Fatalf("ProjectionColumns(%q) = %v, want %v", c.query, got, c.want)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|