{"slug": "sql-only-trace-engineering-edition-testing-db-side-pure-logic", "title": "sql-only-trace Engineering Edition — Testing DB-Side Pure Logic", "summary": "A developer detailed a technique called 'sql-only-trace' for testing database-side pure logic in PostgreSQL, addressing scenarios where application-layer unit tests cannot verify complex stored procedures, FIFO inventory invariants, RLS policy boundaries, and function overload behavior. The approach replaces traditional trace tests with SQL-based tests that run inside the database, using PostgreSQL-specific features like DO blocks, set_config, and RAISE EXCEPTION.", "body_md": "**May 2026** · Series \"Trace Lock — Governance Notes from AI Pair-Programming\" · Post 9 of 9 (series finale)\n\nThis is the final post in the series. It expands the \"sql-only-trace variant\" briefly mentioned in [B2 Offense Engineering Edition](https://./trace-lock-b2-offense-engineering-en.md) at line 485, and fills in the engineering details that [C2 Combined Engineering Edition](https://./trace-lock-c2-combo-engineering-en.md)'s cross-database mapping section left out.\n\nWritten for engineers who already know the 11-piece matrix from C2 and the sql-only-trace variant concept from B2 line 485. If you are a non-technical reader, [C1 Combined Offense + Defense (plain version)](https://./trace-lock-c1-combo-en.md) is the post you want.\n\nMy environment: Vue 3 + Vite + Vitest + Supabase (PostgreSQL) + Node.js scripts. The DO block, `set_config`\n\n, and `RAISE EXCEPTION`\n\nmechanics discussed below are PostgreSQL-specific. Equivalents for MySQL / SQLite / MongoDB are covered in the \"when not to use\" section at the end.\n\nIn the 11-piece matrix, piece 2 (\"Trace test 5-section structure\") assumes Vitest / Jest / Pytest or similar application-layer unit test frameworks. But some business logic lives **purely inside the database**, and application-layer tests cannot reach the core.\n\nFour scenarios where unit tests fall short:\n\n| Scenario | Why frontend / app-layer tests cannot reach |\n|---|---|\n| Complex PL/pgSQL stored procedures | Logic lives inside `DO $$ ... $$` or `CREATE FUNCTION` . App layer only sees \"input → output\" as a black box |\n| FIFO / inventory invariants | Multi-table interactions (`green_beans` + `opened_beans` + `roasting_input_items` ) plus trigger cascades. App layer mocks cannot reproduce them |\n| RLS policy boundaries | RLS conditions use `current_setting('request.jwt.claims')` . You need to simulate JWT to test them |\n| Function overload behavior |\n`CREATE OR REPLACE` vs `DROP FUNCTION` behave differently. When multiple overloads exist, the app layer cannot predict which one gets called |\n\nMy environment hits all four. `fn_roaster_consume_beans_fifo`\n\nis a FIFO consumption RPC (scenarios 1, 2, 3). `fn_issue_order_action_token`\n\nhad an overload incident (scenario 4; see [CLAUDE.md Rule 27](https://../../CLAUDE.md) for the incident pinning case).\n\nTesting these RPCs from the Vue component layer only verifies \"call succeeded vs failed\". It cannot verify \"is FIFO ordering correct\", \"is the inventory invariant maintained\", \"does a bad JWT return permission denied instead of silent return\".\n\nsql-only-trace replaces piece 2's trace test from `.trace.test.js`\n\nwith `.trace.test.sql`\n\n, letting the trace test run inside the database and use the database's own assertion mechanism.\n\nExpand the skeleton from B2 line 485-549 into 6 sections plus an EXCEPTION fallback:\n\n```\n-- scripts/integration-test/T021-fifo-consume-test.sql\n\n-- ════════════════════════════════════════════════════════════\n-- 0. JWT config (let RLS / has_operator_capability pass)\n-- ════════════════════════════════════════════════════════════\nSELECT set_config(\n  'request.jwt.claims',\n  '{\"sub\":\"t021-test-user\",\"system_role\":\"super_admin\",\"tenant_id\":\"coffeeshooters\"}',\n  false\n) AS jwt_set;\n\nDO $T021$\nDECLARE\n  -- ════════════════════════════════════════════════════════════\n  -- 1. Variable declarations (seed IDs, result holders, errors array)\n  -- ════════════════════════════════════════════════════════════\n  v_kind_id UUID := gen_random_uuid();\n  v_gb1_id TEXT := 'T021-GB1-' || substring(gen_random_uuid()::TEXT, 1, 8);\n  v_ro_id  TEXT := 'T021-RO-'  || substring(gen_random_uuid()::TEXT, 1, 8);\n  v_consume_result JSONB;\n  v_gb1_stock_after INTEGER;\n  v_errors TEXT[] := ARRAY[]::TEXT[];\nBEGIN\n  -- ════════════════════════════════════════════════════════════\n  -- 2. SEED (insert fake data with unique prefix to avoid pollution)\n  -- ════════════════════════════════════════════════════════════\n  RAISE NOTICE '=== T021 SEED ===';\n  INSERT INTO public.coffee_bean_kinds (id, tenant_id, name, ...) VALUES (v_kind_id, ...);\n  INSERT INTO public.green_beans (id, kind_id, ...) VALUES (v_gb1_id, v_kind_id, ...);\n  INSERT INTO public.roasting_orders (id, kind_id, ...) VALUES (v_ro_id, v_kind_id, ...);\n\n  -- ════════════════════════════════════════════════════════════\n  -- 3. RUN (call the RPC under test)\n  -- ════════════════════════════════════════════════════════════\n  RAISE NOTICE '=== T021 RUN ===';\n  v_consume_result := public.fn_roaster_consume_beans_fifo(v_ro_id, v_kind_id, 1500);\n  RAISE NOTICE 'consume_result = %', v_consume_result;\n\n  -- ════════════════════════════════════════════════════════════\n  -- 4. ASSERTIONS (append failures to v_errors, do not abort)\n  -- ════════════════════════════════════════════════════════════\n  SELECT stock_grams INTO v_gb1_stock_after FROM public.green_beans WHERE id = v_gb1_id;\n\n  IF COALESCE(v_gb1_stock_after, 0) <> 0 THEN\n    v_errors := array_append(v_errors,\n      format('A1 GB1.stock_grams expected 0, got %s', v_gb1_stock_after));\n  END IF;\n  -- ... other assertions\n\n  -- ════════════════════════════════════════════════════════════\n  -- 5. CLEANUP (explicit DELETE, no transaction rollback available)\n  -- ════════════════════════════════════════════════════════════\n  DELETE FROM public.roasting_input_items WHERE roasting_order_id = v_ro_id;\n  DELETE FROM public.roasting_orders WHERE id = v_ro_id;\n  DELETE FROM public.green_beans WHERE id = v_gb1_id;\n  DELETE FROM public.coffee_bean_kinds WHERE id = v_kind_id;\n\n  -- ════════════════════════════════════════════════════════════\n  -- 6. Conclusion (green or red)\n  -- ════════════════════════════════════════════════════════════\n  IF array_length(v_errors, 1) IS NULL THEN\n    RAISE NOTICE '✅ T-021 ALL ASSERTIONS PASSED';\n  ELSE\n    RAISE EXCEPTION E'❌ T-021 FAILED — %s errors:\\n%',\n      array_length(v_errors, 1),\n      array_to_string(v_errors, E'\\n  - ');\n  END IF;\nEXCEPTION\n  WHEN OTHERS THEN\n    -- 7. EXCEPTION fallback (best-effort cleanup + re-raise)\n    BEGIN\n      DELETE FROM public.roasting_input_items WHERE roasting_order_id = v_ro_id;\n      DELETE FROM public.roasting_orders WHERE id = v_ro_id;\n      DELETE FROM public.green_beans WHERE id = v_gb1_id;\n      DELETE FROM public.coffee_bean_kinds WHERE id = v_kind_id;\n    EXCEPTION WHEN OTHERS THEN NULL;\n    END;\n    RAISE;\nEND\n$T021$;\n\n-- Final SELECT lets the PASS message show in CLI output\nSELECT 'T-021 PASSED' AS status, 16 AS assertions, NOW() AS executed_at;\n```\n\nThe seven sections map to a typical unit test as follows:\n\n| SQL section | Vitest equivalent | Notes |\n|---|---|---|\n| 0 JWT config | `beforeEach(setupAuth)` |\nRLS simulation |\n| 1 Variable declarations | `let v = ...` |\nseed IDs + result holders |\n| 2 SEED | `beforeAll(seed)` |\nNo transaction; need prefix to avoid pollution |\n| 3 RUN | `result = await fn()` |\nCall the RPC |\n| 4 ASSERTIONS | `expect().toBe()` |\nAccumulating, non-aborting |\n| 5 CLEANUP | `afterAll(cleanup)` |\nMust use explicit DELETE |\n| 6 Conclusion | test runner decides | RAISE EXCEPTION = red |\n| 7 EXCEPTION fallback | no equivalent | Unit tests get auto-rollback from transactions; SQL does not |\n\nThe two most critical sections are **0 JWT config** and **7 EXCEPTION fallback**. Without them sql-only-trace cannot run (blocked by RLS) or leaves seed pollution when it crashes (no cleanup).\n\n`npx supabase db query --linked -f`\n\nbehaves differently from a normal unit test runner in four ways. Each one bit me.\n\nSupabase CLI treats each statement as an auto-committing independent transaction. You cannot wrap the entire DO block in `BEGIN; ... ROLLBACK;`\n\n.\n\nImplication: All SEED data stays in the database. CLEANUP must use explicit DELETE; you cannot rely on rollback.\n\nMitigation: Section 5 and section 7 both need DELETE. Best-effort cleanup inside EXCEPTION needs its own copy. The original error is already in flight, so even if cleanup itself fails, swallow that failure.\n\nBy default, the CLI only prints the rows from the last SELECT. RAISE NOTICE is hidden. SELECT statements in the middle of the DO block are hidden.\n\nImplication: To show PASS / FAIL, put the conclusion in the last SELECT. On failure, RAISE EXCEPTION carries the error string out (the CLI shows the EXCEPTION message).\n\nMitigation:\n\n```\n-- Always end with this\nSELECT 'T-021 PASSED' AS status, 16 AS assertions, NOW() AS executed_at;\n```\n\nOn failure, RAISE EXCEPTION aborts before this SELECT runs, so it only shows on PASS. On FAIL, the CLI prints the EXCEPTION message (including the v_errors array string).\n\nThe CLI defaults `client_min_messages`\n\nto `WARNING`\n\n, so `NOTICE`\n\n-level RAISE NOTICE gets swallowed.\n\nWhen debugging, add `SET client_min_messages = NOTICE;`\n\nat the top of the file to expose RAISE NOTICE for inspecting SEED IDs or RUN intermediate state. Do not leave it on for daily test runs.\n\nThe CLI runs a file sequentially top to bottom with no retry. A network blip or DB connection drop is an immediate fail.\n\nMitigation: Write sql-only-trace tests as idempotent (use SEED prefix to ensure multiple runs do not conflict). For CI, run them in both the pre-push hook and the main branch CI (two layers of defense).\n\n```\nnpx supabase db query --linked -f scripts/integration-test/T021-fifo-consume-test.sql\n```\n\nOn PASS the CLI prints:\n\n```\n status        | assertions | executed_at\n---------------+------------+------------------------------\n T-021 PASSED  |         16 | 2026-05-26 14:30:00+00\n```\n\nOn FAIL the CLI prints (RAISE EXCEPTION message):\n\n```\nERROR:  ❌ T-021 FAILED — 2 errors:\n  - A1 GB1.stock_grams expected 0, got 600\n  - A7 inventory invariant violated: deducted 1800 but RPC asked for 1500\n```\n\nExit code is non-zero, so the pre-push hook blocks the push.\n\nPiece 1 (Registry markdown) and pieces 3-4 (Governance rules) originally assume trace tests are `.trace.test.js`\n\nrunning on Vitest. Adding sql-only-trace requires the registry parser to recognize the new category.\n\n```\n### T-021: FIFO inventory invariant for roasting consumption\n\n- **Type**: sql-only-trace\n- **Anchor SSOT**: `supabase/migrations/N_fn_roaster_consume_beans_fifo.sql`\n- **Trace test (SQL)**: `scripts/integration-test/T021-fifo-consume-test.sql`\n- **Business contract**: FIFO consumption ordered by created_at ASC within the same kind. opened_beans remainder takes priority. weighted_avg_cost = SUM(grams × cost) / total_grams\n- **Last edited**: 2026-05-25\n```\n\nCompared to a frontend trace entry, the differences are `Type: sql-only-trace`\n\nand `Trace test (SQL)`\n\ninstead of `Trace test: src/__tests__/traces/T-NN.trace.test.js`\n\n.\n\n`scripts/parse-trace-registry.mjs`\n\nadds ~10 lines:\n\n``` js\nfunction parseTraceEntry(block) {\n  const type = block.match(/\\*\\*Type\\*\\*:\\s*(\\S+)/)?.[1];\n  const isSqlOnly = type === 'sql-only-trace';\n\n  const testPath = isSqlOnly\n    ? block.match(/\\*\\*Trace test \\(SQL\\)\\*\\*:\\s*`([^`]+)`/)?.[1]\n    : block.match(/\\*\\*Trace test\\*\\*:\\s*`([^`]+)`/)?.[1];\n\n  return {\n    id: block.match(/### (T-\\d+)/)?.[1],\n    type,\n    isSqlOnly,\n    anchorPath: block.match(/\\*\\*Anchor SSOT\\*\\*:\\s*`([^`]+)`/)?.[1],\n    testPath,\n    lastEdited: block.match(/\\*\\*Last edited\\*\\*:\\s*(\\S+)/)?.[1],\n  };\n}\n```\n\n`checkTraceTestImportsAnchor()`\n\noriginally enforces that \"trace test must `import`\n\nthe anchor SSOT it declares\" (catches the rot where a trace test was written but no longer actually references the anchor). SQL has no `import`\n\nconcept, so it needs a carveout:\n\n``` js\nfunction checkTraceTestImportsAnchor(traces) {\n  const violations = [];\n  for (const trace of traces) {\n    // sql-only-trace skips import check (SQL has no import concept)\n    if (trace.isSqlOnly) continue;\n\n    const testContent = fs.readFileSync(trace.testPath, 'utf8');\n    const anchorBasename = path.basename(trace.anchorPath, path.extname(trace.anchorPath));\n\n    if (!testContent.includes(anchorBasename)) {\n      violations.push({\n        traceId: trace.id,\n        message: `trace test ${trace.testPath} does not import anchor ${trace.anchorPath}`,\n      });\n    }\n  }\n  return violations;\n}\n```\n\nThe \"every trace in the registry must have a corresponding test file that exists\" rule applies regardless of frontend or SQL:\n\n``` js\nfunction checkTraceRegistryTestCoverage(traces) {\n  const violations = [];\n  for (const trace of traces) {\n    if (!fs.existsSync(trace.testPath)) {\n      violations.push({\n        traceId: trace.id,\n        message: `${trace.testPath} does not exist (${trace.isSqlOnly ? 'SQL' : 'JS'} test)`,\n      });\n    }\n  }\n  return violations;\n}\n```\n\nThe total parser plus governance rule extension is about 15-20 lines, done in the same sprint.\n\nT-021 is the trace test for Gap-3 (\"FIFO inventory invariant for roasting consumption\") in the sprint's 4 BLOCKERs. The actual file lives at `scripts/integration-test/T021-fifo-consume-test.sql`\n\n, 282 lines, 16 assertions.\n\nFrozen 2026-05-25 (the day the sprint ended):\n\n`green_beans.created_at`\n\nASC order (FIFO)`opened_beans`\n\nremainder first, then break a fresh pack from `stock_grams`\n\ninto `opened_beans`\n\n`opened_beans`\n\nfor the next FIFO round`weighted_avg_cost_per_g = SUM(grams × cost_per_g) / total_grams`\n\n`roasting_orders.is_fifo_resolved = true`\n\nwith `green_bean_id/name`\n\ncarrying the primary sourceSix business contract clauses, each backed by 2-3 assertions.\n\n```\nGB1 (earliest, spec=600g): stock=1200g, cost=400/kg, created_at=-3 days\nGB2 (next,     spec=500g): stock=500g,  cost=500/kg, created_at=-2 days\nGB3 (latest,   spec=600g): stock=600g,  cost=600/kg, created_at=-1 day\n\nRoasting order consumes 1500g\n```\n\nExpected FIFO behavior:\n\n`opened_beans`\n\nfor the next FIFO round| # | Check | Expected |\n|---|---|---|\n| A1 | GB1.stock_grams | 0 |\n| A2 | GB1.opened.remaining_grams | 0 |\n| A3 | GB2.stock_grams | 0 |\n| A4 | GB2.opened.remaining_grams | 200 |\n| A5 | GB3.stock_grams (untouched) | 600 |\n| A6 | GB3.opened.remaining_grams (untouched) | 0 |\n| A7 | Inventory invariant | 1500g consumed |\n| A8 | roasting_input_items row count | 2 |\n| A9 | roasting_input_items total grams | 1500 |\n| A10 | seq=1 is GB1 | GB1 id |\n| A11 | seq=1 grams | 1200 |\n| A12 | seq=2 is GB2 | GB2 id |\n| A13 | seq=2 grams | 300 |\n| A14 | roasting_orders.is_fifo_resolved | true |\n| A15 | roasting_orders.input_grams | 1500 |\n| A16 | weighted_avg_cost_per_g | 0.42 |\n\n`weighted_avg_cost_per_g`\n\nformula: `(1200×0.4 + 300×0.5) / 1500 = 0.42`\n\n. This one breaks most easily if someone changes the algorithm (for example, switching to simple average).\n\n```\nv_gb1_id TEXT := 'T021-GB1-' || substring(gen_random_uuid()::TEXT, 1, 8);\n```\n\nEach run has a different prefix, so multiple runs do not collide. Also useful when cleanup fails: you can manually grep `WHERE id LIKE 'T021-%'`\n\nto wipe leftovers.\n\nThe first version of T-021 had no EXCEPTION block. When the RPC threw, seed data stayed in the database and polluted subsequent test runs. With EXCEPTION, even if the RPC throws, best-effort cleanup still runs:\n\n```\nEXCEPTION\n  WHEN OTHERS THEN\n    BEGIN\n      DELETE FROM public.roasting_input_items WHERE roasting_order_id = v_ro_id;\n      DELETE FROM public.roasting_orders WHERE id = v_ro_id;\n      DELETE FROM public.opened_beans WHERE green_bean_id IN (v_gb1_id, v_gb2_id, v_gb3_id);\n      DELETE FROM public.green_beans WHERE id IN (v_gb1_id, v_gb2_id, v_gb3_id);\n      DELETE FROM public.coffee_bean_kinds WHERE id = v_kind_id;\n    EXCEPTION WHEN OTHERS THEN NULL;\n    END;\n    RAISE;\n```\n\nThe inner `EXCEPTION WHEN OTHERS THEN NULL`\n\nswallows cleanup failures (such as FK conflicts). This ensures the outer `RAISE`\n\npropagates the original error. Without this layer, cleanup failure would mask the original error and make debugging painful.\n\nPiece 9 of the defense matrix (\"reverse verification anchor-break flow\") also applies to sql-only-trace, but the break point switches from helper logic to RPC logic.\n\n`fn_roaster_consume_beans_fifo`\n\n's ORDER BY from `created_at ASC`\n\nto `created_at DESC`\n\nGoing through all 5 steps is what makes \"lock has actual catching power\" true. If step 3 does not turn red, your trace test missed a core contract clause. Add the missing assertion.\n\nT-021's break point is ORDER BY ASC vs DESC because that is the **core contract** of FIFO. Changing something else (such as the `gen_random_uuid()`\n\nprefix) would not break the trace (that is not part of FIFO behavior).\n\nPrinciple for choosing break points: choose a \"core business contract logic\" point, not an \"implementation detail\" point. FIFO's core is ORDER BY ordering + opened remainder priority + pack-breaking rule + weighted average formula. Any one of those four broken should turn the trace test red.\n\nThree months from now, an AI pairing on `fn_roaster_consume_beans_fifo`\n\naccidentally drops the ORDER BY (perhaps trying to optimize the query). The trace test goes red in CI and blocks the push.\n\nWithout sql-only-trace, this bug only surfaces when a customer reports \"why did you use the new batch first and leave the old batch to expire\". By then, weeks of bad data may already exist.\n\nFive situations where porting sql-only-trace is **not necessarily worth it**.\n\nIf your business logic lives entirely in Vue components / Pinia stores / composables / pure helpers, and the database is just a CRUD store, sql-only-trace has no anchor to attach to.\n\nMy environment has both, so I use both: frontend traces for frontend logic (T-001 / T-002 / T-005), sql-only-trace for DB-pure logic (T-021 / T-022).\n\nWhen business spans PostgreSQL + Redis + Elasticsearch, sql-only-trace only covers the PostgreSQL portion. Redis has no PL/pgSQL, so you would write redis-cli scripts + Lua + redis-py yourself. Elasticsearch uses _search API + curl + jq.\n\nThis is doable, but **cross-store consistency tests** belong in the application layer (write integration tests from Node / Python that hit all three). sql-only-trace is not enough on its own.\n\n`npx supabase db query --linked -f`\n\nis the standard runner for sql-only-trace. Other stacks have equivalents:\n\n| DB | CLI |\n|---|---|\n| PostgreSQL (native) | `psql -f file.sql` |\n| Supabase | `npx supabase db query --linked -f file.sql` |\n| MySQL | `mysql -u user -p db < file.sql` |\n| MongoDB | `mongosh < file.js` |\n\nIf your stack lacks such a CLI, or your CI cannot run it, sql-only-trace has nowhere to attach. The pre-push hook also cannot wire it up.\n\n`set_config('request.jwt.claims', ...)`\n\nrequires PostgreSQL 9.5+ for the `set_config`\n\nfunction syntax. Older versions need `SET LOCAL \"request.jwt.claims\" = '...'`\n\n. `gen_random_uuid()`\n\nis built-in only in PostgreSQL 13+ (older versions need the `pgcrypto`\n\nextension enabled).\n\nDeploying across multiple Postgres versions means maintaining sql-only-trace per version, which is more work than maintaining frontend traces.\n\nsql-only-trace uses `set_config`\n\nto inject a super_admin JWT and pass RLS. If the goal is to test the RLS policies themselves (such as \"anon should not see X\" / \"customer should only see their own orders\"), you need multiple JWT scenarios, each in its own run, and sql-only-trace becomes 2-3x the work.\n\nMitigation: Split RLS tests into separate files (T-023-rls-customer-only.sql / T-024-rls-anon-readonly.sql), one JWT scenario per file. The engineering effort is 2-3x larger than for a typical sql-only-trace.\n\nIf you are not on PostgreSQL, sql-only-trace downgrades to \"application-layer integration test\":\n\n| Source | Replacement |\n|---|---|\nPG `DO $$ ... $$` block |\nMySQL `CREATE PROCEDURE ... CALL` , two steps |\nPG `set_config('request.jwt.claims', ...)`\n|\nMySQL has no equivalent. Skip RLS; test at app layer |\nPG `RAISE EXCEPTION`\n|\nMySQL `SIGNAL SQLSTATE '45000'`\n|\nPG `RAISE NOTICE`\n|\nMySQL `SELECT 'msg' AS log`\n|\nPG `gen_random_uuid()`\n|\nMongoDB `ObjectId()` or app-layer `uuid()`\n|\nPG `npx supabase db query --linked -f`\n|\nNode + `pg-promise` integration test |\n\nThe skeleton (SEED / RUN / ASSERTIONS / CLEANUP / Conclusion) applies across all DBs. Only the location moves from SQL to the application layer.\n\nThis is post 9 of 9 in \"Trace Lock — Governance Notes from AI Pair-Programming\" (series finale). The 9 posts span 3 series:\n\nDefense (A1 / A2 / parts of D), offense (B1 / B2), and combined (C1 / C2) form three interwoven axes. D fills in the DB-pure logic testing gap, completing the engineering details of the whole dual-blade approach.\n\nFrom the opening E meta post on \"how I discovered methodology through conversations with AI\" to this closing post on sql-only-trace details, the series records:\n\nI gave these working names (\"dual blade\" / \"Trace Lock\" / \"sql-only-trace\") myself. None are industry-standard terminology. If your context happens to match the conditions (solo-maintained / many cross-layer dependencies / relatively stable business contracts / 6+ months maintenance period / AI pair-programming / pure PostgreSQL), parts of this may be useful.\n\nThis post is an organized record of conversations I had with Claude (an AI pair-programming tool)\n\nduring May 2026. I noticed some patterns worth keeping for my own future reference,\n\nso I asked Claude to help structure them into writing.\n\nA few things I'm **not** claiming:\n\nIf a professional engineer spots misuse, or there's already a more standard name for any\n\nof these concepts, **I genuinely welcome corrections**.\n\n*本文原載於我的部落格： sql-only-trace Engineering Edition — Testing DB-Side Pure Logic*", "url": "https://wpnews.pro/news/sql-only-trace-engineering-edition-testing-db-side-pure-logic", "canonical_source": "https://dev.to/dexterlung/sql-only-trace-engineering-edition-testing-db-side-pure-logic-f9j", "published_at": "2026-07-30 13:05:14+00:00", "updated_at": "2026-07-30 13:32:54.767546+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["PostgreSQL", "Supabase", "Vue 3", "Vite", "Vitest", "Node.js"], "alternates": {"html": "https://wpnews.pro/news/sql-only-trace-engineering-edition-testing-db-side-pure-logic", "markdown": "https://wpnews.pro/news/sql-only-trace-engineering-edition-testing-db-side-pure-logic.md", "text": "https://wpnews.pro/news/sql-only-trace-engineering-edition-testing-db-side-pure-logic.txt", "jsonld": "https://wpnews.pro/news/sql-only-trace-engineering-edition-testing-db-side-pure-logic.jsonld"}}