| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- package storage
- import (
- "errors"
- "strings"
- "testing"
- )
- // compositeTestTable creates a table whose primary key is (a, b). The key
- // columns are declared nullable on purpose: CreateTable must make them NOT NULL.
- func compositeTestTable(t *testing.T, schemas *SchemaManager) {
- t.Helper()
- if err := schemas.CreateTable(&Schema{
- Name: "t",
- PrimaryKeyColumns: []string{"a", "b"},
- Columns: []Column{
- {Name: "a", Type: "TEXT", Nullable: true},
- {Name: "b", Type: "TEXT", Nullable: true},
- {Name: "v", Type: "TEXT", Nullable: true},
- },
- }); err != nil {
- t.Fatalf("create table: %v", err)
- }
- }
- func TestCompositePrimaryKeyNormalizesSchema(t *testing.T) {
- _, _, schemas, _ := newTestSession(t)
- compositeTestTable(t, schemas)
- schema, err := schemas.GetSchema("t")
- if err != nil {
- t.Fatalf("get schema: %v", err)
- }
- if schema.PrimaryKey != "" {
- t.Fatalf("PrimaryKey = %q, want empty for a composite key", schema.PrimaryKey)
- }
- if strings.Join(schema.PrimaryKeyList(), ",") != "a,b" {
- t.Fatalf("PrimaryKeyList = %v, want [a b]", schema.PrimaryKeyList())
- }
- if !schema.IsCompositePrimaryKey() || schema.IsSingleColumnPrimaryKey() {
- t.Fatal("composite key predicates disagree with the column list")
- }
- for _, column := range schema.Columns {
- if schema.IsPrimaryKeyColumn(column.Name) {
- if column.Nullable {
- t.Fatalf("key column %s is nullable", column.Name)
- }
- if !column.PrimaryKey {
- t.Fatalf("key column %s is not marked PrimaryKey", column.Name)
- }
- }
- }
- }
- func TestCompositePrimaryKeyEncodingIsUnambiguous(t *testing.T) {
- kv, _, schemas, tables := newTestSession(t)
- compositeTestTable(t, schemas)
- if err := tables.Insert("t", Row{"a": "ab", "b": "c", "v": "first"}); err != nil {
- t.Fatalf("insert first: %v", err)
- }
- if err := tables.Insert("t", Row{"a": "a", "b": "bc", "v": "second"}); err != nil {
- t.Fatalf("insert second: %v", err)
- }
- // Each component is length-prefixed, so the two rows cannot share a key.
- if !kv.hasKey("testdb:_data:t:2:ab1:c") {
- t.Fatal("expected the first composite key to be 2:ab1:c")
- }
- if !kv.hasKey("testdb:_data:t:1:a2:bc") {
- t.Fatal("expected the second composite key to be 1:a2:bc")
- }
- row, err := tables.GetByPK("t", "2:ab1:c")
- if err != nil {
- t.Fatalf("get by composite key: %v", err)
- }
- if row["v"] != "first" {
- t.Fatalf("row under 2:ab1:c = %v, want first", row["v"])
- }
- // A single component, or a truncated encoding, is not a row key.
- for _, key := range []string{"ab", "2:ab", "1:a", "2:ab1:", "2:ab4:bc12"} {
- if _, err := tables.GetByPK("t", key); err != ErrKeyNotFound {
- t.Fatalf("GetByPK(%q) error = %v, want ErrKeyNotFound", key, err)
- }
- }
- }
- func TestCompositePrimaryKeyInsertErrors(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- compositeTestTable(t, schemas)
- var notNull *NotNullViolationError
- if err := tables.Insert("t", Row{"a": "x"}); !errors.As(err, ¬Null) || notNull.Column != "b" {
- t.Fatalf("missing component error = %v, want NOT NULL violation on b", err)
- }
- notNull = nil
- if err := tables.Insert("t", Row{"a": "x", "b": nil}); !errors.As(err, ¬Null) || notNull.Column != "b" {
- t.Fatalf("NULL component error = %v, want NOT NULL violation on b", err)
- }
- if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
- t.Fatalf("insert: %v", err)
- }
- var duplicate *PrimaryKeyViolationError
- if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "again"}); !errors.As(err, &duplicate) {
- t.Fatalf("duplicate error = %v, want PrimaryKeyViolationError", err)
- }
- // Rejected inserts must not consume the row. Only the accepted row exists.
- rows, err := tables.Select("t", nil)
- if err != nil {
- t.Fatalf("select: %v", err)
- }
- if len(rows) != 1 || rows[0]["v"] != "one" {
- t.Fatalf("rows after rejected inserts = %v", rows)
- }
- }
- func TestCompositePrimaryKeyUpdateAndDelete(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- compositeTestTable(t, schemas)
- if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
- t.Fatalf("insert: %v", err)
- }
- if err := tables.Insert("t", Row{"a": "x", "b": "z", "v": "two"}); err != nil {
- t.Fatalf("insert: %v", err)
- }
- old, ok, err := tables.UpdateByPK("t", "1:x1:y", func(Row) (Row, error) {
- return Row{"v": "updated"}, nil
- })
- if err != nil || !ok {
- t.Fatalf("UpdateByPK = ok %v, err %v", ok, err)
- }
- if old["v"] != "one" {
- t.Fatalf("UpdateByPK old row = %v, want one", old["v"])
- }
- row, err := tables.GetByPK("t", "1:x1:y")
- if err != nil || row["v"] != "updated" {
- t.Fatalf("row after update = %v, err %v", row, err)
- }
- // Moving a key component through the scan path moves the durable key.
- session := NewSession(schemas, tables)
- if err := session.Begin(); err != nil {
- t.Fatalf("begin: %v", err)
- }
- count, err := session.UpdateFunc("t",
- func(Row) (Row, error) { return Row{"b": "w"}, nil },
- func(row Row) bool { return row["b"] == "y" },
- )
- if err != nil {
- t.Fatalf("update key component: %v", err)
- }
- if err := session.Commit(); err != nil {
- t.Fatalf("commit: %v", err)
- }
- if count != 1 {
- t.Fatalf("moved rows = %d, want 1", count)
- }
- if _, err := tables.GetByPK("t", "1:x1:y"); err != ErrKeyNotFound {
- t.Fatalf("old key error = %v, want ErrKeyNotFound", err)
- }
- row, err = tables.GetByPK("t", "1:x1:w")
- if err != nil || row["v"] != "updated" {
- t.Fatalf("row under moved key = %v, err %v", row, err)
- }
- deleted, ok, err := tables.DeleteByPK("t", "1:x1:w")
- if err != nil || !ok {
- t.Fatalf("DeleteByPK = ok %v, err %v", ok, err)
- }
- if deleted["v"] != "updated" {
- t.Fatalf("DeleteByPK row = %v, want updated", deleted["v"])
- }
- if _, err := tables.GetByPK("t", "1:x1:w"); err != ErrKeyNotFound {
- t.Fatalf("deleted key error = %v, want ErrKeyNotFound", err)
- }
- if rows, err := tables.Select("t", nil); err != nil || len(rows) != 1 {
- t.Fatalf("rows after delete = %v, err %v", rows, err)
- }
- }
- func TestCompositePrimaryKeyDurableReload(t *testing.T) {
- kv, pool, schemas, tables := newTestSession(t)
- compositeTestTable(t, schemas)
- if err := tables.Insert("t", Row{"a": "x", "b": "y", "v": "one"}); err != nil {
- t.Fatalf("insert: %v", err)
- }
- if err := tables.Insert("t", Row{"a": "x", "b": "z", "v": "two"}); err != nil {
- t.Fatalf("insert: %v", err)
- }
- var stored string
- if err := pool.WithClient(func(c *KVClient) error {
- var err error
- stored, err = c.Read("testdb:_schema:t")
- return err
- }); err != nil {
- t.Fatalf("read schema: %v", err)
- }
- if !strings.Contains(stored, `"primary_key_columns":["a","b"]`) {
- t.Fatalf("persisted schema = %s, want a primary_key_columns list", stored)
- }
- // A new manager pair keeps only the durable state, as after a restart.
- reloadedSchemas := NewSchemaManager(pool, "testdb")
- reloadedTables := NewTableManager(pool, reloadedSchemas, "testdb")
- schema, err := reloadedSchemas.GetSchema("t")
- if err != nil {
- t.Fatalf("get reloaded schema: %v", err)
- }
- if strings.Join(schema.PrimaryKeyList(), ",") != "a,b" || schema.PrimaryKey != "" {
- t.Fatalf("reloaded key = %q/%v, want a,b", schema.PrimaryKey, schema.PrimaryKeyList())
- }
- row, err := reloadedTables.GetByPK("t", "1:x1:y")
- if err != nil || row["v"] != "one" {
- t.Fatalf("reloaded row = %v, err %v", row, err)
- }
- rows, err := reloadedTables.Select("t", nil)
- if err != nil || len(rows) != 2 {
- t.Fatalf("reloaded select = %v, err %v", rows, err)
- }
- // Writes after the reload keep the same encoding.
- if err := reloadedTables.Insert("t", Row{"a": "x", "b": "y", "v": "again"}); err == nil {
- t.Fatal("expected a duplicate key to fail after reload")
- }
- if err := reloadedTables.Insert("t", Row{"a": "q", "b": "r", "v": "three"}); err != nil {
- t.Fatalf("insert after reload: %v", err)
- }
- if !kv.hasKey("testdb:_data:t:1:q1:r") {
- t.Fatal("insert after reload did not use the composite encoding")
- }
- }
- func TestSingleColumnPrimaryKeyEncodingUnchanged(t *testing.T) {
- kv, _, schemas, tables := newTestSession(t)
- if err := schemas.CreateTable(&Schema{
- Name: "ints",
- Columns: []Column{{Name: "id", Type: "INTEGER", PrimaryKey: true}, {Name: "v", Type: "TEXT"}},
- }); err != nil {
- t.Fatalf("create ints: %v", err)
- }
- if err := tables.Insert("ints", Row{"id": int64(7), "v": "seven"}); err != nil {
- t.Fatalf("insert ints: %v", err)
- }
- if !kv.hasKey("testdb:_data:ints:7") {
- t.Fatal("INTEGER primary key row key changed from the bare value")
- }
- if row, err := tables.GetByPK("ints", "7"); err != nil || row["v"] != "seven" {
- t.Fatalf("GetByPK = %v, err %v", row, err)
- }
- if err := schemas.CreateTable(&Schema{
- Name: "texts",
- Columns: []Column{{Name: "name", Type: "TEXT", PrimaryKey: true}},
- }); err != nil {
- t.Fatalf("create texts: %v", err)
- }
- if err := tables.Insert("texts", Row{"name": "alpha"}); err != nil {
- t.Fatalf("insert texts: %v", err)
- }
- if !kv.hasKey("testdb:_data:texts:alpha") {
- t.Fatal("TEXT primary key row key changed from the bare value")
- }
- var stored string
- if err := schemas.GetPool().WithClient(func(c *KVClient) error {
- var err error
- stored, err = c.Read("testdb:_schema:ints")
- return err
- }); err != nil {
- t.Fatalf("read schema: %v", err)
- }
- if !strings.Contains(stored, `"primary_key":"id"`) {
- t.Fatalf("persisted schema = %s, want the legacy primary_key field", stored)
- }
- if strings.Contains(stored, "primary_key_columns") {
- t.Fatalf("persisted schema = %s, want no composite field for a single-column key", stored)
- }
- }
- func TestLegacySingleColumnSchemaLoads(t *testing.T) {
- kv, pool, schemas, tables := newTestSession(t)
- // A catalog written before composite keys only has "primary_key".
- 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}`
- if err := pool.WithClient(func(c *KVClient) error {
- return c.Write("testdb:_schema:legacy", legacy)
- }); err != nil {
- t.Fatalf("write legacy schema: %v", err)
- }
- schema, err := schemas.GetSchema("legacy")
- if err != nil {
- t.Fatalf("get legacy schema: %v", err)
- }
- if schema.PrimaryKey != "id" || strings.Join(schema.PrimaryKeyList(), ",") != "id" {
- t.Fatalf("legacy key = %q/%v, want id", schema.PrimaryKey, schema.PrimaryKeyList())
- }
- if err := tables.Insert("legacy", Row{"id": int64(5), "v": "five"}); err != nil {
- t.Fatalf("insert into legacy table: %v", err)
- }
- if !kv.hasKey("testdb:_data:legacy:5") {
- t.Fatal("legacy INTEGER primary key did not use the bare-value key")
- }
- if row, err := tables.GetByPK("legacy", "5"); err != nil || row["v"] != "five" {
- t.Fatalf("GetByPK = %v, err %v", row, err)
- }
- }
- // Insert resolves catalog defaults for key components and writes every resolved
- // key value back into the caller's row, so callers (RETURNING, conflict
- // matching) observe the key the row was stored under.
- func TestCompositePrimaryKeyInsertWritesResolvedKeyBack(t *testing.T) {
- _, _, schemas, tables := newTestSession(t)
- if err := schemas.CreateTable(&Schema{
- Name: "t",
- PrimaryKeyColumns: []string{"a", "b"},
- Columns: []Column{
- {Name: "a", Type: "TEXT", Default: "d"},
- {Name: "b", Type: "TEXT"},
- {Name: "v", Type: "TEXT"},
- },
- }); err != nil {
- t.Fatalf("create table: %v", err)
- }
- row := Row{"b": "y", "v": "one"}
- if err := tables.Insert("t", row); err != nil {
- t.Fatalf("insert: %v", err)
- }
- if row["a"] != "d" || row["b"] != "y" {
- t.Fatalf("caller row after insert = %v, want a='d' b='y'", row)
- }
- stored, err := tables.GetByPK("t", "1:d1:y")
- if err != nil || stored["v"] != "one" {
- t.Fatalf("stored row = %v, err %v", stored, err)
- }
- }
|