basic list

This commit is contained in:
Georges Chaudy 2024-07-08 10:04:13 +02:00
parent 4275a01bc2
commit 26a2f947e8
No known key found for this signature in database
GPG Key ID: 0EE887FFCA1DB6EF
15 changed files with 136 additions and 326 deletions

View File

@ -504,6 +504,7 @@ func (s *server) List(ctx context.Context, req *ListRequest) (*ListResponse, err
if err := s.Init(); err != nil {
return nil, err
}
// TODO:
rsp, err := s.backend.PrepareList(ctx, req)
// Status???

View File

@ -377,9 +377,9 @@ func (b *backend) Read(ctx context.Context, req *resource.ReadRequest) (*resourc
}
readReq := sqlResourceReadRequest{
SQLTemplate: sqltemplate.New(b.sqlDialect),
Request: req,
readResponseSet: new(readResponseSet),
SQLTemplate: sqltemplate.New(b.sqlDialect),
Request: req,
readResponse: new(readResponse),
}
sr := sqlResourceRead
@ -399,12 +399,40 @@ func (b *backend) Read(ctx context.Context, req *resource.ReadRequest) (*resourc
}
func (b *backend) PrepareList(ctx context.Context, req *resource.ListRequest) (*resource.ListResponse, error) {
_, span := b.tracer.Start(ctx, "storage_server.List")
_, span := b.tracer.Start(ctx, trace_prefix+"List")
defer span.End()
fmt.Printf("TODO, LIST: %+v", req.Options.Key)
readReq := sqlResourceListRequest{
SQLTemplate: sqltemplate.New(b.sqlDialect),
Request: req,
Response: new(resource.ResourceWrapper),
}
query, err := sqltemplate.Execute(sqlResourceList, readReq)
if err != nil {
return nil, fmt.Errorf("execute SQL template to list resources: %w", err)
}
return nil, ErrNotImplementedYet
rows, err := b.sqlDB.QueryContext(ctx, query, readReq.GetArgs()...)
if err != nil {
return nil, fmt.Errorf("list resources: %w", err)
}
out := &resource.ListResponse{
Items: make([]*resource.ResourceWrapper, req.Limit),
ResourceVersion: 0, // TODO
}
for i := 1; rows.Next(); i++ {
if ctx.Err() != nil {
return nil, ctx.Err()
}
if err := rows.Scan(readReq.GetScanDest()...); err != nil {
return nil, fmt.Errorf("scan row #%d: %w", i, err)
}
rw := *readReq.Response
out.Items = append(out.Items, &rw)
}
return out, nil
}
func (b *backend) WatchWriteEvents(ctx context.Context) (<-chan *resource.WrittenEvent, error) {
@ -491,6 +519,9 @@ func (b *backend) poll(ctx context.Context, since int64, stream chan<- *resource
// resourceVersionAtomicInc atomically increases the version of a kind within a
// transaction.
// TODO: Ideally we should attempt to update the RV in the resource and resource_history tables
// in a single roundtrip. This would reduce the latency of the operation, and also increase the
// throughput of the system. This is a good candidate for a future optimization.
func resourceVersionAtomicInc(ctx context.Context, x db.ContextExecer, d sqltemplate.Dialect, key *resource.ResourceKey) (newVersion int64, err error) {
// 1. Increment the resource version

View File

@ -11,14 +11,14 @@ import (
"github.com/grafana/grafana/pkg/storage/unified/resource"
"github.com/grafana/grafana/pkg/storage/unified/sql/db/dbimpl"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/zeebo/assert"
"github.com/stretchr/testify/assert"
)
func TestMain(m *testing.M) {
testsuite.Run(m)
}
func TestBackendCRUDLW(t *testing.T) {
func TestBackendHappyPath(t *testing.T) {
ctx := context.Background()
dbstore := db.InitTestDB(t)
@ -34,11 +34,11 @@ func TestBackendCRUDLW(t *testing.T) {
stream, err := store.WatchWriteEvents(ctx)
assert.NoError(t, err)
t.Run("WriteEvent Add 3 objects", func(t *testing.T) {
t.Run("Add 3 resources", func(t *testing.T) {
for i := 1; i <= 3; i++ {
rv, err := store.WriteEvent(ctx, resource.WriteEvent{
Type: resource.WatchEvent_ADDED,
Value: []byte("initial value"),
Value: []byte("initial value " + strconv.Itoa(i)),
Key: &resource.ResourceKey{
Namespace: "namespace",
Group: "group",
@ -51,7 +51,7 @@ func TestBackendCRUDLW(t *testing.T) {
}
})
t.Run("WriteEvent Update item2", func(t *testing.T) {
t.Run("Update item2", func(t *testing.T) {
rv, err := store.WriteEvent(ctx, resource.WriteEvent{
Type: resource.WatchEvent_MODIFIED,
Value: []byte("updated value"),
@ -66,7 +66,7 @@ func TestBackendCRUDLW(t *testing.T) {
assert.Equal(t, int64(4), rv)
})
t.Run("WriteEvent Delete item1", func(t *testing.T) {
t.Run("Delete item1", func(t *testing.T) {
rv, err := store.WriteEvent(ctx, resource.WriteEvent{
Type: resource.WatchEvent_DELETED,
Key: &resource.ResourceKey{
@ -80,7 +80,7 @@ func TestBackendCRUDLW(t *testing.T) {
assert.Equal(t, int64(5), rv)
})
t.Run("Read latest", func(t *testing.T) {
t.Run("Read latest item 2", func(t *testing.T) {
resp, err := store.Read(ctx, &resource.ReadRequest{
Key: &resource.ResourceKey{
Namespace: "namespace",
@ -94,7 +94,7 @@ func TestBackendCRUDLW(t *testing.T) {
assert.Equal(t, "updated value", string(resp.Value))
})
t.Run("Read early verions", func(t *testing.T) {
t.Run("Read early verion of item2", func(t *testing.T) {
resp, err := store.Read(ctx, &resource.ReadRequest{
Key: &resource.ResourceKey{
Namespace: "namespace",
@ -106,32 +106,40 @@ func TestBackendCRUDLW(t *testing.T) {
})
assert.NoError(t, err)
assert.Equal(t, int64(2), resp.ResourceVersion)
assert.Equal(t, "initial value", string(resp.Value))
assert.Equal(t, "initial value 2", string(resp.Value))
})
t.Run("PrepareList latest", func(t *testing.T) {
resp, err := store.PrepareList(ctx, &resource.ListRequest{})
assert.NoError(t, err)
assert.Len(t, resp.Items, 2)
assert.Equal(t, "updated value", string(resp.Items[0].Value))
assert.Equal(t, "initial value 3", string(resp.Items[1].Value))
})
t.Run("Watch events", func(t *testing.T) {
event := <-stream
assert.Equal(t, "item1", event.Key.Name)
assert.Equal(t, 1, event.ResourceVersion)
assert.Equal(t, int64(1), event.ResourceVersion)
assert.Equal(t, resource.WatchEvent_ADDED, event.Type)
event = <-stream
assert.Equal(t, "item2", event.Key.Name)
assert.Equal(t, 2, event.ResourceVersion)
assert.Equal(t, int64(2), event.ResourceVersion)
assert.Equal(t, resource.WatchEvent_ADDED, event.Type)
event = <-stream
assert.Equal(t, "item3", event.Key.Name)
assert.Equal(t, 3, event.ResourceVersion)
assert.Equal(t, int64(3), event.ResourceVersion)
assert.Equal(t, resource.WatchEvent_ADDED, event.Type)
event = <-stream
assert.Equal(t, "item2", event.Key.Name)
assert.Equal(t, 4, event.ResourceVersion)
assert.Equal(t, int64(4), event.ResourceVersion)
assert.Equal(t, resource.WatchEvent_MODIFIED, event.Type)
event = <-stream
assert.Equal(t, "item1", event.Key.Name)
assert.Equal(t, 5, event.ResourceVersion)
assert.Equal(t, int64(5), event.ResourceVersion)
assert.Equal(t, resource.WatchEvent_DELETED, event.Type)
})
}

View File

@ -0,0 +1,20 @@
SELECT
{{ .Ident "resource_version" | .Into .Response.ResourceVersion }},
{{ .Ident "value" | .Into .Response.Value }}
FROM {{ .Ident "resource" }}
WHERE 1 = 1
{{ if and .Request.Options .Request.Options.Key }}
{{ if .Request.Options.Key.Namespace }}
AND {{ .Ident "namespace" }} = {{ .Arg .Request.Options.Key.Namespace }}
{{ end }}
{{ if .Request.Options.Key.Group }}
AND {{ .Ident "group" }} = {{ .Arg .Request.Options.Key.Group }}
{{ end }}
{{ if .Request.Options.Key.Resource }}
AND {{ .Ident "resource" }} = {{ .Arg .Request.Options.Key.Resource }}
{{ end }}
{{ if .Request.Options.Key.Name }}
AND {{ .Ident "name" }} = {{ .Arg .Request.Options.Key.Name }}
{{ end }}
{{ end }}
;

View File

@ -37,6 +37,7 @@ var (
sqlResourceInsert = mustTemplate("resource_insert.sql")
sqlResourceUpdate = mustTemplate("resource_update.sql")
sqlResourceRead = mustTemplate("resource_read.sql")
sqlResourceList = mustTemplate("resource_list.sql")
sqlResourceUpdateRV = mustTemplate("resource_update_rv.sql")
sqlResourceHistoryRead = mustTemplate("resource_history_read.sql")
sqlResourceHistoryUpdateRV = mustTemplate("resource_history_update_rv.sql")
@ -97,8 +98,6 @@ func (r sqlResourceRequest) Validate() error {
return nil // TODO
}
// sqlResourceHistoryReadRequest can be used to retrieve a row fromthe "resource_history" tables.
type historyPollResponse struct {
Key resource.ResourceKey
ResourceVersion int64
@ -122,24 +121,43 @@ func (r sqlResourceHistoryPollRequest) Validate() error {
// sqlResourceReadRequest can be used to retrieve a row fromthe "resource" tables.
type readResponseSet struct {
type key struct {
resource.ReadResponse
}
func (r *readResponseSet) Results() (*readResponseSet, error) {
type readRequest struct {
resource.ReadResponse
}
type readResponse struct {
resource.ReadResponse
}
func (r *readResponse) Results() (*readResponse, error) {
return r, nil
}
type sqlResourceReadRequest struct {
*sqltemplate.SQLTemplate
Request *resource.ReadRequest
*readResponseSet
*readResponse
}
func (r sqlResourceReadRequest) Validate() error {
return nil // TODO
}
// List
type sqlResourceListRequest struct {
*sqltemplate.SQLTemplate
Request *resource.ListRequest
Response *resource.ResourceWrapper
}
func (r sqlResourceListRequest) Validate() error {
return nil // TODO
}
// update RV
type sqlResourceUpdateRVRequest struct {

View File

@ -160,7 +160,7 @@ func TestQueries(t *testing.T) {
Request: &resource.ReadRequest{
Key: &resource.ResourceKey{},
},
readResponseSet: new(readResponseSet),
readResponse: new(readResponse),
},
Expected: expected{
"resource_read_mysql_sqlite.sql": dialects{
@ -170,6 +170,28 @@ func TestQueries(t *testing.T) {
},
},
},
sqlResourceList: {
{
Name: "filter on namespace",
Data: &sqlResourceListRequest{
SQLTemplate: new(sqltemplate.SQLTemplate),
Request: &resource.ListRequest{
Options: &resource.ListOptions{
Key: &resource.ResourceKey{
Namespace: "ns",
},
},
},
Response: new(resource.ResourceWrapper),
},
Expected: expected{
"resource_list_mysql_sqlite.sql": dialects{
sqltemplate.MySQL,
sqltemplate.SQLite,
},
},
},
},
sqlResourceUpdateRV: {
{
Name: "single path",
@ -189,6 +211,11 @@ func TestQueries(t *testing.T) {
Name: "single path",
Data: &sqlResourceReadRequest{
SQLTemplate: new(sqltemplate.SQLTemplate),
Request: &resource.ReadRequest{
ResourceVersion: 123,
Key: &resource.ResourceKey{},
},
readResponse: new(readResponse),
},
Expected: expected{
"resource_history_read_mysql_sqlite.sql": dialects{
@ -317,282 +344,3 @@ func TestQueries(t *testing.T) {
})
}
}
// func TestReadEntity(t *testing.T) {
// t.Parallel()
// // readonly, shared data for all subtests
// expectedEntity := newEmptyEntity()
// testdataJSON(t, `grpc-res-entity.json`, expectedEntity)
// key, err := grafanaregistry.ParseKey(expectedEntity.Key)
// require.NoErrorf(t, err, "provided key: %#v", expectedEntity)
// t.Run("happy path - entity table, optimistic locking", func(t *testing.T) {
// t.Parallel()
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// x := expectReadEntity(t, mock, cloneEntity(expectedEntity))
// x(ctx, db)
// })
// t.Run("happy path - entity table, no optimistic locking", func(t *testing.T) {
// t.Parallel()
// // test declarations
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// readReq := sqlEntityReadRequest{ // used to generate mock results
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
// Key: new(grafanaregistry.Key),
// returnsEntitySet: newReturnsEntitySet(),
// }
// readReq.Entity.Entity = cloneEntity(expectedEntity)
// results := newMockResults(t, mock, sqlEntityRead, readReq)
// // setup expectations
// results.AddCurrentData()
// mock.ExpectQuery(`select from entity where !resource_version update`).
// WillReturnRows(results.Rows())
// // execute and assert
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key, 0, false, true)
// require.NoError(t, err)
// require.Equal(t, expectedEntity, e.Entity)
// })
// t.Run("happy path - entity_history table", func(t *testing.T) {
// t.Parallel()
// // test declarations
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// readReq := sqlEntityReadRequest{ // used to generate mock results
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
// Key: new(grafanaregistry.Key),
// returnsEntitySet: newReturnsEntitySet(),
// }
// readReq.Entity.Entity = cloneEntity(expectedEntity)
// results := newMockResults(t, mock, sqlEntityRead, readReq)
// // setup expectations
// results.AddCurrentData()
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
// WillReturnRows(results.Rows())
// // execute and assert
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
// expectedEntity.ResourceVersion, false, false)
// require.NoError(t, err)
// require.Equal(t, expectedEntity, e.Entity)
// })
// t.Run("entity table, optimistic locking failed", func(t *testing.T) {
// t.Parallel()
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// x := expectReadEntity(t, mock, nil)
// x(ctx, db)
// })
// t.Run("entity_history table, entity not found", func(t *testing.T) {
// t.Parallel()
// // test declarations
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// readReq := sqlEntityReadRequest{ // used to generate mock results
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
// Key: new(grafanaregistry.Key),
// returnsEntitySet: newReturnsEntitySet(),
// }
// results := newMockResults(t, mock, sqlEntityRead, readReq)
// // setup expectations
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
// WillReturnRows(results.Rows())
// // execute and assert
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
// expectedEntity.ResourceVersion, false, false)
// require.Nil(t, e)
// require.Error(t, err)
// require.ErrorIs(t, err, ErrNotFound)
// })
// t.Run("entity_history table, entity was deleted = not found", func(t *testing.T) {
// t.Parallel()
// // test declarations
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// readReq := sqlEntityReadRequest{ // used to generate mock results
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
// Key: new(grafanaregistry.Key),
// returnsEntitySet: newReturnsEntitySet(),
// }
// readReq.Entity.Entity = cloneEntity(expectedEntity)
// readReq.Entity.Entity.Action = entity.Entity_DELETED
// results := newMockResults(t, mock, sqlEntityRead, readReq)
// // setup expectations
// results.AddCurrentData()
// mock.ExpectQuery(`select from entity_history where resource_version !update`).
// WillReturnRows(results.Rows())
// // execute and assert
// e, err := readEntity(ctx, db, sqltemplate.MySQL, key,
// expectedEntity.ResourceVersion, false, false)
// require.Nil(t, e)
// require.Error(t, err)
// require.ErrorIs(t, err, ErrNotFound)
// })
// }
// // expectReadEntity arranges test expectations so that it's easier to reuse
// // across tests that need to call `readEntity`. If you provide a non-nil
// // *entity.Entity, that will be returned by `readEntity`. If it's nil, then
// // `readEntity` will return ErrOptimisticLockingFailed. It returns the function
// // to execute the actual test and assert the expectations that were set.
// func expectReadEntity(t *testing.T, mock sqlmock.Sqlmock, e *entity.Entity) func(ctx context.Context, db db.DB) {
// t.Helper()
// // test declarations
// readReq := sqlEntityReadRequest{ // used to generate mock results
// SQLTemplate: sqltemplate.New(sqltemplate.MySQL),
// Key: new(grafanaregistry.Key),
// returnsEntitySet: newReturnsEntitySet(),
// }
// results := newMockResults(t, mock, sqlEntityRead, readReq)
// if e != nil {
// readReq.Entity.Entity = cloneEntity(e)
// }
// // setup expectations
// results.AddCurrentData()
// mock.ExpectQuery(`select from entity where !resource_version update`).
// WillReturnRows(results.Rows())
// // execute and assert
// if e != nil {
// return func(ctx context.Context, db db.DB) {
// ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key,
// e.ResourceVersion, true, true)
// require.NoError(t, err)
// require.Equal(t, e, ent.Entity)
// }
// }
// return func(ctx context.Context, db db.DB) {
// ent, err := readEntity(ctx, db, sqltemplate.MySQL, readReq.Key, 1, true,
// true)
// require.Nil(t, ent)
// require.Error(t, err)
// require.ErrorIs(t, err, ErrOptimisticLockingFailed)
// }
// }
// func TestKindVersionAtomicInc(t *testing.T) {
// t.Parallel()
// t.Run("happy path - row locked", func(t *testing.T) {
// t.Parallel()
// // test declarations
// const curVersion int64 = 1
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// // setup expectations
// mock.ExpectQuery(`select resource_version from resource_version where group resource update`).
// WillReturnRows(mock.NewRows([]string{"resource_version"}).AddRow(curVersion))
// mock.ExpectExec("update resource_version set resource_version updated_at where group resource").
// WillReturnResult(sqlmock.NewResult(0, 1))
// // execute and assert
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
// require.NoError(t, err)
// require.Equal(t, curVersion+1, gotVersion)
// })
// t.Run("happy path - row created", func(t *testing.T) {
// t.Parallel()
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// x := expectKindVersionAtomicInc(t, mock, false)
// x(ctx, db)
// })
// t.Run("fail to create row", func(t *testing.T) {
// t.Parallel()
// ctx := testutil.NewDefaultTestContext(t)
// db, mock := newMockDBMatchWords(t)
// x := expectKindVersionAtomicInc(t, mock, true)
// x(ctx, db)
// })
// }
// // expectKindVersionAtomicInc arranges test expectations so that it's easier to
// // reuse across tests that need to call `kindVersionAtomicInc`. If you the test
// // shuld fail, it will do so with `errTest`, and it will return resource version
// // 1 otherwise. It returns the function to execute the actual test and assert
// // the expectations that were set.
// func expectKindVersionAtomicInc(t *testing.T, mock sqlmock.Sqlmock, shouldFail bool) func(ctx context.Context, db db.DB) {
// t.Helper()
// // setup expectations
// mock.ExpectQuery(`select resource_version from resource_version where group resource update`).
// WillReturnRows(mock.NewRows([]string{"resource_version"}))
// call := mock.ExpectExec("insert resource_version resource_version")
// // execute and assert
// if shouldFail {
// call.WillReturnError(errTest)
// return func(ctx context.Context, db db.DB) {
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
// require.Zero(t, gotVersion)
// require.Error(t, err)
// require.ErrorIs(t, err, errTest)
// }
// }
// call.WillReturnResult(sqlmock.NewResult(0, 1))
// return func(ctx context.Context, db db.DB) {
// gotVersion, err := kindVersionAtomicInc(ctx, db, sqltemplate.MySQL, "groupname", "resname")
// require.NoError(t, err)
// require.Equal(t, int64(1), gotVersion)
// }
// }
// func TestMustTemplate(t *testing.T) {
// t.Parallel()
// require.Panics(t, func() {
// mustTemplate("non existent file")
// })
// }
// // Debug provides greater detail about the SQL error. It is defined on the same
// // struct but on a test file so that the intention that its results should not
// // be used in runtime code is very clear. The results could include PII or
// // otherwise regulated information, hence this method is only available in
// // tests, so that it can be used in local debugging only. Note that the error
// // information may still be available through other means, like using the
// // "reflect" package, so care must be taken not to ever expose these information
// // in production.
// func (e SQLError) Debug() string {
// scanDestStr := "(none)"
// if len(e.ScanDest) > 0 {
// format := "[%T" + strings.Repeat(", %T", len(e.ScanDest)-1) + "]"
// scanDestStr = fmt.Sprintf(format, e.ScanDest...)
// }
// return fmt.Sprintf("%s: %s: %v\n\tArguments (%d): %#v\n\tReturn Value "+
// "Types (%d): %s\n\tExecuted Query: %s\n\tRaw SQL Template Output: %s",
// e.TemplateName, e.CallType, e.Err, len(e.arguments), e.arguments,
// len(e.ScanDest), scanDestStr, e.Query, e.RawQuery)
// }

View File

@ -1,3 +0,0 @@
INSERT INTO "entity_folder"
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);

View File

@ -1,5 +0,0 @@
INSERT INTO "entity_folder"
("guid", "namespace", "name", "slug_path", "tree", "depth", "lft", "rgt", "detached")
VALUES
(?, ?, ?, ?, ?, ?, ?, ?, ?),
(?, ?, ?, ?, ?, ?, ?, ?, ?);

View File

@ -1,5 +0,0 @@
SELECT e."guid", e."resource_version", e."key", e."group", e."group_version", e."resource", e."namespace", e."name", e."folder", e."meta", e."body", e."status", e."size", e."etag", e."created_at", e."created_by", e."updated_at", e."updated_by", e."origin", e."origin_key", e."origin_ts", e."title", e."slug", e."description", e."message", e."labels", e."fields", e."errors", e."action"
FROM "entity_history" AS e
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ? AND "resource_version" <= ?
ORDER BY "resource_version" DESC
LIMIT 1 FOR UPDATE NOWAIT;

View File

@ -1 +0,0 @@
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?);

View File

@ -1 +0,0 @@
DELETE FROM "entity_labels" WHERE 1 = 1 AND "guid" = ? AND "label" NOT IN (?, ?);

View File

@ -1,2 +0,0 @@
INSERT INTO "entity_labels" ("guid", "label", "value")
VALUES (?, ?, ?);

View File

@ -1,3 +0,0 @@
INSERT INTO "entity_labels" ("guid", "label", "value") VALUES
(?, ?, ?),
(?, ?, ?);

View File

@ -1,5 +1,6 @@
SELECT "resource_version", "value"
FROM "resource_history"
WHERE 1 = 1 AND "namespace" = ? AND "group" = ? AND "resource" = ? AND "name" = ? AND "resource_version" <= ?
ORDER BY "resource_version" DESC
ORDER BY "resource_version" DESC
LIMIT 1
;

View File

@ -0,0 +1,3 @@
SELECT "resource_version", "value"
FROM "resource"
WHERE 1 = 1 AND "namespace" = ?;