composite_key_test.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348
  1. package storage
  2. import (
  3. "errors"
  4. "strings"
  5. "testing"
  6. )
  7. // compositeTestTable creates a table whose primary key is (a, b). The key
  8. // columns are declared nullable on purpose: CreateTable must make them NOT NULL.
  9. func compositeTestTable(t *testing.T, schemas *SchemaManager) {
  10. t.Helper()
  11. if err := schemas.CreateTable(&Schema{
  12. Name: "t",
  13. PrimaryKeyColumns: []string{"a", "b"},
  14. Columns: []Column{
  15. {Name: "a", Type: "TEXT", Nullable: true},
  16. {Name: "b", Type: "TEXT", Nullable: true},
  17. {Name: "v", Type: "TEXT", Nullable: true},
  18. },
  19. }); err != nil {
  20. t.Fatalf("create table: %v", err)
  21. }
  22. }
  23. func TestCompositePrimaryKeyNormalizesSchema(t *testing.T) {
  24. _, _, schemas, _ := newTestSession(t)
  25. compositeTestTable(t, schemas)
  26. schema, err := schemas.GetSchema("t")
  27. if err != nil {
  28. t.Fatalf("get schema: %v", err)
  29. }
  30. if schema.PrimaryKey != "" {
  31. t.Fatalf("PrimaryKey = %q, want empty for a composite key", schema.PrimaryKey)
  32. }
  33. if strings.Join(schema.PrimaryKeyList(), ",") != "a,b" {
  34. t.Fatalf("PrimaryKeyList = %v, want [a b]", schema.PrimaryKeyList())
  35. }
  36. if !schema.IsCompositePrimaryKey() || schema.IsSingleColumnPrimaryKey() {
  37. t.Fatal("composite key predicates disagree with the column list")
  38. }
  39. for _, column := range schema.Columns {
  40. if schema.IsPrimaryKeyColumn(column.Name) {
  41. if column.Nullable {
  42. t.Fatalf("key column %s is nullable", column.Name)
  43. }
  44. if !column.PrimaryKey {
  45. t.Fatalf("key column %s is not marked PrimaryKey", column.Name)
  46. }
  47. }
  48. }
  49. }
  50. func TestCompositePrimaryKeyEncodingIsUnambiguous(t *testing.T) {
  51. kv, _, schemas, tables := newTestSession(t)
  52. compositeTestTable(t, schemas)
  53. if err := tables.Insert("t", Row{"a": "ab", "b": "c", "v": "first"}); err != nil {
  54. t.Fatalf("insert first: %v", err)
  55. }
  56. if err := tables.Insert("t", Row{"a": "a", "b": "bc", "v": "second"}); err != nil {
  57. t.Fatalf("insert second: %v", err)
  58. }
  59. // Each component is length-prefixed, so the two rows cannot share a key.
  60. if !kv.hasKey("testdb:_data:t:2:ab1:c") {
  61. t.Fatal("expected the first composite key to be 2:ab1:c")
  62. }
  63. if !kv.hasKey("testdb:_data:t:1:a2:bc") {
  64. t.Fatal("expected the second composite key to be 1:a2:bc")
  65. }
  66. row, err := tables.GetByPK("t", "2:ab1:c")
  67. if err != nil {
  68. t.Fatalf("get by composite key: %v", err)
  69. }
  70. if row["v"] != "first" {
  71. t.Fatalf("row under 2:ab1:c = %v, want first", row["v"])
  72. }
  73. // A single component, or a truncated encoding, is not a row key.
  74. for _, key := range []string{"ab", "2:ab", "1:a", "2:ab1:", "2:ab4:bc12"} {
  75. if _, err := tables.GetByPK("t", key); err != ErrKeyNotFound {
  76. t.Fatalf("GetByPK(%q) error = %v, want ErrKeyNotFound", key, err)
  77. }
  78. }
  79. }
  80. func TestCompositePrimaryKeyInsertErrors(t *testing.T) {
  81. _, _, schemas, tables := newTestSession(t)
  82. compositeTestTable(t, schemas)
  83. var notNull *NotNullViolationError
  84. if err := tables.Insert("t", Row{"a": "x"}); !errors.As(err, &notNull) || notNull.Column != "b" {
  85. t.Fatalf("missing component error = %v, want NOT NULL violation on b", err)
  86. }
  87. notNull = nil
  88. if err := tables.Insert("t", Row{"a": "x", "b": nil}); !errors.As(err, &notNull) || notNull.Column != "b" {
  89. t.Fatalf("NULL component error = %v, want NOT NULL violation on b", err)
  90. }
  91. if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
  92. t.Fatalf("insert: %v", err)
  93. }
  94. var duplicate *PrimaryKeyViolationError
  95. if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "again"}); !errors.As(err, &duplicate) {
  96. t.Fatalf("duplicate error = %v, want PrimaryKeyViolationError", err)
  97. }
  98. // Rejected inserts must not consume the row. Only the accepted row exists.
  99. rows, err := tables.Select("t", nil)
  100. if err != nil {
  101. t.Fatalf("select: %v", err)
  102. }
  103. if len(rows) != 1 || rows[0]["v"] != "one" {
  104. t.Fatalf("rows after rejected inserts = %v", rows)
  105. }
  106. }
  107. func TestCompositePrimaryKeyUpdateAndDelete(t *testing.T) {
  108. _, _, schemas, tables := newTestSession(t)
  109. compositeTestTable(t, schemas)
  110. if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
  111. t.Fatalf("insert: %v", err)
  112. }
  113. if err := tables.Insert("t", Row{"a": "x", "b": "z", "v": "two"}); err != nil {
  114. t.Fatalf("insert: %v", err)
  115. }
  116. old, ok, err := tables.UpdateByPK("t", "1:x1:y", func(Row) (Row, error) {
  117. return Row{"v": "updated"}, nil
  118. })
  119. if err != nil || !ok {
  120. t.Fatalf("UpdateByPK = ok %v, err %v", ok, err)
  121. }
  122. if old["v"] != "one" {
  123. t.Fatalf("UpdateByPK old row = %v, want one", old["v"])
  124. }
  125. row, err := tables.GetByPK("t", "1:x1:y")
  126. if err != nil || row["v"] != "updated" {
  127. t.Fatalf("row after update = %v, err %v", row, err)
  128. }
  129. // Moving a key component through the scan path moves the durable key.
  130. session := NewSession(schemas, tables)
  131. if err := session.Begin(); err != nil {
  132. t.Fatalf("begin: %v", err)
  133. }
  134. count, err := session.UpdateFunc("t",
  135. func(Row) (Row, error) { return Row{"b": "w"}, nil },
  136. func(row Row) bool { return row["b"] == "y" },
  137. )
  138. if err != nil {
  139. t.Fatalf("update key component: %v", err)
  140. }
  141. if err := session.Commit(); err != nil {
  142. t.Fatalf("commit: %v", err)
  143. }
  144. if count != 1 {
  145. t.Fatalf("moved rows = %d, want 1", count)
  146. }
  147. if _, err := tables.GetByPK("t", "1:x1:y"); err != ErrKeyNotFound {
  148. t.Fatalf("old key error = %v, want ErrKeyNotFound", err)
  149. }
  150. row, err = tables.GetByPK("t", "1:x1:w")
  151. if err != nil || row["v"] != "updated" {
  152. t.Fatalf("row under moved key = %v, err %v", row, err)
  153. }
  154. deleted, ok, err := tables.DeleteByPK("t", "1:x1:w")
  155. if err != nil || !ok {
  156. t.Fatalf("DeleteByPK = ok %v, err %v", ok, err)
  157. }
  158. if deleted["v"] != "updated" {
  159. t.Fatalf("DeleteByPK row = %v, want updated", deleted["v"])
  160. }
  161. if _, err := tables.GetByPK("t", "1:x1:w"); err != ErrKeyNotFound {
  162. t.Fatalf("deleted key error = %v, want ErrKeyNotFound", err)
  163. }
  164. if rows, err := tables.Select("t", nil); err != nil || len(rows) != 1 {
  165. t.Fatalf("rows after delete = %v, err %v", rows, err)
  166. }
  167. }
  168. func TestCompositePrimaryKeyDurableReload(t *testing.T) {
  169. kv, pool, schemas, tables := newTestSession(t)
  170. compositeTestTable(t, schemas)
  171. if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
  172. t.Fatalf("insert: %v", err)
  173. }
  174. if err := tables.Insert("t", Row{"a": "x", "b": "z", "v": "two"}); err != nil {
  175. t.Fatalf("insert: %v", err)
  176. }
  177. var stored string
  178. if err := pool.WithClient(func(c *KVClient) error {
  179. var err error
  180. stored, err = c.Read("testdb:_schema:t")
  181. return err
  182. }); err != nil {
  183. t.Fatalf("read schema: %v", err)
  184. }
  185. if !strings.Contains(stored, `"primary_key_columns":["a","b"]`) {
  186. t.Fatalf("persisted schema = %s, want a primary_key_columns list", stored)
  187. }
  188. // A new manager pair keeps only the durable state, as after a restart.
  189. reloadedSchemas := NewSchemaManager(pool, "testdb")
  190. reloadedTables := NewTableManager(pool, reloadedSchemas, "testdb")
  191. schema, err := reloadedSchemas.GetSchema("t")
  192. if err != nil {
  193. t.Fatalf("get reloaded schema: %v", err)
  194. }
  195. if strings.Join(schema.PrimaryKeyList(), ",") != "a,b" || schema.PrimaryKey != "" {
  196. t.Fatalf("reloaded key = %q/%v, want a,b", schema.PrimaryKey, schema.PrimaryKeyList())
  197. }
  198. row, err := reloadedTables.GetByPK("t", "1:x1:y")
  199. if err != nil || row["v"] != "one" {
  200. t.Fatalf("reloaded row = %v, err %v", row, err)
  201. }
  202. rows, err := reloadedTables.Select("t", nil)
  203. if err != nil || len(rows) != 2 {
  204. t.Fatalf("reloaded select = %v, err %v", rows, err)
  205. }
  206. // Writes after the reload keep the same encoding.
  207. if err := reloadedTables.Insert("t", Row{"a": "x", "b": "y", "v": "again"}); err == nil {
  208. t.Fatal("expected a duplicate key to fail after reload")
  209. }
  210. if err := reloadedTables.Insert("t", Row{"a": "q", "b": "r", "v": "three"}); err != nil {
  211. t.Fatalf("insert after reload: %v", err)
  212. }
  213. if !kv.hasKey("testdb:_data:t:1:q1:r") {
  214. t.Fatal("insert after reload did not use the composite encoding")
  215. }
  216. }
  217. func TestSingleColumnPrimaryKeyEncodingUnchanged(t *testing.T) {
  218. kv, _, schemas, tables := newTestSession(t)
  219. if err := schemas.CreateTable(&Schema{
  220. Name: "ints",
  221. Columns: []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}, {Name: "v", Type: "TEXT"}},
  222. }); err != nil {
  223. t.Fatalf("create ints: %v", err)
  224. }
  225. if err := tables.Insert("ints", Row{"id": int64(7), "v": "seven"}); err != nil {
  226. t.Fatalf("insert ints: %v", err)
  227. }
  228. if !kv.hasKey("testdb:_data:ints:7") {
  229. t.Fatal("INTEGER primary key row key changed from the bare value")
  230. }
  231. if row, err := tables.GetByPK("ints", "7"); err != nil || row["v"] != "seven" {
  232. t.Fatalf("GetByPK = %v, err %v", row, err)
  233. }
  234. if err := schemas.CreateTable(&Schema{
  235. Name: "texts",
  236. Columns: []Column{{Name: "name", Type: "TEXT", PrimaryKey: true}},
  237. }); err != nil {
  238. t.Fatalf("create texts: %v", err)
  239. }
  240. if err := tables.Insert("texts", Row{"name": "alpha"}); err != nil {
  241. t.Fatalf("insert texts: %v", err)
  242. }
  243. if !kv.hasKey("testdb:_data:texts:alpha") {
  244. t.Fatal("TEXT primary key row key changed from the bare value")
  245. }
  246. var stored string
  247. if err := schemas.GetPool().WithClient(func(c *KVClient) error {
  248. var err error
  249. stored, err = c.Read("testdb:_schema:ints")
  250. return err
  251. }); err != nil {
  252. t.Fatalf("read schema: %v", err)
  253. }
  254. if !strings.Contains(stored, `"primary_key":"id"`) {
  255. t.Fatalf("persisted schema = %s, want the legacy primary_key field", stored)
  256. }
  257. if strings.Contains(stored, "primary_key_columns") {
  258. t.Fatalf("persisted schema = %s, want no composite field for a single-column key", stored)
  259. }
  260. }
  261. func TestLegacySingleColumnSchemaLoads(t *testing.T) {
  262. kv, pool, schemas, tables := newTestSession(t)
  263. // A catalog written before composite keys only has "primary_key".
  264. const legacy = `{"name":"legacy","columns":[{"name":"id","type":"INTEGER","nullable":false,"primary_key":true},{"name":"v","type":"TEXT","nullable":true}],"primary_key":"id","created_at":"2026-01-01T00:00:00Z","next_rowid":0,"autoincrement":false}`
  265. if err := pool.WithClient(func(c *KVClient) error {
  266. return c.Write("testdb:_schema:legacy", legacy)
  267. }); err != nil {
  268. t.Fatalf("write legacy schema: %v", err)
  269. }
  270. schema, err := schemas.GetSchema("legacy")
  271. if err != nil {
  272. t.Fatalf("get legacy schema: %v", err)
  273. }
  274. if schema.PrimaryKey != "id" || strings.Join(schema.PrimaryKeyList(), ",") != "id" {
  275. t.Fatalf("legacy key = %q/%v, want id", schema.PrimaryKey, schema.PrimaryKeyList())
  276. }
  277. if err := tables.Insert("legacy", Row{"id": int64(5), "v": "five"}); err != nil {
  278. t.Fatalf("insert into legacy table: %v", err)
  279. }
  280. if !kv.hasKey("testdb:_data:legacy:5") {
  281. t.Fatal("legacy INTEGER primary key did not use the bare-value key")
  282. }
  283. if row, err := tables.GetByPK("legacy", "5"); err != nil || row["v"] != "five" {
  284. t.Fatalf("GetByPK = %v, err %v", row, err)
  285. }
  286. }
  287. // Insert resolves catalog defaults for key components and writes every resolved
  288. // key value back into the caller's row, so callers (RETURNING, conflict
  289. // matching) observe the key the row was stored under.
  290. func TestCompositePrimaryKeyInsertWritesResolvedKeyBack(t *testing.T) {
  291. _, _, schemas, tables := newTestSession(t)
  292. if err := schemas.CreateTable(&Schema{
  293. Name: "t",
  294. PrimaryKeyColumns: []string{"a", "b"},
  295. Columns: []Column{
  296. {Name: "a", Type: "TEXT", Default: "d"},
  297. {Name: "b", Type: "TEXT"},
  298. {Name: "v", Type: "TEXT"},
  299. },
  300. }); err != nil {
  301. t.Fatalf("create table: %v", err)
  302. }
  303. row := Row{"b": "y", "v": "one"}
  304. if err := tables.Insert("t", row); err != nil {
  305. t.Fatalf("insert: %v", err)
  306. }
  307. if row["a"] != "d" || row["b"] != "y" {
  308. t.Fatalf("caller row after insert = %v, want a='d' b='y'", row)
  309. }
  310. stored, err := tables.GetByPK("t", "1:d1:y")
  311. if err != nil || stored["v"] != "one" {
  312. t.Fatalf("stored row = %v, err %v", stored, err)
  313. }
  314. }