diff --git a/labs/converged-database-lab/README.md b/labs/converged-database-lab/README.md index c112901..9a22cf1 100644 --- a/labs/converged-database-lab/README.md +++ b/labs/converged-database-lab/README.md @@ -93,8 +93,8 @@ All modules run against one deterministically seeded domain. Seeds use an LCG (s | # | Slug | Status | Proofs | Assertions | Links | |---|------|--------|--------|------------|-------| -| 01 | `what-is-a-converged-database` | Live | 4 | 14 | [README](modules/01-what-is-a-converged-database/README.md) · article link lands at publish | -| 02 | `converged-vs-multi-model` | Arrives with its article | — | — | — | +| 01 | `what-is-a-converged-database` | Live | 5 | 20 | [README](modules/01-what-is-a-converged-database/README.md) · [article](https://blogs.oracle.com/developers/what-is-a-converged-database-definition-five-tests-and-ai-use-cases) | +| 02 | `converged-vs-multi-model` | Live | 3 | 18 | [README](modules/02-converged-vs-multi-model/README.md) · article link lands at publish | | 03 | `vector-db-vs-converged` | Arrives with its article | — | — | — | | 04 | `ai-agents-enterprise-data` | Arrives with its article | — | — | — | | 05 | `json-graph-vector-relational-one-db` | Arrives with its article | — | — | — | diff --git a/labs/converged-database-lab/docker/Dockerfile.oracle b/labs/converged-database-lab/docker/Dockerfile.oracle index f0fb77a..b55193a 100644 --- a/labs/converged-database-lab/docker/Dockerfile.oracle +++ b/labs/converged-database-lab/docker/Dockerfile.oracle @@ -13,27 +13,6 @@ RUN printf '[mongodb-org-8.0]\nname=MongoDB Repository\nbaseurl=https://repo.mon && microdnf install -y mongodb-mongosh \ && microdnf clean all -# Bake Oracle's prebuilt augmented all-MiniLM-L12-v2 ONNX model into the image so -# every fresh build (and CI) loads identical in-DB embeddings deterministically — -# no network dependency at init time. This is the official model published by -# Oracle Machine Learning: the 26ai docs "ONNX Pipeline Models: Text Embedding" -# page redirects (/pls/topic/lookup?...&id=oml_ai_models_object_storage) to the -# OML-ai-models object-storage bucket whose index lists this exact URL. The zip -# (~122.6 MB) unpacks to all_MiniLM_L12_v2.onnx (~133.3 MB, 384-dim output), -# loaded by docker/init/08-vector-model.sql via DBMS_VECTOR.LOAD_ONNX_MODEL. -# Placed AFTER the ORDS + mongosh layers so their (slow) cache layers are reused -# when only the model layer changes. Fails the build loudly if the URL stops -# resolving rather than baking a broken image. -ARG MINILM_URL=https://adwc4pm.objectstorage.us-ashburn-1.oci.customer-oci.com/p/TtH6hL2y25EypZ0-rrczRZ1aXp7v1ONbRBfCiT-BDBN8WLKQ3lgyW6RxCfIFLdA6/n/adwc4pm/b/OML-ai-models/o/all_MiniLM_L12_v2_augmented.zip -RUN microdnf install -y unzip \ - && microdnf clean all \ - && mkdir -p /opt/oracle/models \ - && curl -fsSL -o /tmp/minilm.zip "$MINILM_URL" \ - && unzip -o /tmp/minilm.zip -d /opt/oracle/models \ - && test -s /opt/oracle/models/all_MiniLM_L12_v2.onnx \ - && rm -f /tmp/minilm.zip \ - && chown -R oracle:oinstall /opt/oracle/models - COPY init/ /container-entrypoint-initdb.d/ COPY scripts/entrypoint.sh /opt/oracle/scripts/entrypoint.sh diff --git a/labs/converged-database-lab/docker/init/08-vector-model.sql b/labs/converged-database-lab/docker/init/08-vector-model.sql deleted file mode 100644 index 3125b9b..0000000 --- a/labs/converged-database-lab/docker/init/08-vector-model.sql +++ /dev/null @@ -1,79 +0,0 @@ -ALTER SESSION SET CONTAINER = FREEPDB1; - --- In-database embeddings infrastructure (foundational; used by the AI articles). --- Runs once on first boot (gvenzl initdb), after 01-07. Loads Oracle's prebuilt --- augmented all-MiniLM-L12-v2 ONNX model (baked into the image at /opt/oracle/models --- by docker/Dockerfile.oracle), embeds all 300 ticket bodies into a real VECTOR(384) --- column, and builds an IVF vector index on it. Idempotent: every CREATE is guarded --- so a recreated container over a persistent volume re-runs cleanly. --- --- Grants used by the AI modules beyond the base 01-lab-user.sql set: --- CREATE MINING MODEL — DBMS_VECTOR.LOAD_ONNX_MODEL registers an ONNX model --- READ ON ONNX_MODELS — read the baked .onnx from the image directory --- EXECUTE DBMS_VECTOR — VECTOR_EMBEDDING / model load --- EXECUTE DBMS_RLS — VPD policies (permission-aware retrieval proofs) --- CREATE/DROP ANY CONTEXT — application context for VPD tenant/user predicates -GRANT CREATE MINING MODEL TO lab_user; -GRANT CREATE ANY CONTEXT TO lab_user; -GRANT DROP ANY CONTEXT TO lab_user; -GRANT EXECUTE ON DBMS_VECTOR TO lab_user; -GRANT EXECUTE ON DBMS_RLS TO lab_user; - --- Directory over the baked model path; READ for lab_user so the load can open it. -CREATE OR REPLACE DIRECTORY ONNX_MODELS AS '/opt/oracle/models'; -GRANT READ ON DIRECTORY ONNX_MODELS TO lab_user; - -ALTER SESSION SET CURRENT_SCHEMA = lab_user; - --- Load the model into the lab_user schema as MINILM_L12 so VECTOR_EMBEDDING --- (MINILM_L12 USING AS data) resolves for lab_user. The augmented model --- carries its own tokenizer + pooling, so the metadata only needs to declare the --- embedding function and the input attribute name (DATA, matching the AS data --- alias used everywhere downstream). DROP first for idempotent re-runs. -DECLARE - v_model_path VARCHAR2(400); -BEGIN - BEGIN - DBMS_VECTOR.DROP_ONNX_MODEL(model_name => 'MINILM_L12', force => TRUE); - EXCEPTION WHEN OTHERS THEN NULL; /* not yet loaded — nothing to drop */ - END; - DBMS_VECTOR.LOAD_ONNX_MODEL( - directory => 'ONNX_MODELS', - file_name => 'all_MiniLM_L12_v2.onnx', - model_name => 'MINILM_L12', - metadata => JSON('{"function":"embedding","embeddingOutput":"embedding","input":{"input":["DATA"]}}')); -END; -/ - --- Real 384-dim embedding column. The module-01 VECTOR(8) `embedding` column and --- its IVF index (ticket_vec_idx) are LEFT UNTOUCHED. Guarded so a recreated --- container does not error on the already-present column. -DECLARE - v_exists NUMBER; -BEGIN - SELECT COUNT(*) INTO v_exists FROM user_tab_columns - WHERE table_name = 'SUPPORT_TICKETS' AND column_name = 'BODY_VEC'; - IF v_exists = 0 THEN - EXECUTE IMMEDIATE 'ALTER TABLE support_tickets ADD (body_vec VECTOR(384, FLOAT32))'; - END IF; -END; -/ - --- Embed all 300 ticket bodies in-database — no external embedding service, no --- copy pipeline. ~2.5s on the Free container. Re-runnable: recomputes from body. -UPDATE support_tickets SET body_vec = VECTOR_EMBEDDING(MINILM_L12 USING body AS data); - -COMMIT; - --- IVF (NEIGHBOR PARTITIONS) vector index on the real 384-dim column. IVF needs --- NO Vector Pool / VECTOR_MEMORY_SIZE, so it builds within the Free container's --- SGA. Guarded for idempotent re-runs. -DECLARE -BEGIN - EXECUTE IMMEDIATE 'DROP INDEX tickets_bodyvec_ivf'; -EXCEPTION WHEN OTHERS THEN NULL; /* absent — first run */ -END; -/ -CREATE VECTOR INDEX tickets_bodyvec_ivf ON support_tickets(body_vec) - ORGANIZATION NEIGHBOR PARTITIONS - DISTANCE COSINE; diff --git a/labs/converged-database-lab/modules/02-converged-vs-multi-model/README.md b/labs/converged-database-lab/modules/02-converged-vs-multi-model/README.md new file mode 100644 index 0000000..b719139 --- /dev/null +++ b/labs/converged-database-lab/modules/02-converged-vs-multi-model/README.md @@ -0,0 +1,138 @@ +# Module 02 — Converged vs Multi-Model + +Three runnable proofs for leaf article 2's core claims ("multi-model is a +storage claim; converged is a guarantees claim"). Every script executes +against the live lab container and emits machine-checkable assertions +(`ASSERT::PASS|FAIL`). No screenshots, no trust-me — run them yourself. + +--- + +## Proof 1: `scripts/01-survey-challenge.sql` + +**Article claim: the academic literature's canonical multi-model query runs as +one standard SQL statement.** Lu & Holubová's ACM Computing Surveys survey of +multi-model databases (52(3) Art. 55, 2019) opens with a running example +(their Fig. 1): relational customers (Mary, credit 5000; John, 3000; William, +2000), a "knows" social graph, and orders as JSON documents — challenge query: +*"Return all product_no which are ordered by a friend of a customer whose +credit_limit>3000"*, published result `['2724f','3424g']`. The survey solves +it in ArangoDB AQL and OrientDB SQL, each a proprietary language on its own +engine. This script recreates the dataset as module-local tables plus a SQL +property graph, then answers the query in **one statement**: `GRAPH_TABLE` +(SQL/PGQ, ISO/IEC 9075-16) walks the knows-graph, `JSON_TABLE` (SQL/JSON, +SQL:2016) unnests the order lines, a relational predicate filters on +`credit_limit` — and an assertion checks the result set equals the survey's +published answer **exactly**. `EXPLAIN PLAN` then shows graph, document, and +relational access costed as ordinary row sources in a single plan tree. + +**Dataset note:** the survey publishes the customers, the query, and the +result, but not the complete knows-edge topology or full order documents. The +script constructs the minimal dataset consistent with the survey's published +inputs and published result (Mary knows John and William; John's order +contains product `2724f`, William's contains `3424g`); `Order_no`, +`Product_Name`, and `Price` values are constructed placeholders in the +survey's document shape. + +**DDL exception:** this script intentionally performs DDL, and DDL +autocommits — the harness rollback cannot undo it. Cleanup is therefore +explicit and asserted: every module-local object (`smm_` prefix) is dropped +before the script ends, an idempotence guard at the top clears leftovers from +interrupted runs, and the transactional `EXPLAIN PLAN` rows are deleted before +the autocommitting drops so `plan_table` is left unchanged too. + +Expected assertions: + +``` +ASSERT:survey-result-exact:PASS +ASSERT:plan-captured:PASS +ASSERT:plan-spans-graph:PASS +ASSERT:plan-spans-relational:PASS +ASSERT:plan-spans-document:PASS +ASSERT:plan-evaluates-json:PASS +ASSERT:one-plan-tree:PASS +ASSERT:smm-tables-dropped:PASS +ASSERT:smm-graph-dropped:PASS +ASSERT:plan-table-clean:PASS +``` + +## Proof 2: `scripts/02-one-enforcement-domain.js` + +**Article claim: one enforcement domain — the same constraint guards every +API.** The shared domain declares `CHECK (qty > 0)` on `order_items`. The +script pushes `qty: -1` through the MongoDB document API (an `updateOne` on +the `order_dv` duality view) and gets a `MongoServerError` carrying +`ORA-02290` (check-constraint violated); it then submits the same violation as +a SQL `UPDATE` through the `$sql` aggregation stage on the same connection and +gets the same `ORA-02290`. Document and rows verified unchanged after both +attempts. The constraint lives in the engine, beneath every surface — there is +no per-API validation layer to drift. + +**Error-text note:** the exact `MongoServerError` message format (ORA-42692 +wrapping ORA-02290 on the document path; bare ORA-02290 via `$sql`) is +observed behavior on Oracle AI Database 26ai Free as of June 2026, not a +documented contract. Assertions match only the documented element — the +ORA-02290 code. + +Expected assertions: + +``` +ASSERT:doc-read:PASS +ASSERT:mongo-api-rejected:PASS +ASSERT:doc-unchanged:PASS +ASSERT:sql-update-rejected:PASS +ASSERT:final-state-unchanged:PASS +``` + +## Proof 3: `scripts/03-read-after-write-search.sql` + +**Article claim: transactional read-after-write text search.** The script +inserts a support ticket containing a unique marker token, COMMITs, and +immediately finds it with `CONTAINS` — then deletes it, COMMITs, and +immediately does not. Search visibility arrives with the commit, in the same +engine, with no separate search process, change stream, or refresh interval. +The lab's `ticket_text_idx` is created with `PARAMETERS ('SYNC (ON COMMIT)')` +(`docker/init/05-text-vector.sql`), so the text index syncs inside the +committing transaction. + +**Commit exception:** this script intentionally COMMITs (read-after-write can +only be shown across a real commit boundary), so the harness rollback does not +apply; explicit, asserted cleanup restores the domain. It is reseed-safe — +`ticket_id` is identity-generated and the probe row is addressed only by its +unique subject/marker, never a fixed id. + +Expected assertions: + +``` +ASSERT:contains-finds-committed-write:PASS +ASSERT:contains-after-cleanup:PASS +ASSERT:probe-rows-gone:PASS +``` + +## Cross-reference: the one-optimizer proof lives in module 01 + +The companion article also leans on **one cost-based plan spanning graph, +document, vector, and relational** access. That evidence is module 01's +[`05-one-plan.sql`](../01-what-is-a-converged-database/scripts/05-one-plan.sql) +(`EXPLAIN PLAN` over the shared domain: `GRAPH_TABLE` + JSON predicate + +`VECTOR_DISTANCE` + relational joins in one plan tree). Leaf article 2 quotes +that proof; this module deliberately does not duplicate it — proof 1's plan +assertions here cover the survey statement specifically. + +--- + +## Run It Yourself + +```bash +docker compose up -d --build oracle +pip install -r validator/requirements.txt +python validator/run.py +``` + +The validator runs every script (modules 01 and 02) and exits 0 only if all +assertions pass. + +**These proofs leave the seeded domain unchanged.** Two scripts in this module +are documented exceptions to the rollback contract — proof 1 performs DDL +(autocommits) and proof 3 COMMITs — so both clean up explicitly and assert the +cleanup, which keeps the validator repeatable: run it any number of times with +identical results. diff --git a/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/01-survey-challenge.sql b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/01-survey-challenge.sql new file mode 100644 index 0000000..ae53ef0 --- /dev/null +++ b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/01-survey-challenge.sql @@ -0,0 +1,214 @@ +DECLARE /* Module 02 proof 1: the academic literature's canonical multi-model + query, answered in ONE standard SQL statement. + + Lu & Holubova, "Multi-model Databases: A New Journey to Handle the Variety + of Data," ACM Computing Surveys 52(3) Art. 55 (2019), Fig. 1 poses an + e-commerce challenge spanning relational, graph, and document data: + customers Mary (credit limit 5000), John (3000), William (2000); a "knows" + social graph; orders as JSON documents. The Fig. 1 caption query: "Return + all product_no which are ordered by a friend of a customer whose + credit_limit>3000". Published result: ['2724f','3424g']. Their Fig. 2 + solves it in ArangoDB AQL and OrientDB SQL — each a proprietary language on + its own engine. Here it is one standard SQL statement: GRAPH_TABLE + (SQL/PGQ, ISO/IEC 9075-16) + JSON_TABLE (SQL/JSON, SQL:2016) + a relational + predicate, planned by one cost-based optimizer. + + DATASET NOTE: the survey publishes the customers, the query, and the + result, but not the complete knows-edge topology or full order documents. + The rows below are CONSTRUCTED as the minimal dataset consistent with the + survey's published inputs and published result: Mary (the only customer + with credit_limit > 3000) knows John and William; John's order contains + product 2724f, William's contains 3424g. Order_no / Product_Name / Price + values are likewise constructed placeholders in the survey's document + shape. + + DDL WARNING: this script intentionally performs DDL, and DDL AUTOCOMMITS — + the harness rollback cannot undo it. Cleanup is therefore explicit: every + module-local object (smm_ prefix, "survey multi-model") is dropped before + the script ends, and assertions verify the drops. This first block is an + idempotence guard: remove any smm_ leftovers from a previously interrupted + run. */ + PROCEDURE drop_if_exists(p_stmt VARCHAR2) IS + BEGIN + EXECUTE IMMEDIATE p_stmt; + EXCEPTION + WHEN OTHERS THEN NULL; /* object absent — nothing to clean */ + END; +BEGIN + drop_if_exists('DROP PROPERTY GRAPH smm_social'); + drop_if_exists('DROP TABLE smm_orders PURGE'); + drop_if_exists('DROP TABLE smm_knows PURGE'); + drop_if_exists('DROP TABLE smm_customers PURGE'); +END; +/ + +DELETE /* idempotence guard for the EXPLAIN PLAN below: clear any prior rows + for this statement id */ +FROM plan_table WHERE statement_id = 'm02-survey'; + +CREATE TABLE smm_customers ( + customer_id NUMBER PRIMARY KEY, + name VARCHAR2(40) NOT NULL, + credit_limit NUMBER NOT NULL +); + +CREATE TABLE smm_knows ( + knower_id NUMBER NOT NULL REFERENCES smm_customers, + known_id NUMBER NOT NULL REFERENCES smm_customers, + PRIMARY KEY (knower_id, known_id) +); + +CREATE TABLE smm_orders ( + order_id NUMBER PRIMARY KEY, + customer_id NUMBER NOT NULL REFERENCES smm_customers, + doc JSON NOT NULL +); + +CREATE PROPERTY GRAPH smm_social + VERTEX TABLES ( + smm_customers KEY (customer_id) + PROPERTIES (customer_id, name, credit_limit) + ) + EDGE TABLES ( + smm_knows KEY (knower_id, known_id) + SOURCE KEY (knower_id) REFERENCES smm_customers (customer_id) + DESTINATION KEY (known_id) REFERENCES smm_customers (customer_id) + ); + +INSERT INTO smm_customers VALUES (1, 'Mary', 5000); + +INSERT INTO smm_customers VALUES (2, 'John', 3000); + +INSERT INTO smm_customers VALUES (3, 'William', 2000); + +INSERT INTO smm_knows VALUES (1, 2); + +INSERT INTO smm_knows VALUES (1, 3); + +INSERT INTO smm_orders VALUES (1, 2, + JSON('{"Order_no":"34e5e759","Orderlines":[{"Product_no":"2724f","Product_Name":"Toy","Price":66}]}')); + +INSERT INTO smm_orders VALUES (2, 3, + JSON('{"Order_no":"0c6df508","Orderlines":[{"Product_no":"3424g","Product_Name":"Book","Price":40}]}')); + +SELECT DISTINCT jt.product_no /* THE statement — the survey's Fig. 1 query in + one standard SQL statement: graph hop via SQL/PGQ GRAPH_TABLE + (ISO/IEC 9075-16), document unnest via SQL/JSON JSON_TABLE (SQL:2016), + relational predicate on credit_limit — one engine, one optimizer */ +FROM GRAPH_TABLE (smm_social + MATCH (c IS smm_customers) -[IS smm_knows]-> (f IS smm_customers) + WHERE c.credit_limit > 3000 + COLUMNS (f.customer_id AS friend_id)) g +JOIN smm_orders o ON o.customer_id = g.friend_id +CROSS JOIN JSON_TABLE (o.doc, '$.Orderlines[*]' + COLUMNS (product_no VARCHAR2(16) PATH '$.Product_no')) jt; + +SELECT /* the result set equals the survey's published answer EXACTLY: + two distinct products, and both are theirs */ + 'ASSERT:survey-result-exact:' || + CASE WHEN COUNT(*) = 2 + AND COUNT(CASE WHEN product_no = '2724f' THEN 1 END) = 1 + AND COUNT(CASE WHEN product_no = '3424g' THEN 1 END) = 1 + THEN 'PASS' ELSE 'FAIL' END +FROM ( + SELECT DISTINCT jt.product_no + FROM GRAPH_TABLE (smm_social + MATCH (c IS smm_customers) -[IS smm_knows]-> (f IS smm_customers) + WHERE c.credit_limit > 3000 + COLUMNS (f.customer_id AS friend_id)) g + JOIN smm_orders o ON o.customer_id = g.friend_id + CROSS JOIN JSON_TABLE (o.doc, '$.Orderlines[*]' + COLUMNS (product_no VARCHAR2(16) PATH '$.Product_no')) jt +); + +EXPLAIN PLAN SET STATEMENT_ID = 'm02-survey' FOR +SELECT DISTINCT jt.product_no +FROM GRAPH_TABLE (smm_social + MATCH (c IS smm_customers) -[IS smm_knows]-> (f IS smm_customers) + WHERE c.credit_limit > 3000 + COLUMNS (f.customer_id AS friend_id)) g +JOIN smm_orders o ON o.customer_id = g.friend_id +CROSS JOIN JSON_TABLE (o.doc, '$.Orderlines[*]' + COLUMNS (product_no VARCHAR2(16) PATH '$.Product_no')) jt; + +SELECT /* a real costed plan was produced */ 'ASSERT:plan-captured:' || + CASE WHEN COUNT(*) >= 4 THEN 'PASS' ELSE 'FAIL' END +FROM plan_table WHERE statement_id = 'm02-survey'; + +SELECT /* the graph pattern lowers to ordinary row sources over the edge + table — the CBO reads SMM_KNOWS through its PK index, whose name is + system-generated, so resolve it via user_indexes */ + 'ASSERT:plan-spans-graph:' || + CASE WHEN EXISTS (SELECT 1 FROM plan_table p + WHERE p.statement_id = 'm02-survey' + AND (p.object_name = 'SMM_KNOWS' + OR p.object_name IN (SELECT i.index_name + FROM user_indexes i + WHERE i.table_name = 'SMM_KNOWS'))) + THEN 'PASS' ELSE 'FAIL' END +FROM dual; + +SELECT /* the relational vertex table is costed in the same plan */ + 'ASSERT:plan-spans-relational:' || + CASE WHEN EXISTS (SELECT 1 FROM plan_table p + WHERE p.statement_id = 'm02-survey' + AND (p.object_name = 'SMM_CUSTOMERS' + OR p.object_name IN (SELECT i.index_name + FROM user_indexes i + WHERE i.table_name = 'SMM_CUSTOMERS'))) + THEN 'PASS' ELSE 'FAIL' END +FROM dual; + +SELECT /* the JSON order documents are costed in the same plan */ + 'ASSERT:plan-spans-document:' || + CASE WHEN EXISTS (SELECT 1 FROM plan_table p + WHERE p.statement_id = 'm02-survey' + AND (p.object_name = 'SMM_ORDERS' + OR p.object_name IN (SELECT i.index_name + FROM user_indexes i + WHERE i.table_name = 'SMM_ORDERS'))) + THEN 'PASS' ELSE 'FAIL' END +FROM dual; + +SELECT /* the document unnest appears as a JSONTABLE EVALUATION row source + inside the same plan tree */ + 'ASSERT:plan-evaluates-json:' || + CASE WHEN EXISTS (SELECT 1 FROM plan_table p + WHERE p.statement_id = 'm02-survey' + AND p.operation LIKE 'JSONTABLE%') + THEN 'PASS' ELSE 'FAIL' END +FROM dual; + +SELECT /* every row source above belongs to a single plan tree */ + 'ASSERT:one-plan-tree:' || + CASE WHEN COUNT(DISTINCT plan_id) = 1 THEN 'PASS' ELSE 'FAIL' END +FROM plan_table WHERE statement_id = 'm02-survey'; + +DELETE /* EXPLAIN PLAN rows are transactional, but the DROP statements below + autocommit whatever is pending — delete the plan rows first so the implicit + commit leaves plan_table unchanged */ +FROM plan_table WHERE statement_id = 'm02-survey'; + +DROP PROPERTY GRAPH smm_social; + +DROP TABLE smm_orders PURGE; + +DROP TABLE smm_knows PURGE; + +DROP TABLE smm_customers PURGE; + +SELECT /* explicit cleanup verified: no module table survives */ + 'ASSERT:smm-tables-dropped:' || + CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END +FROM user_tables WHERE table_name LIKE 'SMM\_%' ESCAPE '\'; + +SELECT /* explicit cleanup verified: the module property graph is gone */ + 'ASSERT:smm-graph-dropped:' || + CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END +FROM user_property_graphs WHERE graph_name = 'SMM_SOCIAL'; + +SELECT /* the plan rows were deleted before the autocommitting DDL, so + plan_table is left unchanged too */ + 'ASSERT:plan-table-clean:' || + CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END +FROM plan_table WHERE statement_id = 'm02-survey'; diff --git a/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/02-one-enforcement-domain.js b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/02-one-enforcement-domain.js new file mode 100644 index 0000000..1063efe --- /dev/null +++ b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/02-one-enforcement-domain.js @@ -0,0 +1,60 @@ +// Module 02 proof 2: one enforcement domain — the engine's CHECK constraint +// rejects the same illegal write through every API. +// +// The shared domain declares CHECK (qty > 0) on order_items. This script +// attempts qty = -1 through two doors of the same engine: (a) the MongoDB +// document API, updating items.0.qty in the order_dv duality view, and (b) a +// SQL UPDATE submitted through the same MongoDB connection's $sql aggregation +// stage. Both are rejected with the same ORA-02290 check-constraint +// violation, and the data stays unchanged. There is no per-API validation +// layer to drift — the constraint lives in the engine, beneath every surface. +// +// NOTE on error text: the exact MongoServerError message format observed here +// (ORA-42692 wrapping ORA-02290 on the duality-view document path; a bare +// ORA-02290 on the $sql path) is observed behavior on Oracle AI Database 26ai +// Free as of June 2026, not a documented contract. The documented element is +// the ORA-02290 error code itself (docs.oracle.com/error-help/db/ora-02290), +// so assertions match on the code only. The check-constraint NAME is +// system-generated (SYS_C...), so assertions never reference it. + +const col = db.getCollection('order_dv'); +const before = col.findOne({ _id: 1 }); +const origQty = before && before.items && before.items.length > 0 ? before.items[0].qty : null; +print('ASSERT:doc-read:' + (origQty !== null && origQty > 0 ? 'PASS' : 'FAIL')); + +// (a) document API: push an illegal qty into the duality view. +let docErr = null; +try { + col.updateOne({ _id: 1 }, { $set: { 'items.0.qty': -1 } }); +} catch (e) { + docErr = e; +} +const docMsg = docErr ? String(docErr.message || docErr.errmsg || docErr) : ''; +print('ASSERT:mongo-api-rejected:' + (docErr !== null && docMsg.includes('ORA-02290') ? 'PASS' : 'FAIL')); + +// the rejected write changed nothing — the document still carries the +// original qty. +const afterDoc = col.findOne({ _id: 1 }); +print('ASSERT:doc-unchanged:' + (afterDoc.items[0].qty === origQty ? 'PASS' : 'FAIL')); + +// (b) same violation as plain SQL through the same MongoDB connection: the +// $sql stage accepts the UPDATE and the engine rejects it with the same code. +let sqlErr = null; +try { + db.aggregate([{ $sql: 'UPDATE order_items SET qty = -1 WHERE order_id = 1 AND line_no = 1' }]).toArray(); +} catch (e) { + sqlErr = e; +} +const sqlMsg = sqlErr ? String(sqlErr.message || sqlErr.errmsg || sqlErr) : ''; +print('ASSERT:sql-update-rejected:' + (sqlErr !== null && sqlMsg.includes('ORA-02290') ? 'PASS' : 'FAIL')); + +// final state: both the document projection and the relational rows still +// show the original qty — nothing to restore because nothing got through. +const finalDoc = col.findOne({ _id: 1 }); +const finalRows = db.aggregate([ + { $sql: 'SELECT qty AS "qty" FROM order_items WHERE order_id = 1 AND line_no = 1' } +]).toArray(); +print('ASSERT:final-state-unchanged:' + ( + finalDoc.items[0].qty === origQty && + finalRows.length === 1 && + Number(finalRows[0].qty) === origQty ? 'PASS' : 'FAIL')); diff --git a/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/03-read-after-write-search.sql b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/03-read-after-write-search.sql new file mode 100644 index 0000000..f5dd8ba --- /dev/null +++ b/labs/converged-database-lab/modules/02-converged-vs-multi-model/scripts/03-read-after-write-search.sql @@ -0,0 +1,48 @@ +DELETE /* Module 02 proof 3: transactional read-after-write text search. + + A row INSERTed and COMMITted is immediately findable through the Oracle + Text search index (CONTAINS) — no refresh interval, no change-stream lag, + no separate search process. The lab's ticket_text_idx is created with + PARAMETERS ('SYNC (ON COMMIT)') (docker/init/05-text-vector.sql): the index + syncs inside the committing transaction, so search visibility arrives WITH + the commit, not eventually after it. + + COMMIT WARNING: this script intentionally COMMITs — a documented exception + to the rollback contract (see the module README). Read-after-write through + a text index can only be demonstrated across a real commit boundary. + Explicit cleanup restores the domain: the probe ticket is deleted and the + delete is committed. The script is also reseed-safe — ticket_id is + identity-generated and nothing assumes a fixed id; the probe row is + addressed only by its unique subject and marker token. + + This first statement is an idempotence guard: remove any probe rows left by + a previously interrupted run (committed together with the INSERT below). */ +FROM support_tickets WHERE subject = 'm02 rw-search probe'; + +INSERT INTO support_tickets (customer_id, subject, body, status) +VALUES (1, 'm02 rw-search probe', + 'read-after-write search probe, marker token zzqxw9347', 'open'); + +COMMIT; + +SELECT /* the committed row is findable by CONTAINS immediately — same call + stack, same second, no sync window */ + 'ASSERT:contains-finds-committed-write:' || + CASE WHEN COUNT(*) = 1 THEN 'PASS' ELSE 'FAIL' END +FROM support_tickets WHERE CONTAINS(body, 'zzqxw9347') > 0; + +DELETE /* explicit cleanup: remove the probe ticket */ +FROM support_tickets WHERE subject = 'm02 rw-search probe'; + +COMMIT; + +SELECT /* and the committed delete is just as immediately invisible to + search — read-after-write holds in both directions */ + 'ASSERT:contains-after-cleanup:' || + CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END +FROM support_tickets WHERE CONTAINS(body, 'zzqxw9347') > 0; + +SELECT /* domain restored: no probe rows survive in the base table either */ + 'ASSERT:probe-rows-gone:' || + CASE WHEN COUNT(*) = 0 THEN 'PASS' ELSE 'FAIL' END +FROM support_tickets WHERE subject = 'm02 rw-search probe';