Conversation
NitinKumar004
left a comment
There was a problem hiding this comment.
Review notes
Real data-plane engine (per #427): N/A.
Findings
Medium · real-engine — Table names are globally unique, not per-compartment
providers/oci/nosql/nosql.go:417
If a user creates table "orders" in compartment A and then "orders" in compartment B -> the second CreateTable returns 409 AlreadyExists, because table names are a global key rather than compartment-scoped; real OCI scopes NoSQL table names per compartment and both creates succeed, so a multi-compartment SDK/CLI test that reuses a table name across compartments breaks against the emulator only.
m.tables is keyed by table name globally (nosql.go:175); newTable rejects with AlreadyExists on m.tables.Has(name) (nosql.go:417), and CreateOCITable checks m.tables.Get(d.Table) irrespective of spec.CompartmentID (table_extras.go:38-44). resolve()/lookup() also address tables by name globally.
Low · coverage — Portable Query/Scan operators and typed coercion below the 90% coverage pillar
providers/oci/nosql/rows.go:324
If a user issues a portable Query/Scan with a relational sort condition (>, BETWEEN) or stores a LONG/FLOAT/BOOLEAN-typed row -> the ordering (compareOrdering/compareStrings) and coercion (parseTyped/convertTyped) execute for the first time in production with no test guarding them, so a comparison- or coercion-logic regression would ship undetected.
go test -cover: provider 85.8%, server 83.4% (both < 90%). compareOrdering (rows.go:324) 0%, compareStrings (rows.go:341) 0%, compareOp (rows.go:306) 28.6%, parseTyped (row_extras.go:211) 35.7%, convertTyped (row_extras.go:242) 36.8%, SetMonitoring (nosql.go:194) 0%. Query tests exercise only SortOp "="; <, >, <=, >=, BETWEEN and CONTAINS/BEGINS_WITH ordering are never run.
Low · wire-fidelity — No oci-go-sdk SDK-compat test for the handler
server/oci/nosql/handler_test.go:94
If the real oci-go-sdk nosql client shapes a request/response field differently than the hand-modeled types.go structs (e.g. a body vs query-parameter for the Query limit, or a header the SDK requires) -> the divergence is not caught by the current tests, so a genuine client could fail at a step the httptest cases pass; adding one SDK create/get/query round-trip would close this.
handler_test.go drives the handler via raw httptest requests only; the only reference to github.com/oracle/oci-go-sdk is the package doc comment in handler.go. oci-conventions.md calls an SDK round-trip against httptest.NewServer "the strongest evidence the handler is right" (recommended, not required).
NitinKumar004
left a comment
There was a problem hiding this comment.
Review — OCI NoSQL Database
Verdict: Request changes — one blocker (persistence completeness), plus two local-gate failures and a rebase. The wire layer and lifecycle are strong; once these are addressed it's mergeable.
Blocking
Mock is not Snapshottable — providers/oci/nosql/nosql.go:168 (Mock).
The Mock holds tables and names memstore.Store fields, and each tableData nests an items row store, but there's no providers/oci/nosql/snapshot.go and no snapshot.Snapshottable. On a tree that includes the persistence-completeness guard:
--- FAIL: TestSnapshotCompleteness/oci
oci: field NoSQL (*nosql.Mock) holds a memstore.Store but is not Snapshottable
Consequence: CI Test goes red, and a snapshot/restore of the OCI provider silently drops every table and all row data. Fix: add snapshot.go with var _ snapshot.Snapshottable = (*Mock)(nil) + Snapshot/Restore covering tables including each table's nested items rows (the easy-to-forget part), names, and per-table Scope/Tags/Indexes/ttl. Note tableData's unexported fields (items, ttl, Scope) must be promoted/handled to round-trip, mirroring providers/oci/vcn/snapshot.go.
Should fix
- 4× golangci-lint
gocritic hugeParam—nosql.go:381(CreateTable),rows.go:102(UpdateItem),rows.go:206(Scan),indexes.go:12(CreateIndex). These are interface-fixed signatures; add the//nolint:gocritic // hugeParam: interface method signature cannot be changed.thatproviders/aws/dynamodband this PR's ownQuery/CreateOCITablealready carry. Thegolangci-lint run0-issues bar (per CLAUDE.md) currently fails. - Generated docs stale on rebase —
docs/coverage/oci/nosql.md.go generate ./...on the merged tree drops an "Optional capabilities / TableAttributes" block the current coveragegen no longer emits; re-rungo generateafter rebase and commit. - Branch is 781 commits behind
development— rebase; only generateddocs/coverage/README.mdconflicts (code auto-merges).
Non-blocking
- Query numeric-literal formatting —
query.go(rowMatches) /rows.go(compareOp). Comparisons stringify both sides viafmt.Sprintf("%v", …), so aDOUBLE5.0vs literal"5.0"(or a stored5) can mismatch on formatting. Documented SQL subset; the exact-match integer path is verified. Minor fidelity edge.
Verified good
Full lifecycle over the wire (CreateTable DDL → 202 + work request resolving SUCCEEDED synchronously → GetTable ACTIVE with full schema → UpdateRow → GetRow round-trips id:1 as an integer, not 1.0 → Query → CreateIndex → delete → 404): computed OCID/timeCreated byte-stable; dup→409, missing→404, row-under-missing-table→404, bad DDL type→400, ON_DEMAND-with-units→400. Async work-request wiring correct and complete; error taxonomy faithful; rollback on CreateTable name/DDL mismatch; create paths span check-then-set under one write lock (no TOCTOU race); Get/List/Query/Index deep-copy (no aliasing; -race clean); honest 501 stubs for usage/query/prepare/summarize/change-streams; structure conformant.
Implements OCI NoSQL Database against the portable database driver: providers/oci/nosql holds the mock over memstore, server/oci/nosql the /20190828 wire handler. Tables are created from a DDL statement rather than a key list, so the provider parses CREATE TABLE and ALTER TABLE — scalar and JSON column types, NOT NULL, DEFAULT, PRIMARY KEY with an optional SHARD, USING TTL in days, and ADD/DROP on a non-key column. Everything else is refused with the construct named: primary keys wider than the portable partition/sort key pair, composite shard keys, structured column types, generated and MR_COUNTER modifiers, TTL in hours, MODIFY and schema freezing, and JSON-path index keys. Tables carry an OCID, a compartment recorded at create and capacity limits validated against their mode; both list routes require compartmentId. Table and index mutations record a work request and stamp opc-work-request-id. Rows are addressed by typed primary key columns and written synchronously. The query endpoint runs SELECT * and DELETE FROM with AND-ed equality conditions — the REST API has no MultiDelete, so DELETE FROM ... WHERE is the multi-row delete. Table usage and the prepared-statement endpoints answer 501 naming the gap. OCI NoSQL publishes no change stream, so the portable stream operations report Unimplemented rather than an empty iterator. The OCI-only surface is a consumer-side Extras interface in server/oci/nosql with its value types in providers/oci/nosql; nothing is added to services/database/driver. Closes #412
OCI scopes a NoSQL table name to its compartment, so the same name in two compartments is two tables. The table store is keyed by compartment and name, an OCID still resolves across the tenancy, and every OCI entry point takes the compartment alongside the tableNameOrId — which is what OCI's own request models carry it for. The portable driver has no compartment in its shape: it addresses the compartment new resources default to, then the sole compartment holding that name, and leaves a name held by two others unaddressable rather than picking between them. Also covers the query and scan operators, the typed column coercions and the handler error branches that had no test.
Adds providers/oci/nosql/snapshot.go, mirroring the VCN mock's shared storeDump table so Snapshot and Restore cannot drift. Each table nests its own row store, which JSON neither writes nor rebuilds, so tableData carries custom JSON methods: the rows travel as a plain map and the store is remade from them, alongside the unexported attribute TTL. Rows are re-fitted to their declared column types on the way back, since JSON decodes every number to a float64 and an INTEGER key would otherwise come back as one. The mock mints no lazy default or counter, so the two stores are the whole of its state. A query condition on a numeric column is now compared by value rather than by the text form %v spells it in, so a DOUBLE holding 5 matches the literal 5.0. Other columns stay a text comparison, and the portable driver's equality — which has no column types to consult — documents the limit. Also adds the four hugeParam directives the interface-fixed portable signatures need, and re-runs the coverage generator.
b52eb41 to
9522865
Compare
|
Rebased onto Blocking —
|
NitinKumar004
left a comment
There was a problem hiding this comment.
The OCI NoSQL surface is in good shape overall, but one row-key collision loses data and a few wire behaviours differ from real OCI. These should be fixed before merge.
Earlier review points
- Mock not Snapshottable: fixed.
snapshot.goround-trips tables with their nested rows, andTestSnapshotCompletenessandpersistpass. - Table names global instead of per-compartment: fixed with the
compartment\x00namekey. - 4x gocritic hugeParam: fixed. golangci-lint reports 0 issues on the nosql packages and
server/oci. - Stale generated docs: fixed at the time. There is a new conflict now (see Low 1).
- Coverage below 90%: fixed. The provider is at 93.1% and the server at 95.6%.
- Numeric literal formatting in
rowMatches: fixed. - SDK-compat test: declined. One detail from that reply is wrong, see Low 2.
High
- Composite string keys collide, so one row silently overwrites another.
providers/oci/nosql/nosql.go:294(itemKey)
The store key isfmt.Sprintf("%v", shard) + ":" + fmt.Sprintf("%v", sort). That means(a="x:y", b="z")and(a="x", b="y:z")both map tox:y:z.
Repro:CREATE TABLE kv (a STRING, b STRING, v STRING, PRIMARY KEY(SHARD(a), b)), then UpdateRow{"a":"x:y","b":"z","v":"first"}and UpdateRow{"a":"x","b":"y:z","v":"second"}.SELECT * FROM kvreturns one row, and GetRow fora=x:y,b=zreturns thesecondrow. The portable driver uses the same key, so it is affected too.
Fix: build the key so it can't collide. Either length-prefix each component or JSON-encode the key tuple, e.g.json.Marshal([]any{shard, sort}).
Medium
-
UpdateTable applies the ALTER even when the request fails.
providers/oci/nosql/table_extras.go:143
applyAltermutates the table in place, and the limits are validated afterwards.
Repro: PUT/tables/userswithddlStatement: "ALTER TABLE users (ADD age INTEGER)"andtableLimits: {maxReadUnits:0, maxWriteUnits:0}. The call returns 400 InvalidParameter, but a following GetTable shows theagecolumn and the rewritten ddlStatement.timeUpdatedis not bumped either.
Fix: validate the limits (and anything else that can fail) first, or apply the changes to a copy and swap it in only on success. -
Row keys are percent-decoded twice.
server/oci/nosql/row.go:120
r.URL.Query()already decodes the value, anddecodeKeythen runsurl.QueryUnescapeon it again. The SDK encodeskeyonce (collectionFormat:"multi").
Repro: storek="a+b". ThenGET /tables/p/rows?key=k:a%2Bbreturns 404, because the+becomes a space. Storek="50%". Thenkey=k:50%25returns 400 "not valid percent-encoding". Those rows can't be read or deleted through GetRow or DeleteRow.
Fix: drop the secondQueryUnescape. -
The shard key defaults to the leading column instead of the whole primary key.
providers/oci/nosql/ddl.go:262
In Oracle NoSQL, when noSHARD(...)is given, the shard key is the full primary key.
Repro:CREATE TABLE users (id INTEGER, email STRING, name STRING, PRIMARY KEY(id, email)). GetTable then returns"shardKey":["id"], while real OCI returns["id","email"]. The stored ddlStatement is also rewritten after an ALTER toPRIMARY KEY (SHARD(id), email), which is a different schema.
Fix: reportshardKeyas the full primary key when no SHARD is declared. If the portable partition/sort mapping can't express that, keep that mapping internal and don't put it on the wire schema. Also preserve the user's DDL text. -
IF_ABSENT and IF_PRESENT failures return 409 where OCI returns 200.
providers/oci/nosql/row_extras.go:64andserver/oci/nosql/row.go:75
In OCI, a conditional put that fails its option is a normal 200 withversionnull andexistingVersion/existingValueset. A successful put returnsversion. Here a failed condition is409 IncorrectState, and a success returns{}.
Repro: UpdateRow withoption: "IF_ABSENT"on an existing row returns409 {"code":"IncorrectState"}. SDK callers checkresp.Version != nil, so they get a ServiceError instead.
Fix: return 200 with anUpdateRowResult. Setversionon success (an opaque string stored per row works) and leave it null on condition failure. AddexistingValuewhenisGetReturnRowis true. -
UpdateTable by OCID skips the compartment visibility check.
server/oci/nosql/table.go:121
getTable, deleteTable, the row paths and changeCompartment all go throughfindTable, which returns 404 when the OCID belongs to another compartment. updateTable callsUpdateOCITabledirectly.
Repro: GET/tables/<ocid>?compartmentId=<other>returns 404, but PUT/tables/<ocid>withcompartmentId:<other>returns 202 and the tags change.
Fix: callfindTablein updateTable as the other handlers do.
Low
- Merge conflict with current development:
docs/coverage/README.md(generated). The code auto-merges. On the merged tree,go run ./internal/coveragegenonly re-adds the NoSQL link in thedatabaserow, and theproviders/oci,server/oci,persistandcoveragegentests pass. Rebase and regenerate. - Query
limitis read from the body.server/oci/nosql/types.go:129. InQueryRequest,limitandpageare query parameters, andQueryDetailshas nolimitfield. Paging still works becausepaginatereads?limit=. Drop the body field and passocirest.Limit(r)through. isIfExistson DeleteTable is ignored.server/oci/nosql/table.go:134.DELETE /tables/users?isIfExists=trueon a missing table returns 404. Honour the flag.- Compartment existence is not checked. Since #1295, development wires a compartment checker for VCN in
server/oci/oci.go(nonexistent compartment returns 404 NotAuthorizedOrNotFound). This PR's handler accepts CreateTable intoocid1.compartment.oc1..doesnotexistwith 202. Wiring the sameSetCompartmentCheckerhere keeps OCI services consistent. - Table names are case-sensitive. Oracle NoSQL identifiers are case-insensitive, but GET
/tables/USERSafterCREATE TABLE usersreturns 404. Fold the case intableKeyand keep the declared spelling for display. - Work request
operationTypevalues are outside the NoSQL enum. The SDK enum isCREATE_TABLE,UPDATE_TABLE,DELETE_TABLEandUPDATE_CONFIGURATION.CREATE_INDEX,DELETE_INDEXandCHANGE_TABLE_COMPARTMENT(server/oci/nosql/handler.go:66-68) don't exist. Report index and compartment changes asUPDATE_TABLE. isPrepared: trueis silently ignored and the statement runs as plain text. Return 501 as/query/preparedoes, so nothing is accepted and then ignored.- Responses carry no
etag, andif-matchis ignored. GetTable, GetRow and UpdateRow returnetagin OCI. This matches other OCI handlers on development, so it can go in a follow-up issue.
Checked
- Clean build of the pushed SHA 9522865.
- Trial merge with development: one generated-file conflict. Build, vet and tests pass on the merged tree for
providers/oci/...,server/oci/...,persistandinternal/coveragegen. -raceis clean on both nosql packages andserver/oci. golangci-lint and gofmt report 0 issues.- Live serve over raw HTTP: create, get twice (byte-identical), list with paging (
opc-next-page), index create and get, row put, get and delete, query, DELETE FROM, delete followed by 404, and the work request resolving SUCCEEDED. - Wire field names compared against the oci-go-sdk nosql request and response models.
- Terraform was not run. The oracle/oci provider only targets HTTPS
nosql.<region>.oraclecloud.comand has no endpoint override, so raw HTTP against serve was used instead. - Authorization: OCI has no enforce-auth gate on development yet, so that check does not apply to this handler.
- Wiring:
providers/oci/oci.go,server/oci/oci.goandfrom_provider.go. The Snapshottable compile assert is present.
|
Please merge the latest |
Summary
databasedriver.CreateTabletakes a SQL-ish statement rather than an attribute list.services/database/driver; OCI-only behaviour is a consumer-sideExtrasinterface, per Move OCI-only capabilities out of shared driver packages #393.Closes #412. Part of #376.
Changes
providers/oci/nosql/—Mockovermemstoreimplementingdriver.Database, guarded by async.RWMutex, plus the DDL parser and a query evaluator.server/oci/nosql/— the/20190828/surface.providers/oci/oci.goandserver/oci/oci.go.Operations: tables (Create/List/Get/Update/Delete/ChangeCompartment), indexes (Create/List/Get/Delete), rows (GetRow/UpdateRow/DeleteRow), and Query. All 24 portable
Databasemethods implemented.DDL — what is parsed, what is refused
Parsed:
CREATE TABLEandALTER TABLE— scalar andJSONtypes,NOT NULL,DEFAULT,PRIMARY KEYwith optionalSHARD,USING TTL <n> DAYS,ADD/DROPon non-key columns,IF NOT EXISTS.Refused by name, never silently accepted:
ARRAY/MAP/RECORD/ENUMMR_COUNTER/UUIDmodifiersUSING TTL … HOURSSchemareports TTL only in daysMODIFY, schema freezing, JSON-path index keysORDER BY, range conditionsJudgement calls
UpdateStreamConfig/GetStreamRecordsreturnUnimplementedrather than an empty iterator. Noted indocs/services.md.DELETE FROM … WHEREover/query— OCI's REST API has noMultiDeleteoperation; this is the real mechanism.ListIndexesrequirescompartmentIdalthough real OCI marks it optional, so no list is ever unscoped. Documented./tables/{id}/usageand/query/prepare,/query/summarize.usageis omitted from row and query responses for the same reason — zeros would read as real telemetry.Provider Coverage
Checklist
go test ./...) — exit 0, 272 packagesgolangci-lint run --timeout=9m) — 0 issuescloudemu_test.go— driver + handler tests insteadTest Plan
Coverage leak check clean: no OCI operation in
docs/coverage/{aws,azure,gcp}/*.md;git diff development -- services/empty.End-to-end on a running server (port 4613):
A note on parallel worktrees
An earlier run of this branch failed
cmd/cloudemu TestServeOutOfProcess. It is not this change —cmd/cloudemuis untouched by the diff. Six Wave 2 worktrees were running their suites concurrently and that test contends on the shared~/.cloudemudaemon lock. Verified: with nothing else running it passes, and the full suite is exit 0. Worth knowing as shared-state fragility whenever suites run in parallel.