diff --git a/apps/server/app/x-pick.go b/apps/server/app/x-pick.go index 88b90c7..78731aa 100644 --- a/apps/server/app/x-pick.go +++ b/apps/server/app/x-pick.go @@ -112,7 +112,11 @@ func (w *fileWork) xPick() { w.P(" ps = append(ps, ", eq, "(v))") w.P(" }") case ormpb.Type_TYPE_TIME: - w.P(" return ", eq, "(k.Get", name.Go(), "().AsTime()), nil") + // Appended like any other field. Returned here, as it once + // was, it dropped what came before it and never read what + // came after: a lookup by (desk, starts_at) matched every + // row at that time, whatever its desk. + w.P(" ps = append(ps, ", eq, "(k.Get", name.Go(), "().AsTime()))") default: w.P(" ps = append(ps, ", eq, "(k.Get", name.Go(), "()))") } diff --git a/go.mod b/go.mod index 8b36891..a0ccf4c 100644 --- a/go.mod +++ b/go.mod @@ -22,7 +22,7 @@ require ( github.com/hashicorp/hcl/v2 v2.24.0 // indirect github.com/mitchellh/go-wordwrap v1.0.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20260906213448-03adaf8cb0fc // indirect + github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20261010065542-f9ac5b66df3e // indirect github.com/protobuf-orm/protoc-gen-orm-service v0.0.0-20260906213434-46a5916d5339 // indirect github.com/rogpeppe/go-internal v1.16.0 // indirect github.com/zclconf/go-cty v1.19.0 // indirect diff --git a/go.sum b/go.sum index 6340300..8bf4fec 100644 --- a/go.sum +++ b/go.sum @@ -34,8 +34,8 @@ github.com/protobuf-orm/ent v0.0.0-20260906224055-1575ccbff508 h1:Pvk4/sP7JWUmhk github.com/protobuf-orm/ent v0.0.0-20260906224055-1575ccbff508/go.mod h1:EE8o+t0Y4xtH8DRH/shGerAb2mPQJ0A1TqWT5LU7pZQ= github.com/protobuf-orm/protobuf-orm v0.0.0-20260906212449-04c0cd58f10a h1:EXbJ28yY25wzt2HbzVkw7Tk46pF1GAHV+XMcC+kir40= github.com/protobuf-orm/protobuf-orm v0.0.0-20260906212449-04c0cd58f10a/go.mod h1:OYlbo4/9t+Hgj/XZwdgN8Z9MyViTLPHxBWLUR4+j52o= -github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20260906213448-03adaf8cb0fc h1:vH89IDBqdLR6H5LAR1i4qp4FwyFfZo91Hk8knu4ZVfY= -github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20260906213448-03adaf8cb0fc/go.mod h1:4q7UNtqEYxIZ69Uimb9jhDpZKBwEfKVsOQXwwD+T5Oo= +github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20261010065542-f9ac5b66df3e h1:g77on2k8uh1A8m7XwFwZI56cDpVofVohL07p8pf3W6A= +github.com/protobuf-orm/protoc-gen-orm-go v0.0.0-20261010065542-f9ac5b66df3e/go.mod h1:/QT/BggjLKYPVwF7hsxF4dmLG0L9WoiRVzrX4kKFuk8= github.com/protobuf-orm/protoc-gen-orm-service v0.0.0-20260906213434-46a5916d5339 h1:5n6G91XIVVUoHD+qYXAd9I1gb+XzO0EoJyZjCAjT04E= github.com/protobuf-orm/protoc-gen-orm-service v0.0.0-20260906213434-46a5916d5339/go.mod h1:0zV6CUB1IioAWwCtWYh8WI7+pQuu7Jjvbc2zg3j0r7c= github.com/rogpeppe/go-internal v1.16.0 h1:O9DK+vNMDVGLr2BeZqmpLeMjiMNkuXfcqntWbZV6S5g= diff --git a/internal/apptest/ent/shift.g.go b/internal/apptest/ent/shift.g.go new file mode 100644 index 0000000..a2bc915 --- /dev/null +++ b/internal/apptest/ent/shift.g.go @@ -0,0 +1,17 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/shift.proto + +package ent + +import ( + apptest "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" +) + +func (e *Shift) Proto() *apptest.Shift { + x := &apptest.Shift{} + x.SetId(e.Id[:]) + x.SetDesk(e.Desk) + x.SetStartsAt(timestamppb.New(e.StartsAt)) + return x +} diff --git a/internal/apptest/query.g.go b/internal/apptest/query.g.go index c93c620..3253e11 100644 --- a/internal/apptest/query.g.go +++ b/internal/apptest/query.g.go @@ -4,6 +4,8 @@ package apptest import ( bytes "bytes" + proto "google.golang.org/protobuf/proto" + timestamppb "google.golang.org/protobuf/types/known/timestamppb" ) func (x *ValueFieldRef) Pick() *ValueFieldGetRequest { @@ -422,3 +424,68 @@ func SeatByDeputy(deputy *UserRef) *SeatRef { func SeatGetByDeputy(deputy *UserRef) *SeatGetRequest { return SeatGetRequest_builder{Ref: SeatByDeputy(deputy)}.Build() } + +func (x *ShiftRef) Pick() *ShiftGetRequest { + return ShiftGetRequest_builder{Ref: x}.Build() +} + +func (x *Shift) Ref() *ShiftRef { + if v := x.GetId(); len(v) > 0 { + return ShiftById(v) + } + { + v1 := x.GetDesk() + v2 := x.GetStartsAt() + if len(v1) > 0 && v2 != nil { + return ShiftBySlot(v1, v2) + } + } + + return nil +} + +func (x *Shift) Pick() *ShiftGetRequest { + return x.Ref().Pick() +} + +func (x *ShiftRef) Picks(v *Shift) bool { + switch x.WhichKey() { + case ShiftRef_Id_case: + return bytes.Equal(x.GetId(), v.GetId()) + case ShiftRef_Slot_case: + x := x.GetSlot() + return (x.GetDesk() == v.GetDesk()) && + (proto.Equal(x.GetStartsAt(), v.GetStartsAt())) + default: + return false + } +} + +func (x *ShiftGetRequest) WithSelect(f func(s *ShiftSelect)) *ShiftGetRequest { + if !x.HasSelect() { + x.SetSelect(&ShiftSelect{}) + } + f(x.GetSelect()) + return x +} + +func ShiftById(v []byte) *ShiftRef { + x := &ShiftRef{} + x.SetId(v) + return x +} + +func ShiftGetById(v []byte) *ShiftGetRequest { + return ShiftGetRequest_builder{Ref: ShiftById(v)}.Build() +} + +func ShiftBySlot(desk string, starts_at *timestamppb.Timestamp) *ShiftRef { + x := &ShiftRefBySlot{} + x.SetDesk(desk) + x.SetStartsAt(starts_at) + return ShiftRef_builder{Slot: x}.Build() +} + +func ShiftGetBySlot(desk string, starts_at *timestamppb.Timestamp) *ShiftGetRequest { + return ShiftGetRequest_builder{Ref: ShiftBySlot(desk, starts_at)}.Build() +} diff --git a/internal/apptest/schema/shift.go b/internal/apptest/schema/shift.go new file mode 100644 index 0000000..4d4d6a6 --- /dev/null +++ b/internal/apptest/schema/shift.go @@ -0,0 +1,39 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/shift.proto + +package schema + +import ( + ent "github.com/protobuf-orm/ent" + entsql "github.com/protobuf-orm/ent/dialect/entsql" + schema "github.com/protobuf-orm/ent/schema" + field "github.com/protobuf-orm/ent/schema/field" + index "github.com/protobuf-orm/ent/schema/index" +) + +type Shift struct { + ent.Schema +} + +func (Shift) Fields() []ent.Field { + return []ent.Field{ + field.Uuid("id"). + Unique(). + Immutable(), + field.String("desk"), + field.Time("starts_at"), + } +} + +func (Shift) Indexes() []ent.Index { + return []ent.Index{ + index.Fields("desk", "starts_at"). + Unique(), + } +} + +func (Shift) Annotations() []schema.Annotation { + return []schema.Annotation{ + entsql.Annotation{Table: "shift"}, + } +} diff --git a/internal/apptest/server/bare/shift.g.go b/internal/apptest/server/bare/shift.g.go new file mode 100644 index 0000000..9fae70b --- /dev/null +++ b/internal/apptest/server/bare/shift.g.go @@ -0,0 +1,427 @@ +// Code generated by protoc-gen-orm-ent. DO NOT EDIT. +// source: apptest/shift.proto + +package bare + +import ( + context "context" + errors "errors" + patchpb "github.com/lesomnus/protobuf-patch/patchpb" + ent1 "github.com/protobuf-orm/ent" + sqlgraph "github.com/protobuf-orm/ent/dialect/sql/sqlgraph" + ormpatch "github.com/protobuf-orm/protobuf-orm/ormpatch" + apptest "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + ent "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent" + predicate "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/predicate" + shift "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/shift" + entpatch "github.com/protobuf-orm/protoc-gen-orm-ent/runtime/entpatch" + entuuid "github.com/protobuf-orm/protoc-gen-orm-ent/runtime/entuuid" + codes "google.golang.org/grpc/codes" + status "google.golang.org/grpc/status" + uuid "uuid" +) + +type ShiftServiceServer struct { + Store + + apptest.UnimplementedShiftServiceServer +} + +// NewShiftServiceServer answers with a server that runs its queries with `db`. +// +// It takes the options of [Server] so that what is built here can be told +// where to report its writes and what it may see. Built without them, it +// reports nowhere and sees everything. +func NewShiftServiceServer(db *ent.Client, opts ...Option) apptest.ShiftServiceServer { + s := Server{Store: Store{Db: db}} + for _, opt := range opts { + opt(&s) + } + return ShiftServiceServer{Store: s.Store} +} + +// ShiftNarrow answers with `p` and everything else that narrows a +// read of a Shift, which is whatever `scope` says. +// +// Every read this package makes goes through it, and a read written by +// hand should too -- a List is the one read nothing generates, and so the +// one that would otherwise answer with rows nobody should be given. +func ShiftNarrow(ctx context.Context, scope Scope, p predicate.Shift) (predicate.Shift, error) { + ps := make([]predicate.Shift, 0, 2) + if p != nil { + ps = append(ps, p) + } + if scope != nil { + q, err := scope.ShiftScope(ctx) + if err != nil { + return nil, err + } + if q != nil { + ps = append(ps, q) + } + } + + switch len(ps) { + case 0: + return nil, nil + case 1: + return ps[0], nil + default: + return shift.And(ps...), nil + } +} + +// narrow is [ShiftNarrow] with this server's own scope. +func (s ShiftServiceServer) narrow(ctx context.Context, p predicate.Shift) (predicate.Shift, error) { + return ShiftNarrow(ctx, s.Scope, p) +} + +func (s ShiftServiceServer) Add(ctx context.Context, req *apptest.ShiftAddRequest) (*apptest.Shift, error) { + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, s.Rec != nil) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + q := st.Db.Shift.Create() + var k uuid.UUID + if req.HasId() { + if v, err := entuuid.FromBytes(req.GetId()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + k = v + } + } + if v, err := mint(ctx, s.Mint, "apptest.Shift", k, req.HasId()); err != nil { + return nil, err + } else { + q.SetId(v) + } + q.SetDesk(req.GetDesk()) + q.SetStartsAt(req.GetStartsAt().AsTime()) + + u, err := q.Save(ctx) + if err != nil { + if err, ok := err.(*ent.ConstraintError); ok { + if sqlgraph.IsUniqueConstraintError(err) { + return nil, status.Error(codes.AlreadyExists, "Shift already exists") + } + if sqlgraph.IsForeignKeyConstraintError(err) { + return nil, status.Error(codes.NotFound, "Shift: referenced entity not found") + } + } + return nil, err + } + + if err := record(ctx, s.Rec, st.Db, Change{ + By: apptest.ShiftService_Add_FullMethodName, + Key: u.Id, + }); err != nil { + return nil, err + } + if err := tx.Commit(); err != nil { + return nil, err + } + + return u.Proto(), nil +} + +func (s ShiftServiceServer) Get(ctx context.Context, req *apptest.ShiftGetRequest) (*apptest.Shift, error) { + p, err := ShiftPick(req.GetRef()) + if err != nil { + return nil, err + } + p, err = s.narrow(ctx, p) + if err != nil { + return nil, err + } + + q := s.Db.Shift.Query().Where(p) + ShiftSelectInit(q, req.GetSelect()) + + v, err := q.Only(ctx) + if err != nil { + if ent.IsNotFound(err) { + return nil, status.Error(codes.NotFound, "Shift not found") + } + return nil, err + } + return v.Proto(), nil +} + +func selectShiftKey(q *ent.ShiftQuery) { + q.Select(shift.FieldId) +} + +func ShiftSelectedFields(m *apptest.ShiftSelect) []string { + if m.GetAll() { + return shift.Columns + } + + vs := make([]string, 0, len(shift.Columns)) + { + vs = append(vs, shift.FieldId) + } + if m.GetDesk() { + vs = append(vs, shift.FieldDesk) + } + if m.GetStartsAt() { + vs = append(vs, shift.FieldStartsAt) + } + + return vs +} + +func ShiftSelect(q *ent.ShiftQuery, m *apptest.ShiftSelect) { + if !m.GetAll() { + fields := ShiftSelectedFields(m) + q.Select(fields...) + } +} + +func ShiftSelectInit(q *ent.ShiftQuery, m *apptest.ShiftSelect) { + if m != nil { + ShiftSelect(q, m) + } else { + } +} + +func (s ShiftServiceServer) Patch(ctx context.Context, req *apptest.ShiftPatchRequest) (*apptest.Shift, error) { + doc, err := ormpatch.FromPatchRequest(shiftOrmEntity, req.ProtoReflect(), nil) + if err != nil { + if _, ok := status.FromError(err); ok { + return nil, err + } + if errors.Is(err, ormpatch.ErrRequestLayout) { + return nil, status.Errorf(codes.Internal, "%s", err) + } + if errors.Is(err, ormpatch.ErrUnsupported) { + return nil, status.Errorf(codes.Unimplemented, "%s", err) + } + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + + return s.apply(ctx, req.GetRef(), doc, apptest.ShiftService_Patch_FullMethodName) +} + +func ShiftGetKey(ctx context.Context, db *ent.Client, ref *apptest.ShiftRef) (uuid.UUID, error) { + var z uuid.UUID + if ref.HasId() { + if v, err := entuuid.FromBytes(ref.GetId()); err != nil { + return z, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + return v, nil + } + } + + p, err := ShiftPick(ref) + if err != nil { + return z, err + } + + v, err := db.Shift.Query().Where(p).OnlyId(ctx) + if err != nil { + if ent.IsNotFound(err) { + return z, status.Error(codes.NotFound, "Shift not found") + } + return z, err + } + + return v, nil +} + +var shiftOrmEntity = ormpatch.MustEntityOf(apptest.File_apptest_shift_proto, "Shift") + +var shiftPatchColumns = entpatch.Columns{ + 1: shift.FieldId, 2: shift.FieldDesk, 3: shift.FieldStartsAt} + +func (s ShiftServiceServer) Apply(ctx context.Context, req *apptest.ShiftApplyRequest) (*apptest.Shift, error) { + if !req.HasPatch() { + return nil, status.Errorf(codes.InvalidArgument, "%s", ormpatch.ErrNoPatch) + } + return s.apply(ctx, req.GetRef(), req.GetPatch(), apptest.ShiftService_Apply_FullMethodName) +} + +func (s ShiftServiceServer) apply(ctx context.Context, ref *apptest.ShiftRef, doc *patchpb.Patch, by string) (*apptest.Shift, error) { + plan := &ormpatch.Plan{Entity: shiftOrmEntity} + if doc != nil { + v, err := ormpatch.Compile(shiftOrmEntity, doc) + if err != nil { + if errors.Is(err, ormpatch.ErrUnsupported) { + return nil, status.Errorf(codes.Unimplemented, "%s", err) + } + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + plan = v + } + + pred, mod, err := entpatch.Build(plan, shiftPatchColumns, s.Db.Dialect()) + if err != nil { + if errors.Is(err, entpatch.ErrValue) { + return nil, status.Errorf(codes.InvalidArgument, "%s", err) + } + return nil, status.Errorf(codes.Internal, "%s", err) + } + + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, true) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + k, err := ShiftGetKey(ctx, st.Db, ref) + if err != nil { + return nil, err + } + at := &apptest.ShiftRef{} + at.SetId(k[:]) + p, err := s.narrow(ctx, shift.IdEQ(k)) + if err != nil { + return nil, err + } + + if mod == nil { + q := st.Db.Shift.Query().Where(p) + if pred != nil { + q.Where(predicate.Shift(pred)) + } + if ok, err := q.Exist(ctx); err != nil { + return nil, err + } else if !ok { + return nil, func() error { + if ok, err := st.Db.Shift.Query().Where(p).Exist(ctx); err != nil { + return err + } else if !ok { + return status.Error(codes.NotFound, "Shift not found") + } + return status.Error(codes.FailedPrecondition, "a test in the patch did not hold") + }() + } + } else { + q := st.Db.Shift.Update().Where(p) + if pred != nil { + q.Where(predicate.Shift(pred)) + } + q.Modify(mod) + if n, err := q.Save(ctx); err != nil { + if err, ok := err.(*ent.ConstraintError); ok { + if sqlgraph.IsUniqueConstraintError(err) { + return nil, status.Error(codes.AlreadyExists, "Shift already exists") + } + if sqlgraph.IsForeignKeyConstraintError(err) { + return nil, status.Error(codes.NotFound, "Shift: referenced entity not found") + } + } + return nil, err + } else if n == 0 { + return nil, func() error { + if ok, err := st.Db.Shift.Query().Where(p).Exist(ctx); err != nil { + return err + } else if !ok { + return status.Error(codes.NotFound, "Shift not found") + } + return status.Error(codes.FailedPrecondition, "a test in the patch did not hold") + }() + } + } + + if mod != nil { + if err := record(ctx, s.Rec, st.Db, Change{ + By: by, + Key: k, + Patch: doc, + }); err != nil { + return nil, err + } + } + + out, err := st.Get(ctx, at.Pick()) + if err != nil { + return nil, err + } + if err := tx.Commit(); err != nil { + return nil, err + } + return out, nil +} + +func (s ShiftServiceServer) Erase(ctx context.Context, req *apptest.ShiftRef) (*apptest.ShiftEraseResponse, error) { + p, err := ShiftPick(req) + if err != nil { + return nil, err + } + p, err = s.narrow(ctx, p) + if err != nil { + return nil, err + } + + tx, err := ent1.JoinTx[*ent.Client, *ent.Tx](ctx, s.Db, s.Rec != nil) + if err != nil { + return nil, err + } + defer tx.Close() + + st := s + st.Db = tx.Db + + var k any + if s.Rec != nil { + v, err := st.Db.Shift.Query().Where(p).OnlyId(ctx) + if err != nil { + if ent.IsNotFound(err) { + return &apptest.ShiftEraseResponse{}, nil + } + return nil, err + } + + k = v + p = shift.And(p, shift.IdEQ(v)) + } + + n, err := st.Db.Shift.Delete().Where(p).Exec(ctx) + if err != nil { + return nil, err + } + if n > 0 { + if err := record(ctx, s.Rec, st.Db, Change{ + By: apptest.ShiftService_Erase_FullMethodName, + Key: k, + }); err != nil { + return nil, err + } + } + if err := tx.Commit(); err != nil { + return nil, err + } + res := &apptest.ShiftEraseResponse{} + res.SetErased(n > 0) + + return res, nil +} + +func ShiftPick(req *apptest.ShiftRef) (predicate.Shift, error) { + switch req.WhichKey() { + case apptest.ShiftRef_Id_case: + if v, err := entuuid.FromBytes(req.GetId()); err != nil { + return nil, status.Errorf(codes.InvalidArgument, "id: %s", err) + } else { + return shift.IdEQ(v), nil + } + case apptest.ShiftRef_Slot_case: + k := req.GetSlot() + ps := make([]predicate.Shift, 0, 2) + ps = append(ps, shift.DeskEQ(k.GetDesk())) + ps = append(ps, shift.StartsAtEQ(k.GetStartsAt().AsTime())) + return shift.And(ps...), nil + case apptest.ShiftRef_Key_not_set_case: + return nil, status.Errorf(codes.InvalidArgument, "key not set: Shift") + default: + return nil, status.Errorf(codes.Unimplemented, "unknown type of key: %s", req.WhichKey()) + } +} diff --git a/internal/apptest/server/bare/store.g.go b/internal/apptest/server/bare/store.g.go index 55a8e97..3c6f920 100644 --- a/internal/apptest/server/bare/store.g.go +++ b/internal/apptest/server/bare/store.g.go @@ -15,6 +15,7 @@ import ( note "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/note" predicate "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/predicate" seat "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/seat" + shift "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/shift" tenant "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/tenant" user "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/user" valuefield "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest/ent/valuefield" @@ -321,6 +322,7 @@ type Scope interface { TenantScope(ctx context.Context) (predicate.Tenant, error) UserScope(ctx context.Context) (predicate.User, error) SeatScope(ctx context.Context) (predicate.Seat, error) + ShiftScope(ctx context.Context) (predicate.Shift, error) } // Unscoped is a [Scope] that narrows nothing. Embed it and write out the @@ -356,6 +358,9 @@ func (Unscoped) UserScope(_ context.Context) (predicate.User, error) { func (Unscoped) SeatScope(_ context.Context) (predicate.Seat, error) { return nil, nil } +func (Unscoped) ShiftScope(_ context.Context) (predicate.Shift, error) { + return nil, nil +} // Scopes is several [Scope]s at once: a row is in scope when every one of // them says so. @@ -516,6 +521,26 @@ func (ss Scopes) SeatScope(ctx context.Context) (predicate.Seat, error) { return seat.And(ps...), nil } +func (ss Scopes) ShiftScope(ctx context.Context) (predicate.Shift, error) { + ps := make([]predicate.Shift, 0, len(ss)) + for _, s := range ss { + p, err := s.ShiftScope(ctx) + if err != nil { + return nil, err + } + if p == nil { + continue + } + + ps = append(ps, p) + } + if len(ps) == 0 { + return nil, nil + } + + return shift.And(ps...), nil +} + // Minter decides the key a row is stored under. It is asked once per Add, // for an entity whose key is a uuid, and only for those. // @@ -647,3 +672,4 @@ func (s Server) Note() apptest.NoteServiceServer { return NoteServiceServer{ func (s Server) Tenant() apptest.TenantServiceServer { return TenantServiceServer{Store: s.Store} } func (s Server) User() apptest.UserServiceServer { return UserServiceServer{Store: s.Store} } func (s Server) Seat() apptest.SeatServiceServer { return SeatServiceServer{Store: s.Store} } +func (s Server) Shift() apptest.ShiftServiceServer { return ShiftServiceServer{Store: s.Store} } diff --git a/internal/apptest/server/bare/x-indextime_test.go b/internal/apptest/server/bare/x-indextime_test.go new file mode 100644 index 0000000..50a7cdf --- /dev/null +++ b/internal/apptest/server/bare/x-indextime_test.go @@ -0,0 +1,49 @@ +package bare_test + +import ( + "context" + "testing" + "time" + + pb "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest" + "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// A unique index with a timestamp among its fields finds its row by all of +// them. The lookup used to return at the timestamp: what came before it was +// dropped and what came after was never read, so a reference by (desk, +// starts_at) matched every row at that time, and `Get` answered whichever the +// database returned first. +func TestRefByAnIndexWithATimestamp(t *testing.T) { + T(func(ctx context.Context, x *require.Assertions, c *Client) { + at := time.Date(2026, 10, 10, 9, 0, 0, 0, time.UTC) + + // Two shifts at one time, at two desks: the time alone names both. + a, err := c.Shift().Add(ctx, pb.ShiftAddRequest_builder{Desk: "a", StartsAt: timestamppb.New(at)}.Build()) + x.NoError(err) + b, err := c.Shift().Add(ctx, pb.ShiftAddRequest_builder{Desk: "b", StartsAt: timestamppb.New(at)}.Build()) + x.NoError(err) + + for _, want := range []*pb.Shift{a, b} { + got, err := c.Shift().Get(ctx, pb.ShiftGetRequest_builder{ + Ref: pb.ShiftBySlot(want.GetDesk(), timestamppb.New(at)), + }.Build()) + x.NoError(err) + x.Equal(want.GetId(), got.GetId(), "desk %s", want.GetDesk()) + } + + // A desk with nothing at that time is nothing, not the first row at it. + _, err = c.Shift().Get(ctx, pb.ShiftGetRequest_builder{Ref: pb.ShiftBySlot("c", timestamppb.New(at))}.Build()) + x.Equal(codes.NotFound, status.Code(err)) + + // And the time still counts: the same desk at another time is another + // shift, or none. + _, err = c.Shift().Get(ctx, pb.ShiftGetRequest_builder{ + Ref: pb.ShiftBySlot("a", timestamppb.New(at.Add(time.Minute))), + }.Build()) + x.Equal(codes.NotFound, status.Code(err)) + })(t) +} diff --git a/internal/apptest/store.g.go b/internal/apptest/store.g.go index 2d83db6..7f173b6 100644 --- a/internal/apptest/store.g.go +++ b/internal/apptest/store.g.go @@ -33,6 +33,7 @@ type Server interface { Tenant() TenantServiceServer User() UserServiceServer Seat() SeatServiceServer + Shift() ShiftServiceServer } // RegisterServer registers every service of `s` with `g`. @@ -47,6 +48,7 @@ func RegisterServer(g grpc.ServiceRegistrar, s Server) { RegisterTenantServiceServer(g, s.Tenant()) RegisterUserServiceServer(g, s.User()) RegisterSeatServiceServer(g, s.Seat()) + RegisterShiftServiceServer(g, s.Shift()) } type UnimplementedServer struct { @@ -57,6 +59,7 @@ type UnimplementedServer struct { TenantServer TenantServiceServer UserServer UserServiceServer SeatServer SeatServiceServer + ShiftServer ShiftServiceServer } func (UnimplementedServer) ValueField() ValueFieldServiceServer { @@ -72,6 +75,7 @@ func (UnimplementedServer) Note() NoteServiceServer { return UnimplementedNo func (UnimplementedServer) Tenant() TenantServiceServer { return UnimplementedTenantServiceServer{} } func (UnimplementedServer) User() UserServiceServer { return UnimplementedUserServiceServer{} } func (UnimplementedServer) Seat() SeatServiceServer { return UnimplementedSeatServiceServer{} } +func (UnimplementedServer) Shift() ShiftServiceServer { return UnimplementedShiftServiceServer{} } type StaticServer struct { ValueFieldServer ValueFieldServiceServer @@ -81,6 +85,7 @@ type StaticServer struct { TenantServer TenantServiceServer UserServer UserServiceServer SeatServer SeatServiceServer + ShiftServer ShiftServiceServer } func (s StaticServer) ValueField() ValueFieldServiceServer { return s.ValueFieldServer } @@ -90,6 +95,7 @@ func (s StaticServer) Note() NoteServiceServer { return s.NoteSe func (s StaticServer) Tenant() TenantServiceServer { return s.TenantServer } func (s StaticServer) User() UserServiceServer { return s.UserServer } func (s StaticServer) Seat() SeatServiceServer { return s.SeatServer } +func (s StaticServer) Shift() ShiftServiceServer { return s.ShiftServer } type Client interface { ValueField() ValueFieldServiceClient @@ -99,6 +105,7 @@ type Client interface { Tenant() TenantServiceClient User() UserServiceClient Seat() SeatServiceClient + Shift() ShiftServiceClient } func NewClient(c *grpc.ClientConn) Client { @@ -110,6 +117,7 @@ func NewClient(c *grpc.ClientConn) Client { _Tenant: NewTenantServiceClient(c), _User: NewUserServiceClient(c), _Seat: NewSeatServiceClient(c), + _Shift: NewShiftServiceClient(c), } } @@ -121,6 +129,7 @@ type client struct { _Tenant TenantServiceClient _User UserServiceClient _Seat SeatServiceClient + _Shift ShiftServiceClient } func (c *client) ValueField() ValueFieldServiceClient { return c._ValueField } @@ -130,6 +139,7 @@ func (c *client) Note() NoteServiceClient { return c._Note } func (c *client) Tenant() TenantServiceClient { return c._Tenant } func (c *client) User() UserServiceClient { return c._User } func (c *client) Seat() SeatServiceClient { return c._Seat } +func (c *client) Shift() ShiftServiceClient { return c._Shift } // Middleware is a server that delegates to another server. type Middleware interface { diff --git a/proto/apptest/shift.proto b/proto/apptest/shift.proto new file mode 100644 index 0000000..b5d8c7b --- /dev/null +++ b/proto/apptest/shift.proto @@ -0,0 +1,44 @@ +edition = "2023"; + +package apptest; + +import "google/protobuf/timestamp.proto"; +import "orm.proto"; + +option features.field_presence = IMPLICIT; +option go_package = "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest"; + +// Shift is unique over a desk and the time it starts, which is the shape of +// any index with a timestamp among its fields. A lookup by it has to read +// every field of the index: two shifts may start at the same time at two +// desks, and the time alone names both. +message Shift { + bytes id = 1 [(orm.field) = { + type: TYPE_UUID + key: true + default: "" + }]; + + string desk = 2; + google.protobuf.Timestamp starts_at = 3; + + option (orm.message) = { + rpc: {crud: true} + indexes: [ + { + name: "slot" + refs: [ + { + name: "desk" + number: 2 + }, + { + name: "starts_at" + number: 3 + } + ] + unique: true + } + ] + }; +} diff --git a/proto/apptest/shift_svc.g.proto b/proto/apptest/shift_svc.g.proto new file mode 100644 index 0000000..3da862b --- /dev/null +++ b/proto/apptest/shift_svc.g.proto @@ -0,0 +1,76 @@ +// Code generated by protoc-gen-orm-service. DO NOT EDIT. +// source: apptest/shift.proto + +edition = "2023"; + +package apptest; + +import "apptest/shift.proto"; +import "google/protobuf/timestamp.proto"; +import "patch/patch.proto"; + +option go_package = "github.com/protobuf-orm/protoc-gen-orm-ent/internal/apptest"; + +service ShiftService { + // Add creates a new Shift + rpc Add(ShiftAddRequest) returns (Shift); + // Get retrieves a Shift + rpc Get(ShiftGetRequest) returns (Shift); + // Patch updates an existing Shift + rpc Patch(ShiftPatchRequest) returns (Shift); + // Apply applies a patch document to an existing Shift + rpc Apply(ShiftApplyRequest) returns (Shift); + // Erase deletes a Shift + rpc Erase(ShiftRef) returns (ShiftEraseResponse); +} + +message ShiftAddRequest { + bytes id = 1; + string desk = 2 [ + features.field_presence = IMPLICIT + ]; + google.protobuf.Timestamp starts_at = 3; +} + +message ShiftRefBySlot { + string desk = 2; + google.protobuf.Timestamp starts_at = 3; +} + +message ShiftRef { + oneof key { + bytes id = 1; + ShiftRefBySlot slot = 2; + } +} + +message ShiftSelect { + bool all = 1; + bool desk = 2; + bool starts_at = 3; +} + +message ShiftGetRequest { + ShiftRef ref = 1; + ShiftSelect select = 2; +} + +message ShiftPatchRequest { + ShiftRef ref = 1; + string desk = 4; + google.protobuf.Timestamp starts_at = 6; +} + +message ShiftApplyRequest { + ShiftRef ref = 1; + patch.Patch patch = 2; +} + +message ShiftEraseResponse { + // Erased is whether this call is the one that erased the row. + // + // False for a row that was already gone, was never there, or is out + // of this caller's reach -- which are one answer on purpose, and the + // reason the RPC does not fail instead. + bool erased = 1; +}