pg_catalog_test.go 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760
  1. package executor
  2. import (
  3. "errors"
  4. "fmt"
  5. "strings"
  6. "testing"
  7. "github.com/danfragoso/pizzasql-next/pkg/pgerror"
  8. )
  9. // setupPGCatalogFixture creates a schema that exercises every catalog-backed
  10. // object shape: single and composite primary keys, UNIQUE, FOREIGN KEY, CHECK,
  11. // explicit indexes, arrays, JSONB, defaults, and a view.
  12. func setupPGCatalogFixture(t *testing.T) *Executor {
  13. t.Helper()
  14. _, schema, table := newTestDB(t)
  15. e := newExec(schema, table)
  16. for _, sql := range []string{
  17. "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)",
  18. "CREATE TABLE orders (id INTEGER PRIMARY KEY, account_id INTEGER NOT NULL, note TEXT DEFAULT 'new', tags TEXT[], meta JSONB, CHECK (id > 0), CONSTRAINT fk_orders_account FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE)",
  19. "CREATE INDEX idx_orders_note ON orders (note)",
  20. "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))",
  21. "CREATE VIEW active_orders AS SELECT id, note FROM orders",
  22. } {
  23. execMust(t, e, sql)
  24. }
  25. return e
  26. }
  27. func catalogCell(t *testing.T, res *Result, row, col int) string {
  28. t.Helper()
  29. if row >= len(res.Rows) || col >= len(res.Rows[row]) {
  30. t.Fatalf("row %d column %d out of range: %v", row, col, res.Rows)
  31. }
  32. value := res.Rows[row][col]
  33. if value == nil {
  34. return "<nil>"
  35. }
  36. return fmt.Sprintf("%v", value)
  37. }
  38. func catalogRows(t *testing.T, e *Executor, query string) [][]string {
  39. t.Helper()
  40. res, err := execSQL(e, query)
  41. if err != nil {
  42. t.Fatalf("%s: %v", query, err)
  43. }
  44. rows := make([][]string, len(res.Rows))
  45. for i := range res.Rows {
  46. rows[i] = make([]string, len(res.Rows[i]))
  47. for j, value := range res.Rows[i] {
  48. if value == nil {
  49. rows[i][j] = "<nil>"
  50. continue
  51. }
  52. rows[i][j] = fmt.Sprintf("%v", value)
  53. }
  54. }
  55. return rows
  56. }
  57. // A PostgreSQL-style column listing joins pg_class, pg_namespace, and
  58. // pg_attribute; the synthetic OIDs must line up across all three.
  59. func TestPGCatalogJoinListsColumns(t *testing.T) {
  60. e := setupPGCatalogFixture(t)
  61. rows := catalogRows(t, e, `
  62. SELECT n.nspname, c.relname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), a.attnotnull
  63. FROM pg_catalog.pg_class c
  64. JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  65. JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  66. WHERE c.relname = 'orders' AND n.nspname = 'public' AND a.attnum > 0 AND NOT a.attisdropped
  67. ORDER BY a.attnum`)
  68. want := [][]string{
  69. {"public", "orders", "id", "integer", "true"},
  70. {"public", "orders", "account_id", "integer", "true"},
  71. {"public", "orders", "note", "text", "false"},
  72. {"public", "orders", "tags", "text[]", "false"},
  73. {"public", "orders", "meta", "jsonb", "false"},
  74. }
  75. if fmt.Sprint(rows) != fmt.Sprint(want) {
  76. t.Fatalf("columns = %v, want %v", rows, want)
  77. }
  78. }
  79. func TestPGCatalogInformationSchemaColumns(t *testing.T) {
  80. e := setupPGCatalogFixture(t)
  81. rows := catalogRows(t, e, `
  82. SELECT table_name, column_name, data_type, is_nullable, udt_name, ordinal_position
  83. FROM information_schema.columns
  84. WHERE table_name = 'orders'
  85. ORDER BY ordinal_position`)
  86. want := [][]string{
  87. {"orders", "id", "integer", "NO", "int4", "1"},
  88. {"orders", "account_id", "integer", "NO", "int4", "2"},
  89. {"orders", "note", "text", "YES", "text", "3"},
  90. {"orders", "tags", "ARRAY", "YES", "_text", "4"},
  91. {"orders", "meta", "jsonb", "YES", "jsonb", "5"},
  92. }
  93. if fmt.Sprint(rows) != fmt.Sprint(want) {
  94. t.Fatalf("information_schema.columns = %v, want %v", rows, want)
  95. }
  96. // Schema names are case-insensitive, as in PostgreSQL.
  97. res := execMust(t, e, `SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'orders'`)
  98. if res.Rows[0][0] != int64(1) {
  99. t.Fatalf("uppercase information schema lookup = %v, want 1", res.Rows[0][0])
  100. }
  101. res = execMust(t, e, `
  102. SELECT count(*) FROM information_schema.tables
  103. WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`)
  104. if res.Rows[0][0] != int64(3) {
  105. t.Fatalf("base table count = %v, want 3", res.Rows[0][0])
  106. }
  107. res = execMust(t, e, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'active_orders' AND table_type = 'VIEW'`)
  108. if res.Rows[0][0] != int64(1) {
  109. t.Fatalf("view count = %v, want 1", res.Rows[0][0])
  110. }
  111. res = execMust(t, e, `SELECT count(*) FROM information_schema.sequences`)
  112. if res.Rows[0][0] != int64(0) {
  113. t.Fatalf("sequence count = %v, want 0", res.Rows[0][0])
  114. }
  115. }
  116. func TestPGCatalogIndexRelations(t *testing.T) {
  117. e := setupPGCatalogFixture(t)
  118. rows := catalogRows(t, e, `
  119. SELECT c.relname, i.indisunique, i.indisprimary, i.indkey
  120. FROM pg_catalog.pg_index i
  121. JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
  122. JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
  123. WHERE t.relname = 'orders'
  124. ORDER BY c.relname`)
  125. want := [][]string{
  126. {"idx_orders_note", "false", "false", "{3}"},
  127. {"orders_pkey", "true", "true", "{1}"},
  128. }
  129. if fmt.Sprint(rows) != fmt.Sprint(want) {
  130. t.Fatalf("pg_index = %v, want %v", rows, want)
  131. }
  132. rows = catalogRows(t, e, `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders' ORDER BY indexname`)
  133. if len(rows) != 2 {
  134. t.Fatalf("pg_indexes rows = %v, want 2", rows)
  135. }
  136. if !strings.Contains(rows[0][1], "CREATE INDEX idx_orders_note ON public.orders USING btree (note)") {
  137. t.Fatalf("indexdef = %q", rows[0][1])
  138. }
  139. if !strings.Contains(rows[1][1], "CREATE UNIQUE INDEX orders_pkey ON public.orders USING btree (id)") {
  140. t.Fatalf("indexdef = %q", rows[1][1])
  141. }
  142. // Composite keys keep declaration order in pg_index.indkey.
  143. rows = catalogRows(t, e, `
  144. SELECT i.indkey FROM pg_index i
  145. JOIN pg_class c ON c.oid = i.indexrelid
  146. WHERE c.relname = 'pairs_pkey'`)
  147. if len(rows) != 1 || rows[0][0] != "{1,2}" {
  148. t.Fatalf("composite indkey = %v, want {1,2}", rows)
  149. }
  150. }
  151. func TestPGCatalogConstraints(t *testing.T) {
  152. e := setupPGCatalogFixture(t)
  153. rows := catalogRows(t, e, `
  154. SELECT conname, contype, conkey, confkey, pg_get_constraintdef(oid)
  155. FROM pg_constraint
  156. WHERE conrelid = 'orders'::regclass
  157. ORDER BY conname`)
  158. want := [][]string{
  159. {"fk_orders_account", "f", "{2}", "{1}", "FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE"},
  160. {"orders_check", "c", "<nil>", "<nil>", "CHECK (((id > 0)))"},
  161. {"orders_pkey", "p", "{1}", "<nil>", "PRIMARY KEY (id)"},
  162. }
  163. if fmt.Sprint(rows) != fmt.Sprint(want) {
  164. t.Fatalf("pg_constraint = %v, want %v", rows, want)
  165. }
  166. rows = catalogRows(t, e, `SELECT conname, contype FROM pg_constraint WHERE conrelid = 'accounts'::regclass ORDER BY conname`)
  167. want = [][]string{
  168. {"accounts_pkey", "p"},
  169. {"uniq_accounts_email", "u"},
  170. }
  171. if fmt.Sprint(rows) != fmt.Sprint(want) {
  172. t.Fatalf("account constraints = %v, want %v", rows, want)
  173. }
  174. rows = catalogRows(t, e, `
  175. SELECT constraint_name, constraint_type FROM information_schema.table_constraints
  176. WHERE table_name = 'orders' ORDER BY constraint_name`)
  177. want = [][]string{
  178. {"fk_orders_account", "FOREIGN KEY"},
  179. {"orders_check", "CHECK"},
  180. {"orders_pkey", "PRIMARY KEY"},
  181. }
  182. if fmt.Sprint(rows) != fmt.Sprint(want) {
  183. t.Fatalf("table_constraints = %v, want %v", rows, want)
  184. }
  185. rows = catalogRows(t, e, `
  186. SELECT constraint_name, column_name, ordinal_position, position_in_unique_constraint
  187. FROM information_schema.key_column_usage
  188. WHERE table_name = 'orders' ORDER BY constraint_name, ordinal_position`)
  189. want = [][]string{
  190. {"fk_orders_account", "account_id", "1", "1"},
  191. {"orders_pkey", "id", "1", "<nil>"},
  192. }
  193. if fmt.Sprint(rows) != fmt.Sprint(want) {
  194. t.Fatalf("key_column_usage = %v, want %v", rows, want)
  195. }
  196. rows = catalogRows(t, e, `
  197. SELECT constraint_name, unique_constraint_name, delete_rule, update_rule
  198. FROM information_schema.referential_constraints ORDER BY constraint_name`)
  199. want = [][]string{{"fk_orders_account", "accounts_pkey", "CASCADE", "NO ACTION"}}
  200. if fmt.Sprint(rows) != fmt.Sprint(want) {
  201. t.Fatalf("referential_constraints = %v, want %v", rows, want)
  202. }
  203. }
  204. // regclass is symbolic in PostgreSQL: a name literal resolves to the same OID
  205. // pg_class reports, and rendering the OID yields the name again.
  206. func TestPGCatalogRegclassRoundTrip(t *testing.T) {
  207. e := setupPGCatalogFixture(t)
  208. rows := catalogRows(t, e, `
  209. SELECT c.relname, c.oid, 'orders'::regclass, c.oid::regclass
  210. FROM pg_class c WHERE c.relname = 'orders'`)
  211. if len(rows) != 1 {
  212. t.Fatalf("rows = %v", rows)
  213. }
  214. if rows[0][0] != "orders" || rows[0][2] != "orders" || rows[0][3] != "orders" {
  215. t.Fatalf("regclass rendering = %v", rows)
  216. }
  217. // Text output of a regclass value is the relation name, as in PostgreSQL;
  218. // ::oid exposes the numeric OID for comparisons.
  219. rows = catalogRows(t, e, `
  220. SELECT 'orders'::regclass::text,
  221. 'orders'::regclass::oid = (SELECT oid FROM pg_class WHERE relname = 'orders'),
  222. (SELECT oid FROM pg_class WHERE relname = 'orders') = 'orders'::regclass`)
  223. if rows[0][0] != "orders" || rows[0][1] != "true" || rows[0][2] != "true" {
  224. t.Fatalf("regclass text/comparison = %v", rows)
  225. }
  226. // regtype resolves the built-in type table.
  227. rows = catalogRows(t, e, `SELECT 'integer'::regtype, 'integer'::regtype = 'int4'::regtype`)
  228. if rows[0][0] != "integer" || rows[0][1] != "true" {
  229. t.Fatalf("regtype = %v", rows)
  230. }
  231. if _, err := execSQL(e, `SELECT 'missing_relation'::regclass`); err == nil {
  232. t.Fatal("unknown relation cast must fail")
  233. }
  234. }
  235. func TestPGCatalogIntrospectionFunctions(t *testing.T) {
  236. e := setupPGCatalogFixture(t)
  237. rows := catalogRows(t, e, `
  238. SELECT current_schema(), current_database(),
  239. pg_table_is_visible('orders'::regclass),
  240. has_table_privilege('orders', 'SELECT'),
  241. has_table_privilege('missing_table', 'SELECT')`)
  242. if rows[0][0] != "public" || rows[0][1] != "testdb" || rows[0][2] != "true" || rows[0][3] != "true" || rows[0][4] != "false" {
  243. t.Fatalf("identity/privilege functions = %v", rows)
  244. }
  245. rows = catalogRows(t, e, `
  246. SELECT pg_get_serial_sequence('orders', 'id'),
  247. obj_description('orders'::regclass, 'pg_class'),
  248. col_description('orders'::regclass, 1)`)
  249. if len(rows) != 1 || rows[0][0] != "<nil>" || rows[0][1] != "<nil>" || rows[0][2] != "<nil>" {
  250. t.Fatalf("description functions = %v, want NULLs", rows)
  251. }
  252. rows = catalogRows(t, e, `
  253. SELECT pg_get_expr(d.adbin, d.adrelid)
  254. FROM pg_attrdef d WHERE d.adrelid = 'orders'::regclass`)
  255. if len(rows) != 1 || rows[0][0] != "'new'" {
  256. t.Fatalf("pg_get_expr = %v, want the stored default text", rows)
  257. }
  258. rows = catalogRows(t, e, `SELECT pg_catalog.version(), current_setting('search_path')`)
  259. if !strings.Contains(rows[0][0], "PostgreSQL") || rows[0][1] != "public" {
  260. t.Fatalf("version/current_setting = %v", rows)
  261. }
  262. rows = catalogRows(t, e, `SELECT format_type(23, -1), format_type(1043, 14), format_type(999999, -1)`)
  263. if rows[0][0] != "integer" || rows[0][1] != "character varying(10)" || rows[0][2] != "???" {
  264. t.Fatalf("format_type = %v", rows)
  265. }
  266. }
  267. // A psql-style column listing combines the catalog relations with LEFT JOINs;
  268. // it must return one row per live attribute and mark the primary key.
  269. func TestPGCatalogPsqlStyleDescribe(t *testing.T) {
  270. e := setupPGCatalogFixture(t)
  271. rows := catalogRows(t, e, `
  272. SELECT a.attname,
  273. pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
  274. a.attnotnull AS notnull,
  275. pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS dflt,
  276. i.indisprimary AS is_pk
  277. FROM pg_catalog.pg_class c
  278. JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  279. JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  280. LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
  281. LEFT JOIN pg_catalog.pg_index i ON i.indrelid = c.oid AND a.attnum = ANY(i.indkey)
  282. WHERE c.relname = 'orders' AND c.relkind = 'r' AND n.nspname = 'public'
  283. AND a.attnum > 0 AND NOT a.attisdropped
  284. ORDER BY a.attnum`)
  285. want := [][]string{
  286. {"id", "integer", "true", "<nil>", "true"},
  287. {"account_id", "integer", "true", "<nil>", "<nil>"},
  288. {"note", "text", "false", "'new'", "false"},
  289. {"tags", "text[]", "false", "<nil>", "<nil>"},
  290. {"meta", "jsonb", "false", "<nil>", "<nil>"},
  291. }
  292. if fmt.Sprint(rows) != fmt.Sprint(want) {
  293. t.Fatalf("psql-style describe = %v, want %v", rows, want)
  294. }
  295. }
  296. // Catalog rows reflect DDL immediately, and transactional DDL is visible only
  297. // to the connection that staged it.
  298. func TestPGCatalogTransactionalDDLVisibility(t *testing.T) {
  299. _, schema, table := newTestDB(t)
  300. writer := newExec(schema, table)
  301. observer := newExec(schema, table)
  302. execMust(t, writer, "CREATE TABLE base (id INTEGER PRIMARY KEY)")
  303. execMust(t, writer, "BEGIN")
  304. execMust(t, writer, "CREATE TABLE staged (id INTEGER PRIMARY KEY)")
  305. execMust(t, writer, "ALTER TABLE base ADD COLUMN note TEXT")
  306. execMust(t, writer, "CREATE INDEX idx_base_note ON base (note)")
  307. res := execMust(t, writer, `SELECT table_name FROM information_schema.tables WHERE table_name = 'staged'`)
  308. if len(res.Rows) != 1 {
  309. t.Fatalf("writer does not see staged table: %v", res.Rows)
  310. }
  311. res = execMust(t, writer, `
  312. SELECT column_name FROM information_schema.columns
  313. WHERE table_name = 'base' ORDER BY ordinal_position`)
  314. if len(res.Rows) != 2 || res.Rows[1][0] != "note" {
  315. t.Fatalf("writer does not see staged column: %v", res.Rows)
  316. }
  317. res = execMust(t, writer, `SELECT count(*) FROM pg_indexes WHERE tablename = 'base' AND indexname = 'idx_base_note'`)
  318. if res.Rows[0][0] != int64(1) {
  319. t.Fatalf("writer does not see staged index: %v", res.Rows)
  320. }
  321. res = execMust(t, observer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
  322. if res.Rows[0][0] != int64(0) {
  323. t.Fatalf("observer sees staged table: %v", res.Rows)
  324. }
  325. res = execMust(t, observer, `SELECT count(*) FROM information_schema.columns WHERE table_name = 'base'`)
  326. if res.Rows[0][0] != int64(1) {
  327. t.Fatalf("observer sees staged column: %v", res.Rows)
  328. }
  329. execMust(t, writer, "ROLLBACK")
  330. res = execMust(t, writer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
  331. if res.Rows[0][0] != int64(0) {
  332. t.Fatalf("rolled-back table still visible: %v", res.Rows)
  333. }
  334. res = execMust(t, writer, `SELECT count(*) FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid WHERE c.relname = 'base'`)
  335. if res.Rows[0][0] != int64(1) {
  336. t.Fatalf("rolled-back column still visible: %v", res.Rows)
  337. }
  338. }
  339. // OIDs are deterministic per database, so two connections and repeated
  340. // statements agree.
  341. func TestPGCatalogOIDsAreDeterministic(t *testing.T) {
  342. _, schema, table := newTestDB(t)
  343. first := newExec(schema, table)
  344. execMust(t, first, "CREATE TABLE deterministic (id INTEGER PRIMARY KEY, name TEXT)")
  345. query := `SELECT oid, relname FROM pg_class WHERE relname = 'deterministic'`
  346. one := catalogRows(t, first, query)
  347. two := catalogRows(t, first, query)
  348. if fmt.Sprint(one) != fmt.Sprint(two) {
  349. t.Fatalf("OID changed between statements: %v vs %v", one, two)
  350. }
  351. second := newExec(schema, table)
  352. three := catalogRows(t, second, query)
  353. if fmt.Sprint(one) != fmt.Sprint(three) {
  354. t.Fatalf("OID differs across connections: %v vs %v", one, three)
  355. }
  356. if len(one) != 1 || one[0][1] != "deterministic" {
  357. t.Fatalf("unexpected relation row: %v", one)
  358. }
  359. }
  360. func TestPGCatalogRejectsWrites(t *testing.T) {
  361. e := setupPGCatalogFixture(t)
  362. for _, sql := range []string{
  363. `INSERT INTO pg_catalog.pg_class VALUES (1)`,
  364. `INSERT INTO information_schema.tables VALUES ('x')`,
  365. `UPDATE pg_catalog.pg_type SET typname = 'x'`,
  366. `DELETE FROM pg_catalog.pg_attribute`,
  367. `CREATE INDEX idx_pg_class_name ON pg_class (relname)`,
  368. `ALTER TABLE pg_class ADD COLUMN nope TEXT`,
  369. `DROP TABLE pg_catalog.pg_class`,
  370. `CREATE VIEW pg_class AS SELECT 1`,
  371. } {
  372. _, err := execSQL(e, sql)
  373. if !errors.Is(err, pgerror.ErrFeatureNotSupported) {
  374. t.Fatalf("%s: err = %v, want 0A000 feature_not_supported", sql, err)
  375. }
  376. }
  377. }
  378. func TestPGSearchPathSettings(t *testing.T) {
  379. e := setupPGCatalogFixture(t)
  380. for _, sql := range []string{
  381. "SET search_path TO public",
  382. "SET search_path = public",
  383. "SET client_encoding TO 'UTF8'",
  384. "SET TIME ZONE 'UTC'",
  385. "SET statement_timeout = 0",
  386. "SET NAMES 'utf8'",
  387. "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED",
  388. } {
  389. if _, err := execSQL(e, sql); err != nil {
  390. t.Fatalf("%s: %v", sql, err)
  391. }
  392. }
  393. res := execMust(t, e, "SHOW search_path")
  394. if len(res.Rows) != 1 || res.Rows[0][0] != "public" {
  395. t.Fatalf("SHOW search_path = %v", res.Rows)
  396. }
  397. res = execMust(t, e, "SHOW server_version")
  398. if len(res.Rows) != 1 || res.Rows[0][0] == "" {
  399. t.Fatalf("SHOW server_version = %v", res.Rows)
  400. }
  401. if _, err := execSQL(e, "SHOW definitely_not_a_setting"); err == nil {
  402. t.Fatal("unknown SHOW setting must fail")
  403. }
  404. }
  405. // The SQLite introspection surface keeps working alongside the PostgreSQL
  406. // catalogs; sqlite_master must not list the virtual relations.
  407. func TestPGCatalogDoesNotChangeSQLiteIntrospection(t *testing.T) {
  408. e := setupPGCatalogFixture(t)
  409. res := execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)
  410. names := make([]string, len(res.Rows))
  411. for i, row := range res.Rows {
  412. names[i] = fmt.Sprintf("%v", row[0])
  413. }
  414. if strings.Join(names, ",") != "accounts,orders,pairs" {
  415. t.Fatalf("sqlite_master tables = %v", names)
  416. }
  417. res = execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name`)
  418. if len(res.Rows) != 2 {
  419. t.Fatalf("sqlite_master indexes = %v", res.Rows)
  420. }
  421. res = execMust(t, e, `PRAGMA table_info(orders)`)
  422. if len(res.Rows) != 5 {
  423. t.Fatalf("PRAGMA table_info rows = %v", res.Rows)
  424. }
  425. }
  426. // An index may share its table's name. pg_class then holds two rows with the
  427. // same relname, so relkind is part of the relation identity: the rows keep
  428. // distinct OIDs and pg_index/pg_attribute stay consistent with each of them.
  429. func TestPGCatalogIndexSharingTableName(t *testing.T) {
  430. _, schema, table := newTestDB(t)
  431. e := newExec(schema, table)
  432. execMust(t, e, "CREATE TABLE shared (id INTEGER PRIMARY KEY, label TEXT)")
  433. execMust(t, e, "CREATE INDEX shared ON shared (label)")
  434. rows := catalogRows(t, e, `SELECT relkind, oid FROM pg_class WHERE relname = 'shared' ORDER BY relkind`)
  435. if len(rows) != 2 {
  436. t.Fatalf("pg_class rows for shared = %v, want a table and an index", rows)
  437. }
  438. if rows[0][0] != "i" || rows[1][0] != "r" {
  439. t.Fatalf("pg_class row kinds = %v, want i then r", rows)
  440. }
  441. if rows[0][1] == rows[1][1] {
  442. t.Fatalf("table and index share OID %v", rows[0][1])
  443. }
  444. // pg_index must point the index row at the table row, not at itself.
  445. rows = catalogRows(t, e, `
  446. SELECT c.relname, i.indexrelid, i.indrelid, i.indrelid = t.oid
  447. FROM pg_index i
  448. JOIN pg_class c ON c.oid = i.indexrelid
  449. JOIN pg_class t ON t.oid = i.indrelid
  450. WHERE c.relname = 'shared'`)
  451. if len(rows) != 1 || rows[0][0] != "shared" || rows[0][1] == rows[0][2] || rows[0][3] != "true" {
  452. t.Fatalf("pg_index row = %v, want indexrelid != indrelid = t.oid", rows)
  453. }
  454. // The table's attributes belong to exactly one relation, so the index row
  455. // must not double them.
  456. res := execMust(t, e, `
  457. SELECT count(*) FROM pg_attribute a
  458. JOIN pg_class c ON c.oid = a.attrelid
  459. WHERE c.relname = 'shared' AND c.relkind = 'r' AND a.attnum > 0`)
  460. if res.Rows[0][0] != int64(2) {
  461. t.Fatalf("pg_attribute rows for the shared table = %v, want 2", res.Rows[0][0])
  462. }
  463. // regclass resolution prefers the table when the names collide.
  464. res = execMust(t, e, `SELECT 'shared'::regclass::oid = (SELECT oid FROM pg_class WHERE relname = 'shared' AND relkind = 'r')`)
  465. if res.Rows[0][0] != true {
  466. t.Fatalf("regclass did not resolve to the table: %v", res.Rows)
  467. }
  468. }
  469. // A durable table named <x>_pkey occupies the synthetic primary-key index name,
  470. // so the synthesized index must pick a free name instead of colliding with it.
  471. func TestPGCatalogSyntheticPrimaryKeyNameCollision(t *testing.T) {
  472. _, schema, table := newTestDB(t)
  473. e := newExec(schema, table)
  474. execMust(t, e, "CREATE TABLE owner (id INTEGER PRIMARY KEY)")
  475. execMust(t, e, "CREATE TABLE owner_pkey (id INTEGER PRIMARY KEY)")
  476. rows := catalogRows(t, e, `SELECT relname, relkind FROM pg_class WHERE relname LIKE 'owner%' ORDER BY relname, relkind`)
  477. want := [][]string{
  478. {"owner", "r"},
  479. {"owner_pkey", "r"},
  480. {"owner_pkey2", "i"},
  481. {"owner_pkey_pkey", "i"},
  482. }
  483. if fmt.Sprint(rows) != fmt.Sprint(want) {
  484. t.Fatalf("pg_class rows = %v, want %v", rows, want)
  485. }
  486. // The primary key constraint still points at the renamed synthetic index.
  487. rows = catalogRows(t, e, `
  488. SELECT con.conname, c.relname
  489. FROM pg_constraint con
  490. JOIN pg_class c ON c.oid = con.conindid
  491. WHERE con.conrelid = 'owner'::regclass AND con.contype = 'p'`)
  492. if len(rows) != 1 || rows[0][1] != "owner_pkey2" {
  493. t.Fatalf("primary key index = %v, want owner_pkey2", rows)
  494. }
  495. // No name is reported twice with the same relkind.
  496. res := execMust(t, e, `
  497. SELECT count(*) FROM (
  498. SELECT relname, relkind, count(*) AS n FROM pg_class GROUP BY relname, relkind
  499. ) d WHERE d.n > 1`)
  500. if res.Rows[0][0] != int64(0) {
  501. t.Fatalf("pg_class contains duplicate relname/relkind rows: %v", res.Rows)
  502. }
  503. }
  504. // An unknown regclass or regtype reports undefined_table (42P01) instead of an
  505. // internal error, and it fails the statement even inside a WHERE clause.
  506. // to_regclass/to_regtype are the NULL-returning variants.
  507. func TestPGCatalogUndefinedRelationError(t *testing.T) {
  508. e := setupPGCatalogFixture(t)
  509. for _, sql := range []string{
  510. `SELECT 'missing_relation'::regclass`,
  511. `SELECT 'missing_type'::regtype`,
  512. `SELECT count(*) FROM pg_class WHERE oid = 'missing_relation'::regclass`,
  513. `SELECT count(*) FROM pg_class c JOIN pg_attribute a ON a.attrelid = c.oid WHERE a.attrelid = 'missing_relation'::regclass`,
  514. } {
  515. _, err := execSQL(e, sql)
  516. if !errors.Is(err, pgerror.ErrUndefinedTable) {
  517. t.Fatalf("%s: err = %v, want an undefined_table error", sql, err)
  518. }
  519. }
  520. rows := catalogRows(t, e, `SELECT to_regclass('orders'), to_regclass('missing_relation'), to_regtype('missing_type')`)
  521. if len(rows) != 1 || rows[0][0] != "orders" || rows[0][1] != "<nil>" || rows[0][2] != "<nil>" {
  522. t.Fatalf("to_regclass/to_regtype = %v", rows)
  523. }
  524. res := execMust(t, e, `
  525. SELECT to_regclass('orders')::oid = (SELECT oid FROM pg_class WHERE relname = 'orders' AND relkind = 'r')`)
  526. if res.Rows[0][0] != true {
  527. t.Fatalf("to_regclass OID = %v, want the pg_class OID", res.Rows)
  528. }
  529. res = execMust(t, e, `SELECT to_regclass('orders')`)
  530. if fmt.Sprint(res.ColumnTypes) != "[REGCLASS]" {
  531. t.Fatalf("to_regclass result type = %v, want REGCLASS", res.ColumnTypes)
  532. }
  533. }
  534. // A CTE shadows a virtual catalog relation of the same name, exactly as a CTE
  535. // shadows a table.
  536. func TestPGCatalogCTEShadowsVirtualRelation(t *testing.T) {
  537. e := setupPGCatalogFixture(t)
  538. rows := catalogRows(t, e, `WITH pg_attribute AS (SELECT 7 AS attnum) SELECT * FROM pg_attribute`)
  539. if len(rows) != 1 || rows[0][0] != "7" {
  540. t.Fatalf("non-recursive CTE did not shadow pg_attribute: %v", rows)
  541. }
  542. rows = catalogRows(t, e, `
  543. WITH RECURSIVE pg_class(oid) AS (SELECT 42 UNION ALL SELECT oid + 1 FROM pg_class WHERE oid < 44)
  544. SELECT * FROM pg_class`)
  545. if len(rows) != 3 || rows[0][0] != "42" || rows[2][0] != "44" {
  546. t.Fatalf("recursive CTE did not shadow pg_class: %v", rows)
  547. }
  548. res := execMust(t, e, `
  549. WITH pg_attribute(attname) AS (SELECT 'shadowed')
  550. SELECT count(*) FROM pg_catalog.pg_attribute`)
  551. if res.Rows[0][0] == int64(1) {
  552. t.Fatalf("CTE shadowed an explicitly qualified catalog relation")
  553. }
  554. }
  555. // A schema qualifier only resolves inside the schema it names: public.pg_class
  556. // is not pg_catalog.pg_class, so it is an undefined table.
  557. func TestPGCatalogQualifiedNameStaysInItsSchema(t *testing.T) {
  558. e := setupPGCatalogFixture(t)
  559. for _, sql := range []string{
  560. `SELECT count(*) FROM public.pg_class`,
  561. `SELECT count(*) FROM public.pg_attribute`,
  562. } {
  563. _, err := execSQL(e, sql)
  564. if !errors.Is(err, pgerror.ErrUndefinedTable) {
  565. t.Fatalf("%s: err = %v, want an undefined_table error", sql, err)
  566. }
  567. }
  568. // A real public table still resolves through the qualifier.
  569. res := execMust(t, e, `SELECT count(*) FROM public.orders`)
  570. if res.Rows[0][0] != int64(0) {
  571. t.Fatalf("public.orders rows = %v", res.Rows)
  572. }
  573. if res := execMust(t, e, `SELECT count(*) FROM pg_catalog.pg_class`); res.Rows[0][0] == int64(0) {
  574. t.Fatal("pg_catalog.pg_class is empty")
  575. }
  576. }
  577. // A view listed by information_schema is listed everywhere else with the same
  578. // shape: pg_class relkind 'v', its pg_attribute rows, and its columns.
  579. func TestPGCatalogViewShape(t *testing.T) {
  580. e := setupPGCatalogFixture(t)
  581. rows := catalogRows(t, e, `SELECT relname, relkind, relnatts FROM pg_class WHERE relname = 'active_orders'`)
  582. if len(rows) != 1 || rows[0][1] != "v" || rows[0][2] != "2" {
  583. t.Fatalf("pg_class view row = %v", rows)
  584. }
  585. rows = catalogRows(t, e, `
  586. SELECT a.attnum, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod)
  587. FROM pg_attribute a
  588. JOIN pg_class c ON c.oid = a.attrelid
  589. WHERE c.relname = 'active_orders' AND c.relkind = 'v' AND a.attnum > 0
  590. ORDER BY a.attnum`)
  591. want := [][]string{{"1", "id", "integer"}, {"2", "note", "text"}}
  592. if fmt.Sprint(rows) != fmt.Sprint(want) {
  593. t.Fatalf("view attributes = %v, want %v", rows, want)
  594. }
  595. rows = catalogRows(t, e, `
  596. SELECT table_type, is_insertable_into FROM information_schema.tables WHERE table_name = 'active_orders'`)
  597. if len(rows) != 1 || rows[0][0] != "VIEW" || rows[0][1] != "NO" {
  598. t.Fatalf("information_schema.tables view row = %v", rows)
  599. }
  600. rows = catalogRows(t, e, `
  601. SELECT column_name, data_type, ordinal_position
  602. FROM information_schema.columns WHERE table_name = 'active_orders'
  603. ORDER BY ordinal_position`)
  604. want = [][]string{{"id", "integer", "1"}, {"note", "text", "2"}}
  605. if fmt.Sprint(rows) != fmt.Sprint(want) {
  606. t.Fatalf("information_schema.columns view rows = %v, want %v", rows, want)
  607. }
  608. res := execMust(t, e, `SELECT count(*) FROM information_schema.views WHERE table_name = 'active_orders'`)
  609. if res.Rows[0][0] != int64(1) {
  610. t.Fatalf("information_schema.views rows = %v, want 1", res.Rows[0][0])
  611. }
  612. // Every view information_schema lists has the same number of columns in
  613. // pg_attribute and information_schema.columns.
  614. res = execMust(t, e, `
  615. SELECT count(*) FROM information_schema.tables t
  616. WHERE t.table_type = 'VIEW' AND t.table_schema = 'public'
  617. AND (SELECT count(*) FROM information_schema.columns c
  618. WHERE c.table_name = t.table_name)
  619. <> (SELECT relnatts FROM pg_class c
  620. WHERE c.relname = t.table_name AND c.relkind = 'v')`)
  621. if res.Rows[0][0] != int64(0) {
  622. t.Fatalf("a public view has an inconsistent catalog shape: %v", res.Rows[0][0])
  623. }
  624. }
  625. // pg_type's function attributes are regproc columns holding 0, not the text "-".
  626. func TestPGCatalogTypeRegprocAttributes(t *testing.T) {
  627. e := setupPGCatalogFixture(t)
  628. rows := catalogRows(t, e, `
  629. SELECT typname, typinput, typoutput, typreceive, typsend, typmodin, typanalyze
  630. FROM pg_type WHERE typname = 'int4'`)
  631. want := [][]string{{"int4", "0", "0", "0", "0", "0", "0"}}
  632. if fmt.Sprint(rows) != fmt.Sprint(want) {
  633. t.Fatalf("pg_type regproc attributes = %v, want %v", rows, want)
  634. }
  635. res := execMust(t, e, `SELECT typinput, typoutput FROM pg_type WHERE typname = 'text'`)
  636. if fmt.Sprint(res.ColumnTypes) != "[REGPROC REGPROC]" {
  637. t.Fatalf("pg_type regproc column types = %v", res.ColumnTypes)
  638. }
  639. }
  640. // search_path follows PostgreSQL's transaction rules: SET LOCAL expires at
  641. // COMMIT (and on ROLLBACK TO SAVEPOINT), and a session SET inside an aborted
  642. // transaction is undone by ROLLBACK.
  643. func TestPGSearchPathTransactionScope(t *testing.T) {
  644. e := setupPGCatalogFixture(t)
  645. showPath := func() string {
  646. t.Helper()
  647. res := execMust(t, e, "SHOW search_path")
  648. if len(res.Rows) != 1 {
  649. t.Fatalf("SHOW search_path = %v", res.Rows)
  650. }
  651. return fmt.Sprintf("%v", res.Rows[0][0])
  652. }
  653. // SET LOCAL outside a transaction block has no effect.
  654. execMust(t, e, "SET LOCAL search_path TO pg_catalog")
  655. if got := showPath(); got != "public" {
  656. t.Fatalf("search_path after SET LOCAL outside a transaction = %q", got)
  657. }
  658. execMust(t, e, "BEGIN")
  659. execMust(t, e, "SET LOCAL search_path TO pg_catalog")
  660. if got := showPath(); got != "pg_catalog" {
  661. t.Fatalf("search_path inside the transaction = %q", got)
  662. }
  663. if res := execMust(t, e, "SELECT current_schema()"); res.Rows[0][0] != "pg_catalog" {
  664. t.Fatalf("current_schema() inside the transaction = %v", res.Rows[0][0])
  665. }
  666. execMust(t, e, "COMMIT")
  667. if got := showPath(); got != "public" {
  668. t.Fatalf("search_path after COMMIT = %q, want SET LOCAL to expire", got)
  669. }
  670. // A session SET is transactional: ROLLBACK restores the previous value,
  671. // COMMIT keeps it.
  672. execMust(t, e, "BEGIN")
  673. execMust(t, e, "SET search_path TO pg_catalog")
  674. execMust(t, e, "ROLLBACK")
  675. if got := showPath(); got != "public" {
  676. t.Fatalf("search_path after ROLLBACK = %q, want the pre-transaction value", got)
  677. }
  678. execMust(t, e, "BEGIN")
  679. execMust(t, e, "SET search_path TO pg_catalog")
  680. execMust(t, e, "COMMIT")
  681. if got := showPath(); got != "pg_catalog" {
  682. t.Fatalf("search_path after COMMIT = %q, want the session SET to persist", got)
  683. }
  684. execMust(t, e, "SET search_path TO public")
  685. // ROLLBACK TO SAVEPOINT undoes a SET LOCAL issued after the savepoint.
  686. execMust(t, e, "BEGIN")
  687. execMust(t, e, "SAVEPOINT sp")
  688. execMust(t, e, "SET LOCAL search_path TO pg_catalog")
  689. execMust(t, e, "ROLLBACK TO SAVEPOINT sp")
  690. if got := showPath(); got != "public" {
  691. t.Fatalf("search_path after ROLLBACK TO SAVEPOINT = %q", got)
  692. }
  693. execMust(t, e, "COMMIT")
  694. }