Skip to content

Commit 8968f4f

Browse files
authored
feat(bigtable): add opensession descriptors for bigtable (#14647)
…ion types
1 parent 62307ad commit 8968f4f

2 files changed

Lines changed: 350 additions & 0 deletions

File tree

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal
16+
17+
import (
18+
"fmt"
19+
20+
spb "cloud.google.com/go/bigtable/apiv2/bigtablepb"
21+
"google.golang.org/protobuf/proto"
22+
)
23+
24+
// SessionType represents the protocol target session type.
25+
type SessionType int
26+
27+
const (
28+
// SessionTypeTable indicates standard table session type.
29+
SessionTypeTable SessionType = iota
30+
// SessionTypeAuthorizedView indicates authorized view session type.
31+
SessionTypeAuthorizedView
32+
// SessionTypeMaterializedView indicates materialized view session type.
33+
SessionTypeMaterializedView
34+
)
35+
36+
func (t SessionType) String() string {
37+
switch t {
38+
case SessionTypeTable:
39+
return "table"
40+
case SessionTypeAuthorizedView:
41+
return "authorized_view"
42+
case SessionTypeMaterializedView:
43+
return "materialized_view"
44+
default:
45+
return "unknown"
46+
}
47+
}
48+
49+
// SessionDescriptor models a dynamic envelope handshake parameters compiler.
50+
type SessionDescriptor struct {
51+
Type SessionType
52+
MethodName string
53+
HeaderKeys []string
54+
LogNameFn func(req proto.Message) string
55+
MetadataFn func(req proto.Message) map[string]string // Populates session metadata headers required for OpenSession{} RPC
56+
}
57+
58+
var (
59+
// TABLE_SESSION manages standard table scoped Session streams.
60+
TABLE_SESSION = &SessionDescriptor{
61+
Type: SessionTypeTable,
62+
MethodName: "OpenTable",
63+
HeaderKeys: []string{"table_name", "app_profile_id", "permission"},
64+
LogNameFn: func(req proto.Message) string {
65+
r, ok := req.(*spb.OpenTableRequest)
66+
if !ok || r == nil {
67+
return "TableSession(nil)"
68+
}
69+
return fmt.Sprintf("TableSession(table=%s, app_profile=%s, perm=%s)", r.TableName, r.AppProfileId, r.Permission.String())
70+
},
71+
MetadataFn: func(req proto.Message) map[string]string {
72+
r, ok := req.(*spb.OpenTableRequest)
73+
if !ok || r == nil {
74+
return nil
75+
}
76+
return map[string]string{
77+
"open_session.payload.table_name": r.TableName,
78+
"open_session.payload.app_profile_id": r.AppProfileId,
79+
"open_session.payload.permission": r.Permission.String(),
80+
}
81+
},
82+
}
83+
84+
// AUTHORIZED_VIEW_SESSION manages authorized view scoped Session streams.
85+
AUTHORIZED_VIEW_SESSION = &SessionDescriptor{
86+
Type: SessionTypeAuthorizedView,
87+
MethodName: "OpenAuthorizedView",
88+
HeaderKeys: []string{"authorized_view_name", "app_profile_id", "permission"},
89+
LogNameFn: func(req proto.Message) string {
90+
r, ok := req.(*spb.OpenAuthorizedViewRequest)
91+
if !ok || r == nil {
92+
return "AuthorizedViewSession(nil)"
93+
}
94+
return fmt.Sprintf("AuthorizedViewSession(view=%s, app_profile=%s, perm=%s)", r.AuthorizedViewName, r.AppProfileId, r.Permission.String())
95+
},
96+
MetadataFn: func(req proto.Message) map[string]string {
97+
r, ok := req.(*spb.OpenAuthorizedViewRequest)
98+
if !ok || r == nil {
99+
return nil
100+
}
101+
return map[string]string{
102+
"open_session.payload.authorized_view_name": r.AuthorizedViewName,
103+
"open_session.payload.app_profile_id": r.AppProfileId,
104+
"open_session.payload.permission": r.Permission.String(),
105+
}
106+
},
107+
}
108+
109+
// MATERIALIZED_VIEW_SESSION manages materialized view scoped Session streams (Read-Only).
110+
MATERIALIZED_VIEW_SESSION = &SessionDescriptor{
111+
Type: SessionTypeMaterializedView,
112+
MethodName: "OpenMaterializedView",
113+
HeaderKeys: []string{"materialized_view_name", "app_profile_id", "permission"},
114+
LogNameFn: func(req proto.Message) string {
115+
r, ok := req.(*spb.OpenMaterializedViewRequest)
116+
if !ok || r == nil {
117+
return "MaterializedViewSession(nil)"
118+
}
119+
return fmt.Sprintf("MaterializedViewSession(view=%s, app_profile=%s, perm=%s)", r.MaterializedViewName, r.AppProfileId, r.Permission.String())
120+
},
121+
MetadataFn: func(req proto.Message) map[string]string {
122+
r, ok := req.(*spb.OpenMaterializedViewRequest)
123+
if !ok || r == nil {
124+
return nil
125+
}
126+
return map[string]string{
127+
"open_session.payload.materialized_view_name": r.MaterializedViewName,
128+
"open_session.payload.app_profile_id": r.AppProfileId,
129+
"open_session.payload.permission": r.Permission.String(),
130+
}
131+
},
132+
}
133+
)
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
// Copyright 2026 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package internal
16+
17+
import (
18+
"testing"
19+
20+
spb "cloud.google.com/go/bigtable/apiv2/bigtablepb"
21+
)
22+
23+
func TestSessionType_String(t *testing.T) {
24+
tests := []struct {
25+
t SessionType
26+
want string
27+
}{
28+
{SessionTypeTable, "table"},
29+
{SessionTypeAuthorizedView, "authorized_view"},
30+
{SessionTypeMaterializedView, "materialized_view"},
31+
{SessionType(99), "unknown"},
32+
}
33+
34+
for _, tt := range tests {
35+
if got := tt.t.String(); got != tt.want {
36+
t.Errorf("SessionType(%d).String() = %q, want %q", int(tt.t), got, tt.want)
37+
}
38+
}
39+
}
40+
41+
func TestTableSessionDescriptor(t *testing.T) {
42+
desc := TABLE_SESSION
43+
44+
if desc.Type != SessionTypeTable {
45+
t.Errorf("Expected type %v, got %v", SessionTypeTable, desc.Type)
46+
}
47+
if desc.MethodName != "OpenTable" {
48+
t.Errorf("Expected MethodName 'OpenTable', got %q", desc.MethodName)
49+
}
50+
51+
expectedHeaders := []string{"table_name", "app_profile_id", "permission"}
52+
if len(desc.HeaderKeys) != len(expectedHeaders) {
53+
t.Fatalf("Expected HeaderKeys length %d, got %d", len(expectedHeaders), len(desc.HeaderKeys))
54+
}
55+
for i, h := range expectedHeaders {
56+
if desc.HeaderKeys[i] != h {
57+
t.Errorf("At index %d: expected header key %q, got %q", i, h, desc.HeaderKeys[i])
58+
}
59+
}
60+
61+
req := &spb.OpenTableRequest{
62+
TableName: "projects/p/instances/i/tables/t",
63+
AppProfileId: "app-profile-1",
64+
Permission: spb.OpenTableRequest_PERMISSION_READ_WRITE,
65+
}
66+
67+
// Test LogNameFn
68+
logName := desc.LogNameFn(req)
69+
expectedLogName := "TableSession(table=projects/p/instances/i/tables/t, app_profile=app-profile-1, perm=PERMISSION_READ_WRITE)"
70+
if logName != expectedLogName {
71+
t.Errorf("Expected log name %q, got %q", expectedLogName, logName)
72+
}
73+
74+
// Test MetadataFn
75+
meta := desc.MetadataFn(req)
76+
expectedMeta := map[string]string{
77+
"open_session.payload.table_name": "projects/p/instances/i/tables/t",
78+
"open_session.payload.app_profile_id": "app-profile-1",
79+
"open_session.payload.permission": "PERMISSION_READ_WRITE",
80+
}
81+
if len(meta) != len(expectedMeta) {
82+
t.Fatalf("Expected metadata length %d, got %d", len(expectedMeta), len(meta))
83+
}
84+
for k, v := range expectedMeta {
85+
if meta[k] != v {
86+
t.Errorf("Metadata key %q: expected value %q, got %q", k, v, meta[k])
87+
}
88+
}
89+
}
90+
91+
func TestAuthorizedViewSessionDescriptor(t *testing.T) {
92+
desc := AUTHORIZED_VIEW_SESSION
93+
94+
if desc.Type != SessionTypeAuthorizedView {
95+
t.Errorf("Expected type %v, got %v", SessionTypeAuthorizedView, desc.Type)
96+
}
97+
if desc.MethodName != "OpenAuthorizedView" {
98+
t.Errorf("Expected MethodName 'OpenAuthorizedView', got %q", desc.MethodName)
99+
}
100+
101+
expectedHeaders := []string{"authorized_view_name", "app_profile_id", "permission"}
102+
if len(desc.HeaderKeys) != len(expectedHeaders) {
103+
t.Fatalf("Expected HeaderKeys length %d, got %d", len(expectedHeaders), len(desc.HeaderKeys))
104+
}
105+
for i, h := range expectedHeaders {
106+
if desc.HeaderKeys[i] != h {
107+
t.Errorf("At index %d: expected header key %q, got %q", i, h, desc.HeaderKeys[i])
108+
}
109+
}
110+
111+
req := &spb.OpenAuthorizedViewRequest{
112+
AuthorizedViewName: "projects/p/instances/i/tables/t/authorizedViews/v",
113+
AppProfileId: "app-profile-2",
114+
Permission: spb.OpenAuthorizedViewRequest_PERMISSION_READ,
115+
}
116+
117+
// Test LogNameFn
118+
logName := desc.LogNameFn(req)
119+
expectedLogName := "AuthorizedViewSession(view=projects/p/instances/i/tables/t/authorizedViews/v, app_profile=app-profile-2, perm=PERMISSION_READ)"
120+
if logName != expectedLogName {
121+
t.Errorf("Expected log name %q, got %q", expectedLogName, logName)
122+
}
123+
124+
// Test MetadataFn
125+
meta := desc.MetadataFn(req)
126+
expectedMeta := map[string]string{
127+
"open_session.payload.authorized_view_name": "projects/p/instances/i/tables/t/authorizedViews/v",
128+
"open_session.payload.app_profile_id": "app-profile-2",
129+
"open_session.payload.permission": "PERMISSION_READ",
130+
}
131+
if len(meta) != len(expectedMeta) {
132+
t.Fatalf("Expected metadata length %d, got %d", len(expectedMeta), len(meta))
133+
}
134+
for k, v := range expectedMeta {
135+
if meta[k] != v {
136+
t.Errorf("Metadata key %q: expected value %q, got %q", k, v, meta[k])
137+
}
138+
}
139+
}
140+
141+
func TestMaterializedViewSessionDescriptor(t *testing.T) {
142+
desc := MATERIALIZED_VIEW_SESSION
143+
144+
if desc.Type != SessionTypeMaterializedView {
145+
t.Errorf("Expected type %v, got %v", SessionTypeMaterializedView, desc.Type)
146+
}
147+
if desc.MethodName != "OpenMaterializedView" {
148+
t.Errorf("Expected MethodName 'OpenMaterializedView', got %q", desc.MethodName)
149+
}
150+
151+
expectedHeaders := []string{"materialized_view_name", "app_profile_id", "permission"}
152+
if len(desc.HeaderKeys) != len(expectedHeaders) {
153+
t.Fatalf("Expected HeaderKeys length %d, got %d", len(expectedHeaders), len(desc.HeaderKeys))
154+
}
155+
for i, h := range expectedHeaders {
156+
if desc.HeaderKeys[i] != h {
157+
t.Errorf("At index %d: expected header key %q, got %q", i, h, desc.HeaderKeys[i])
158+
}
159+
}
160+
161+
req := &spb.OpenMaterializedViewRequest{
162+
MaterializedViewName: "projects/p/instances/i/materializedViews/mv",
163+
AppProfileId: "app-profile-3",
164+
Permission: spb.OpenMaterializedViewRequest_PERMISSION_READ,
165+
}
166+
167+
// Test LogNameFn
168+
logName := desc.LogNameFn(req)
169+
expectedLogName := "MaterializedViewSession(view=projects/p/instances/i/materializedViews/mv, app_profile=app-profile-3, perm=PERMISSION_READ)"
170+
if logName != expectedLogName {
171+
t.Errorf("Expected log name %q, got %q", expectedLogName, logName)
172+
}
173+
174+
// Test MetadataFn
175+
meta := desc.MetadataFn(req)
176+
expectedMeta := map[string]string{
177+
"open_session.payload.materialized_view_name": "projects/p/instances/i/materializedViews/mv",
178+
"open_session.payload.app_profile_id": "app-profile-3",
179+
"open_session.payload.permission": "PERMISSION_READ",
180+
}
181+
if len(meta) != len(expectedMeta) {
182+
t.Fatalf("Expected metadata length %d, got %d", len(expectedMeta), len(meta))
183+
}
184+
for k, v := range expectedMeta {
185+
if meta[k] != v {
186+
t.Errorf("Metadata key %q: expected value %q, got %q", k, v, meta[k])
187+
}
188+
}
189+
}
190+
191+
func TestSessionDescriptors_SafeAssertions(t *testing.T) {
192+
// 1. Verify with nil request
193+
if name := TABLE_SESSION.LogNameFn(nil); name != "TableSession(nil)" {
194+
t.Errorf("Expected LogNameFn(nil) = 'TableSession(nil)', got %q", name)
195+
}
196+
if meta := TABLE_SESSION.MetadataFn(nil); meta != nil {
197+
t.Errorf("Expected MetadataFn(nil) = nil, got %v", meta)
198+
}
199+
200+
// 2. Verify with typed nil pointer
201+
var nilTableReq *spb.OpenTableRequest
202+
if name := TABLE_SESSION.LogNameFn(nilTableReq); name != "TableSession(nil)" {
203+
t.Errorf("Expected LogNameFn(typed nil) = 'TableSession(nil)', got %q", name)
204+
}
205+
if meta := TABLE_SESSION.MetadataFn(nilTableReq); meta != nil {
206+
t.Errorf("Expected MetadataFn(typed nil) = nil, got %v", meta)
207+
}
208+
209+
// 3. Verify with wrong message type
210+
wrongReq := &spb.OpenAuthorizedViewRequest{}
211+
if name := TABLE_SESSION.LogNameFn(wrongReq); name != "TableSession(nil)" {
212+
t.Errorf("Expected LogNameFn(wrong type) = 'TableSession(nil)', got %q", name)
213+
}
214+
if meta := TABLE_SESSION.MetadataFn(wrongReq); meta != nil {
215+
t.Errorf("Expected MetadataFn(wrong type) = nil, got %v", meta)
216+
}
217+
}

0 commit comments

Comments
 (0)