pg_catalog_test.go 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457
  1. package executor
  2. import (
  3. "fmt"
  4. "strings"
  5. "testing"
  6. )
  7. // setupPGCatalogFixture creates a schema that exercises every catalog-backed
  8. // object shape: single and composite primary keys, UNIQUE, FOREIGN KEY, CHECK,
  9. // explicit indexes, arrays, JSONB, defaults, and a view.
  10. func setupPGCatalogFixture(t *testing.T) *Executor {
  11. t.Helper()
  12. _, schema, table := newTestDB(t)
  13. e := newExec(schema, table)
  14. for _, sql := range []string{
  15. "CREATE TABLE accounts (id INTEGER PRIMARY KEY, email TEXT NOT NULL UNIQUE)",
  16. "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)",
  17. "CREATE INDEX idx_orders_note ON orders (note)",
  18. "CREATE TABLE pairs (a TEXT, b TEXT, v TEXT, PRIMARY KEY (a, b))",
  19. "CREATE VIEW active_orders AS SELECT id, note FROM orders",
  20. } {
  21. execMust(t, e, sql)
  22. }
  23. return e
  24. }
  25. func catalogCell(t *testing.T, res *Result, row, col int) string {
  26. t.Helper()
  27. if row >= len(res.Rows) || col >= len(res.Rows[row]) {
  28. t.Fatalf("row %d column %d out of range: %v", row, col, res.Rows)
  29. }
  30. value := res.Rows[row][col]
  31. if value == nil {
  32. return "<nil>"
  33. }
  34. return fmt.Sprintf("%v", value)
  35. }
  36. func catalogRows(t *testing.T, e *Executor, query string) [][]string {
  37. t.Helper()
  38. res, err := execSQL(e, query)
  39. if err != nil {
  40. t.Fatalf("%s: %v", query, err)
  41. }
  42. rows := make([][]string, len(res.Rows))
  43. for i := range res.Rows {
  44. rows[i] = make([]string, len(res.Rows[i]))
  45. for j, value := range res.Rows[i] {
  46. if value == nil {
  47. rows[i][j] = "<nil>"
  48. continue
  49. }
  50. rows[i][j] = fmt.Sprintf("%v", value)
  51. }
  52. }
  53. return rows
  54. }
  55. // A PostgreSQL-style column listing joins pg_class, pg_namespace, and
  56. // pg_attribute; the synthetic OIDs must line up across all three.
  57. func TestPGCatalogJoinListsColumns(t *testing.T) {
  58. e := setupPGCatalogFixture(t)
  59. rows := catalogRows(t, e, `
  60. SELECT n.nspname, c.relname, a.attname, pg_catalog.format_type(a.atttypid, a.atttypmod), a.attnotnull
  61. FROM pg_catalog.pg_class c
  62. JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  63. JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  64. WHERE c.relname = 'orders' AND n.nspname = 'public' AND a.attnum > 0 AND NOT a.attisdropped
  65. ORDER BY a.attnum`)
  66. want := [][]string{
  67. {"public", "orders", "id", "integer", "true"},
  68. {"public", "orders", "account_id", "integer", "true"},
  69. {"public", "orders", "note", "text", "false"},
  70. {"public", "orders", "tags", "text[]", "false"},
  71. {"public", "orders", "meta", "jsonb", "false"},
  72. }
  73. if fmt.Sprint(rows) != fmt.Sprint(want) {
  74. t.Fatalf("columns = %v, want %v", rows, want)
  75. }
  76. }
  77. func TestPGCatalogInformationSchemaColumns(t *testing.T) {
  78. e := setupPGCatalogFixture(t)
  79. rows := catalogRows(t, e, `
  80. SELECT table_name, column_name, data_type, is_nullable, udt_name, ordinal_position
  81. FROM information_schema.columns
  82. WHERE table_name = 'orders'
  83. ORDER BY ordinal_position`)
  84. want := [][]string{
  85. {"orders", "id", "integer", "NO", "int4", "1"},
  86. {"orders", "account_id", "integer", "NO", "int4", "2"},
  87. {"orders", "note", "text", "YES", "text", "3"},
  88. {"orders", "tags", "ARRAY", "YES", "_text", "4"},
  89. {"orders", "meta", "jsonb", "YES", "jsonb", "5"},
  90. }
  91. if fmt.Sprint(rows) != fmt.Sprint(want) {
  92. t.Fatalf("information_schema.columns = %v, want %v", rows, want)
  93. }
  94. // Schema names are case-insensitive, as in PostgreSQL.
  95. res := execMust(t, e, `SELECT count(*) FROM INFORMATION_SCHEMA.TABLES WHERE table_name = 'orders'`)
  96. if res.Rows[0][0] != int64(1) {
  97. t.Fatalf("uppercase information schema lookup = %v, want 1", res.Rows[0][0])
  98. }
  99. res = execMust(t, e, `
  100. SELECT count(*) FROM information_schema.tables
  101. WHERE table_schema = 'public' AND table_type = 'BASE TABLE'`)
  102. if res.Rows[0][0] != int64(3) {
  103. t.Fatalf("base table count = %v, want 3", res.Rows[0][0])
  104. }
  105. res = execMust(t, e, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'active_orders' AND table_type = 'VIEW'`)
  106. if res.Rows[0][0] != int64(1) {
  107. t.Fatalf("view count = %v, want 1", res.Rows[0][0])
  108. }
  109. res = execMust(t, e, `SELECT count(*) FROM information_schema.sequences`)
  110. if res.Rows[0][0] != int64(0) {
  111. t.Fatalf("sequence count = %v, want 0", res.Rows[0][0])
  112. }
  113. }
  114. func TestPGCatalogIndexRelations(t *testing.T) {
  115. e := setupPGCatalogFixture(t)
  116. rows := catalogRows(t, e, `
  117. SELECT c.relname, i.indisunique, i.indisprimary, i.indkey
  118. FROM pg_catalog.pg_index i
  119. JOIN pg_catalog.pg_class c ON c.oid = i.indexrelid
  120. JOIN pg_catalog.pg_class t ON t.oid = i.indrelid
  121. WHERE t.relname = 'orders'
  122. ORDER BY c.relname`)
  123. want := [][]string{
  124. {"idx_orders_note", "false", "false", "{3}"},
  125. {"orders_pkey", "true", "true", "{1}"},
  126. }
  127. if fmt.Sprint(rows) != fmt.Sprint(want) {
  128. t.Fatalf("pg_index = %v, want %v", rows, want)
  129. }
  130. rows = catalogRows(t, e, `SELECT indexname, indexdef FROM pg_indexes WHERE tablename = 'orders' ORDER BY indexname`)
  131. if len(rows) != 2 {
  132. t.Fatalf("pg_indexes rows = %v, want 2", rows)
  133. }
  134. if !strings.Contains(rows[0][1], "CREATE INDEX idx_orders_note ON public.orders USING btree (note)") {
  135. t.Fatalf("indexdef = %q", rows[0][1])
  136. }
  137. if !strings.Contains(rows[1][1], "CREATE UNIQUE INDEX orders_pkey ON public.orders USING btree (id)") {
  138. t.Fatalf("indexdef = %q", rows[1][1])
  139. }
  140. // Composite keys keep declaration order in pg_index.indkey.
  141. rows = catalogRows(t, e, `
  142. SELECT i.indkey FROM pg_index i
  143. JOIN pg_class c ON c.oid = i.indexrelid
  144. WHERE c.relname = 'pairs_pkey'`)
  145. if len(rows) != 1 || rows[0][0] != "{1,2}" {
  146. t.Fatalf("composite indkey = %v, want {1,2}", rows)
  147. }
  148. }
  149. func TestPGCatalogConstraints(t *testing.T) {
  150. e := setupPGCatalogFixture(t)
  151. rows := catalogRows(t, e, `
  152. SELECT conname, contype, conkey, confkey, pg_get_constraintdef(oid)
  153. FROM pg_constraint
  154. WHERE conrelid = 'orders'::regclass
  155. ORDER BY conname`)
  156. want := [][]string{
  157. {"fk_orders_account", "f", "{2}", "{1}", "FOREIGN KEY (account_id) REFERENCES accounts(id) ON DELETE CASCADE"},
  158. {"orders_check", "c", "<nil>", "<nil>", "CHECK (((id > 0)))"},
  159. {"orders_pkey", "p", "{1}", "<nil>", "PRIMARY KEY (id)"},
  160. }
  161. if fmt.Sprint(rows) != fmt.Sprint(want) {
  162. t.Fatalf("pg_constraint = %v, want %v", rows, want)
  163. }
  164. rows = catalogRows(t, e, `SELECT conname, contype FROM pg_constraint WHERE conrelid = 'accounts'::regclass ORDER BY conname`)
  165. want = [][]string{
  166. {"accounts_pkey", "p"},
  167. {"uniq_accounts_email", "u"},
  168. }
  169. if fmt.Sprint(rows) != fmt.Sprint(want) {
  170. t.Fatalf("account constraints = %v, want %v", rows, want)
  171. }
  172. rows = catalogRows(t, e, `
  173. SELECT constraint_name, constraint_type FROM information_schema.table_constraints
  174. WHERE table_name = 'orders' ORDER BY constraint_name`)
  175. want = [][]string{
  176. {"fk_orders_account", "FOREIGN KEY"},
  177. {"orders_check", "CHECK"},
  178. {"orders_pkey", "PRIMARY KEY"},
  179. }
  180. if fmt.Sprint(rows) != fmt.Sprint(want) {
  181. t.Fatalf("table_constraints = %v, want %v", rows, want)
  182. }
  183. rows = catalogRows(t, e, `
  184. SELECT constraint_name, column_name, ordinal_position, position_in_unique_constraint
  185. FROM information_schema.key_column_usage
  186. WHERE table_name = 'orders' ORDER BY constraint_name, ordinal_position`)
  187. want = [][]string{
  188. {"fk_orders_account", "account_id", "1", "1"},
  189. {"orders_pkey", "id", "1", "<nil>"},
  190. }
  191. if fmt.Sprint(rows) != fmt.Sprint(want) {
  192. t.Fatalf("key_column_usage = %v, want %v", rows, want)
  193. }
  194. rows = catalogRows(t, e, `
  195. SELECT constraint_name, unique_constraint_name, delete_rule, update_rule
  196. FROM information_schema.referential_constraints ORDER BY constraint_name`)
  197. want = [][]string{{"fk_orders_account", "accounts_pkey", "CASCADE", "NO ACTION"}}
  198. if fmt.Sprint(rows) != fmt.Sprint(want) {
  199. t.Fatalf("referential_constraints = %v, want %v", rows, want)
  200. }
  201. }
  202. // regclass is symbolic in PostgreSQL: a name literal resolves to the same OID
  203. // pg_class reports, and rendering the OID yields the name again.
  204. func TestPGCatalogRegclassRoundTrip(t *testing.T) {
  205. e := setupPGCatalogFixture(t)
  206. rows := catalogRows(t, e, `
  207. SELECT c.relname, c.oid, 'orders'::regclass, c.oid::regclass
  208. FROM pg_class c WHERE c.relname = 'orders'`)
  209. if len(rows) != 1 {
  210. t.Fatalf("rows = %v", rows)
  211. }
  212. if rows[0][0] != "orders" || rows[0][2] != "orders" || rows[0][3] != "orders" {
  213. t.Fatalf("regclass rendering = %v", rows)
  214. }
  215. // Text output of a regclass value is the relation name, as in PostgreSQL;
  216. // ::oid exposes the numeric OID for comparisons.
  217. rows = catalogRows(t, e, `
  218. SELECT 'orders'::regclass::text,
  219. 'orders'::regclass::oid = (SELECT oid FROM pg_class WHERE relname = 'orders'),
  220. (SELECT oid FROM pg_class WHERE relname = 'orders') = 'orders'::regclass`)
  221. if rows[0][0] != "orders" || rows[0][1] != "true" || rows[0][2] != "true" {
  222. t.Fatalf("regclass text/comparison = %v", rows)
  223. }
  224. // regtype resolves the built-in type table.
  225. rows = catalogRows(t, e, `SELECT 'integer'::regtype, 'integer'::regtype = 'int4'::regtype`)
  226. if rows[0][0] != "integer" || rows[0][1] != "true" {
  227. t.Fatalf("regtype = %v", rows)
  228. }
  229. if _, err := execSQL(e, `SELECT 'missing_relation'::regclass`); err == nil {
  230. t.Fatal("unknown relation cast must fail")
  231. }
  232. }
  233. func TestPGCatalogIntrospectionFunctions(t *testing.T) {
  234. e := setupPGCatalogFixture(t)
  235. rows := catalogRows(t, e, `
  236. SELECT current_schema(), current_database(),
  237. pg_table_is_visible('orders'::regclass),
  238. has_table_privilege('orders', 'SELECT'),
  239. has_table_privilege('missing_table', 'SELECT')`)
  240. if rows[0][0] != "public" || rows[0][1] != "testdb" || rows[0][2] != "true" || rows[0][3] != "true" || rows[0][4] != "false" {
  241. t.Fatalf("identity/privilege functions = %v", rows)
  242. }
  243. rows = catalogRows(t, e, `
  244. SELECT pg_get_serial_sequence('orders', 'id'),
  245. obj_description('orders'::regclass, 'pg_class'),
  246. col_description('orders'::regclass, 1)`)
  247. if len(rows) != 1 || rows[0][0] != "<nil>" || rows[0][1] != "<nil>" || rows[0][2] != "<nil>" {
  248. t.Fatalf("description functions = %v, want NULLs", rows)
  249. }
  250. rows = catalogRows(t, e, `
  251. SELECT pg_get_expr(d.adbin, d.adrelid)
  252. FROM pg_attrdef d WHERE d.adrelid = 'orders'::regclass`)
  253. if len(rows) != 1 || rows[0][0] != "'new'" {
  254. t.Fatalf("pg_get_expr = %v, want the stored default text", rows)
  255. }
  256. rows = catalogRows(t, e, `SELECT pg_catalog.version(), current_setting('search_path')`)
  257. if !strings.Contains(rows[0][0], "PostgreSQL") || rows[0][1] != "public" {
  258. t.Fatalf("version/current_setting = %v", rows)
  259. }
  260. rows = catalogRows(t, e, `SELECT format_type(23, -1), format_type(1043, 14), format_type(999999, -1)`)
  261. if rows[0][0] != "integer" || rows[0][1] != "character varying(10)" || rows[0][2] != "???" {
  262. t.Fatalf("format_type = %v", rows)
  263. }
  264. }
  265. // A psql-style column listing combines the catalog relations with LEFT JOINs;
  266. // it must return one row per live attribute and mark the primary key.
  267. func TestPGCatalogPsqlStyleDescribe(t *testing.T) {
  268. e := setupPGCatalogFixture(t)
  269. rows := catalogRows(t, e, `
  270. SELECT a.attname,
  271. pg_catalog.format_type(a.atttypid, a.atttypmod) AS type,
  272. a.attnotnull AS notnull,
  273. pg_catalog.pg_get_expr(d.adbin, d.adrelid) AS dflt,
  274. i.indisprimary AS is_pk
  275. FROM pg_catalog.pg_class c
  276. JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace
  277. JOIN pg_catalog.pg_attribute a ON a.attrelid = c.oid
  278. LEFT JOIN pg_catalog.pg_attrdef d ON d.adrelid = a.attrelid AND d.adnum = a.attnum
  279. LEFT JOIN pg_catalog.pg_index i ON i.indrelid = c.oid AND a.attnum = ANY(i.indkey)
  280. WHERE c.relname = 'orders' AND c.relkind = 'r' AND n.nspname = 'public'
  281. AND a.attnum > 0 AND NOT a.attisdropped
  282. ORDER BY a.attnum`)
  283. want := [][]string{
  284. {"id", "integer", "true", "<nil>", "true"},
  285. {"account_id", "integer", "true", "<nil>", "<nil>"},
  286. {"note", "text", "false", "'new'", "false"},
  287. {"tags", "text[]", "false", "<nil>", "<nil>"},
  288. {"meta", "jsonb", "false", "<nil>", "<nil>"},
  289. }
  290. if fmt.Sprint(rows) != fmt.Sprint(want) {
  291. t.Fatalf("psql-style describe = %v, want %v", rows, want)
  292. }
  293. }
  294. // Catalog rows reflect DDL immediately, and transactional DDL is visible only
  295. // to the connection that staged it.
  296. func TestPGCatalogTransactionalDDLVisibility(t *testing.T) {
  297. _, schema, table := newTestDB(t)
  298. writer := newExec(schema, table)
  299. observer := newExec(schema, table)
  300. execMust(t, writer, "CREATE TABLE base (id INTEGER PRIMARY KEY)")
  301. execMust(t, writer, "BEGIN")
  302. execMust(t, writer, "CREATE TABLE staged (id INTEGER PRIMARY KEY)")
  303. execMust(t, writer, "ALTER TABLE base ADD COLUMN note TEXT")
  304. execMust(t, writer, "CREATE INDEX idx_base_note ON base (note)")
  305. res := execMust(t, writer, `SELECT table_name FROM information_schema.tables WHERE table_name = 'staged'`)
  306. if len(res.Rows) != 1 {
  307. t.Fatalf("writer does not see staged table: %v", res.Rows)
  308. }
  309. res = execMust(t, writer, `
  310. SELECT column_name FROM information_schema.columns
  311. WHERE table_name = 'base' ORDER BY ordinal_position`)
  312. if len(res.Rows) != 2 || res.Rows[1][0] != "note" {
  313. t.Fatalf("writer does not see staged column: %v", res.Rows)
  314. }
  315. res = execMust(t, writer, `SELECT count(*) FROM pg_indexes WHERE tablename = 'base' AND indexname = 'idx_base_note'`)
  316. if res.Rows[0][0] != int64(1) {
  317. t.Fatalf("writer does not see staged index: %v", res.Rows)
  318. }
  319. res = execMust(t, observer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
  320. if res.Rows[0][0] != int64(0) {
  321. t.Fatalf("observer sees staged table: %v", res.Rows)
  322. }
  323. res = execMust(t, observer, `SELECT count(*) FROM information_schema.columns WHERE table_name = 'base'`)
  324. if res.Rows[0][0] != int64(1) {
  325. t.Fatalf("observer sees staged column: %v", res.Rows)
  326. }
  327. execMust(t, writer, "ROLLBACK")
  328. res = execMust(t, writer, `SELECT count(*) FROM information_schema.tables WHERE table_name = 'staged'`)
  329. if res.Rows[0][0] != int64(0) {
  330. t.Fatalf("rolled-back table still visible: %v", res.Rows)
  331. }
  332. res = execMust(t, writer, `SELECT count(*) FROM pg_attribute a JOIN pg_class c ON c.oid = a.attrelid WHERE c.relname = 'base'`)
  333. if res.Rows[0][0] != int64(1) {
  334. t.Fatalf("rolled-back column still visible: %v", res.Rows)
  335. }
  336. }
  337. // OIDs are deterministic per database, so two connections and repeated
  338. // statements agree.
  339. func TestPGCatalogOIDsAreDeterministic(t *testing.T) {
  340. _, schema, table := newTestDB(t)
  341. first := newExec(schema, table)
  342. execMust(t, first, "CREATE TABLE deterministic (id INTEGER PRIMARY KEY, name TEXT)")
  343. query := `SELECT oid, relname FROM pg_class WHERE relname = 'deterministic'`
  344. one := catalogRows(t, first, query)
  345. two := catalogRows(t, first, query)
  346. if fmt.Sprint(one) != fmt.Sprint(two) {
  347. t.Fatalf("OID changed between statements: %v vs %v", one, two)
  348. }
  349. second := newExec(schema, table)
  350. three := catalogRows(t, second, query)
  351. if fmt.Sprint(one) != fmt.Sprint(three) {
  352. t.Fatalf("OID differs across connections: %v vs %v", one, three)
  353. }
  354. if len(one) != 1 || one[0][1] != "deterministic" {
  355. t.Fatalf("unexpected relation row: %v", one)
  356. }
  357. }
  358. func TestPGCatalogRejectsWrites(t *testing.T) {
  359. e := setupPGCatalogFixture(t)
  360. for _, sql := range []string{
  361. `INSERT INTO pg_catalog.pg_class VALUES (1)`,
  362. `INSERT INTO information_schema.tables VALUES ('x')`,
  363. `UPDATE pg_catalog.pg_type SET typname = 'x'`,
  364. `DELETE FROM pg_catalog.pg_attribute`,
  365. `CREATE INDEX idx_pg_class_name ON pg_catalog.pg_class (relname)`,
  366. `ALTER TABLE pg_catalog.pg_class ADD COLUMN nope TEXT`,
  367. `DROP TABLE pg_catalog.pg_class`,
  368. `CREATE VIEW pg_catalog.pg_class AS SELECT 1`,
  369. } {
  370. if _, err := execSQL(e, sql); err == nil {
  371. t.Fatalf("%s: catalog write must be rejected", sql)
  372. }
  373. }
  374. }
  375. func TestPGSearchPathSettings(t *testing.T) {
  376. e := setupPGCatalogFixture(t)
  377. for _, sql := range []string{
  378. "SET search_path TO public",
  379. "SET search_path = public",
  380. "SET client_encoding TO 'UTF8'",
  381. "SET TIME ZONE 'UTC'",
  382. "SET statement_timeout = 0",
  383. "SET NAMES 'utf8'",
  384. "SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL READ COMMITTED",
  385. } {
  386. if _, err := execSQL(e, sql); err != nil {
  387. t.Fatalf("%s: %v", sql, err)
  388. }
  389. }
  390. res := execMust(t, e, "SHOW search_path")
  391. if len(res.Rows) != 1 || res.Rows[0][0] != "public" {
  392. t.Fatalf("SHOW search_path = %v", res.Rows)
  393. }
  394. res = execMust(t, e, "SHOW server_version")
  395. if len(res.Rows) != 1 || res.Rows[0][0] == "" {
  396. t.Fatalf("SHOW server_version = %v", res.Rows)
  397. }
  398. if _, err := execSQL(e, "SHOW definitely_not_a_setting"); err == nil {
  399. t.Fatal("unknown SHOW setting must fail")
  400. }
  401. }
  402. // The SQLite introspection surface keeps working alongside the PostgreSQL
  403. // catalogs; sqlite_master must not list the virtual relations.
  404. func TestPGCatalogDoesNotChangeSQLiteIntrospection(t *testing.T) {
  405. e := setupPGCatalogFixture(t)
  406. res := execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'table' ORDER BY name`)
  407. names := make([]string, len(res.Rows))
  408. for i, row := range res.Rows {
  409. names[i] = fmt.Sprintf("%v", row[0])
  410. }
  411. if strings.Join(names, ",") != "accounts,orders,pairs" {
  412. t.Fatalf("sqlite_master tables = %v", names)
  413. }
  414. res = execMust(t, e, `SELECT name FROM sqlite_master WHERE type = 'index' ORDER BY name`)
  415. if len(res.Rows) != 2 {
  416. t.Fatalf("sqlite_master indexes = %v", res.Rows)
  417. }
  418. res = execMust(t, e, `PRAGMA table_info(orders)`)
  419. if len(res.Rows) != 5 {
  420. t.Fatalf("PRAGMA table_info rows = %v", res.Rows)
  421. }
  422. }