Skip to content

Query & Keys ​

Updated: 2026-07-19 · Status: Outline

Outline: heading menetapkan cakupan; isi ditulis bertahap.

1. Translasi Filter Operator ​

Cakupan hari ini lebih sempit dari kontrak. Operator kontrak (../../spec/backend/01-core-basic.md §6 mendaftar eq neq gt gte lt lte between in nin like ilike null notnull) — yang benar-benar diterjemahkan ke SQL hari ini cuma sembilan: eq neq gt gte lt lte like in nin. between, ilike, null, notnull belum diimplementasikan sama sekali.

Filter/sort juga cuma bisa menyasar field yang punya index/unique/ natural_key — tidak ada fallback query ke path JSONB mentah (data->>'field') untuk field yang tidak ter-index seperti sempat didesain; field non-indexed sederhananya tidak bisa difilter/disortir sama sekali hari ini, bukan "bisa tapi lebih lambat". Klaim "hasil query identik terlepas jalur mana yang dipakai" di kontrak karena itu belum bisa diuji sepenuhnya — cuma ada satu jalur (generated column), bukan dua jalur yang perilakunya perlu disamakan.

2. Natural Key Counter ​

ctx.next_key (../../spec/backend/01-core-basic.md §2, ../../spec/backend/04-persist-backend.md §2) diimplementasikan lewat tabel counter:

sql
-- PK = tenant/resource/field/scope/period
formspec_natural_key_counters (tenant_id, resource, field, scope, period, seq)

NaturalKeyCounter.NextSequence meng-UPSERT baris counter secara atomik (satu statement, gap-free dan duplicate-free untuk nilai counter itu sendiri) — bukan lewat scan MAX(). Pada jalur auto-generate saat create (EntityStore.Insert), UPSERT ini sekarang jalan di transaksi yang sama dengan INSERT baris Document (InTx, renderers/jsonb-persist/tx.go) — kalau insert gagal setelah counter terlanjur increment, rollback membatalkan keduanya, menutup gap yang sebelumnya dicatat di sini (lihat 01-architecture.md §3 untuk cakupan penuh mutasi atomik ini, termasuk apa yang masih belum tercakup). Mode gap-free penuh ("angka tidak pernah bolong sama sekali") tetap belum berarti lock ditahan lintas request bersamaan — ini "counter dan insert commit/rollback bersama", bukan serialisasi antar request konkuren, yang cukup untuk kontrak ini (gap-free per unit transaksi, bukan zero-contention). scope_field opsional memetakan nilai field lain (mis. branch_id) jadi komponen scope, sehingga satu sequence independen per nilai itu alih-alih satu sequence tenant-wide. Pemanggilan eksplisit ctx.next_key(field) dari script saat ini selalu pakai scope tenant-wide (jalur itu tidak punya data resource untuk resolve nilai scope_field) — menyamakan perilakunya dengan jalur auto-generate saat create adalah follow-up yang belum dikerjakan.

natural_key_rule.strategy: custom (kontrak §2: sequence | custom) berarti framework tidak auto-generate nilai ini — pemanggil (hook/script/import) wajib mengisinya sendiri; generateNaturalKeys melewati field semacam itu tanpa menyentuh counter, dan validasi required-field jadi pengaman kalau field itu ternyata tidak diisi siapa pun.

3. Idempotency Store ​

(tenant, action, key) → pending | completed | failed + response tersimpan (../../spec/backend/01-core-basic.md §5) — entry tidak pernah dihapus saat commit, kedaluwarsa via CleanupExpired lewat retention (core.idempotency_retention, default 24 jam, IdempotencyStore.WithTTL). resource.App (resource/formspec.go) membuat satu IdempotencyStore per App dan mengeksposnya lewat App.Idempotency() — TTL-nya nyata dipakai, bukan field yang dihitung lalu dibuang. Retention dibaca dari manifest lewat resolveIdempotencyTTL (boot dan ReloadSpec, jadi mengubah nilainya berlaku tanpa restart): key core.idempotency_retention menang, lalu Config.IdempotencyTTL (seam Go), lalu default 24 jam. Nilai non-positif berarti tanpa kedaluwarsa; nilai yang tidak bisa diparse dilaporkan lalu default dipakai alih-alih diam-diam mematikan retention. Format durasi mengikuti retention stream (7d, 24h, 30m).

Jalur HTTP sudah aktif (Fase 2.7, 2026-08-17). Store di-wire ke router (RouterBuilder.SetIdempotencyStore) di New() dan ReloadSpec(), dan dipakai oleh:

  • Prepare dua-langkah (01-core-basic.md §5): action idempotent: true dengan idempotency_key.from: server mengekspos POST /api/v1/{module}/{plural}/create/prepare (create) dan POST /api/v1/{module}/{plural}/{action}/prepare (custom action) di kedua surface (external + /_ui/entity/). Endpoint mengeluarkan key UUID v7; klien melampirkan key itu di header Idempotency-Key saat memanggil action sebenarnya. Action from: header/param tidak punya endpoint prepare (klien menyuplai key sendiri).
  • Enforcement di handler (HandleCreate + HandleCustomAction):
    • key completed → replay response asli (status + body tersimpan);
    • key pending (in-flight) → 409 CONFLICT;
    • key failed → retry diizinkan;
    • key baru / kedaluwarsa → klaim + eksekusi. IdempotencyStore.Lookup membedakan pending vs failed — TryClaim sendiri menggabungkan keduanya sebagai "retryable". Action idempotent tanpa key → 422 VALIDATION_ERROR.

Frontend (renderers/react-shadcn/src/lib/api/client.ts) sudah mengirim header Idempotency-Key via apiPost(..., {idempotencyKey}) — kini server menghormatinya.

4. Summary Multi-Source ​

Kontrak "gabungkan sources by join_key" (../../spec/backend/02-core-extended.md §6) dijawab lewat SQL join biasa antar tabel yang di-generate framework ini — detail konkretnya (bentuk join, strategi refresh) mengikuti bagaimana Summary itu dipopulasikan dari event durable (rebuild via replay event stream, bukan query on-demand terhadap sources-nya).

5. Dialek ctx.db ​

Resource yang memilih ctx.db (../../spec/backend/04-persist-backend.md §5) mendapat SQL mentah sesuai driver aktif (DriverName() — sqlite untuk dev, postgres untuk produksi) lewat internal/db.DB. Dialek SQL antar kedua driver tidak dijamin identik (mis. sintaks upsert, fungsi tanggal) — resource yang memakai ctx.db bertanggung jawab sendiri menulis SQL yang kompatibel driver yang ia targetkan, atau menerima keterkuncian ke satu driver tertentu.

6. Translasi Operator Tree ​

Kontrak operator hierarki untuk field relation ber-tree: true (../../spec/backend/05-field-types.md §4) — descendant_of, ancestor_of, child_of, root — dipenuhi backend ini tanpa recursive CTE, memakai prefix-match terhadap kolom materialized path (_tpath_<field>, lihat 02-schema-strategies.md §4).

6.1 descendant_of ​

Mencari semua turunan rekursif dari node X:

sql
-- X bukan root
SELECT * FROM gl_accounts
WHERE tenant_id = $1
  AND _tpath_parent_id LIKE '<path(X)>.<X>.%'
  AND deleted_at IS NULL;

-- X adalah root (path kosong)
SELECT * FROM gl_accounts
WHERE tenant_id = $1
  AND _tpath_parent_id LIKE '<X>.%'
  AND deleted_at IS NULL;

Karena pola LIKE dimulai dengan prefix tetap (bukan wildcard di awal), B-tree index pada (tenant_id, _tpath_parent_id) digunakan penuh — tidak ada sequential scan dan tidak ada recursive CTE.

6.2 ancestor_of ​

Mencari semua leluhur rekursif dari node X:

  1. Ambil path node X dari data->>'__tpath.parent_id' (atau dari kolom _tpath_parent_id)
  2. Split path dengan separator . → dapatkan daftar ID ancestor
  3. Query: WHERE id IN (<daftar_id>) AND tenant_id = $1

Karena lookup berdasarkan PK (id), ini adalah O(depth) query — depth tree bisnis praktis jarang melebihi 10 level.

6.3 child_of ​

Anak langsung (satu tingkat) dari node X:

sql
SELECT * FROM gl_accounts
WHERE tenant_id = $1
  AND data->>'parent_id' = '<X>'
  AND deleted_at IS NULL;

Ini adalah query field relation biasa — bukan operator path. Indeks pada FK parent_id (jika ada generated column untuk field tersebut) mencukupi.

6.4 root ​

Node akar (tanpa parent):

sql
SELECT * FROM gl_accounts
WHERE tenant_id = $1
  AND data->>'parent_id' IS NULL
  AND deleted_at IS NULL;

Atau ekuivalen: _tpath_parent_id = ''.

6.5 Siklus ​

Integritas tree (§4 kontrak) dicegah sebelum path ditulis: pada create/update/reparent, framework memeriksa apakah kandidat parent adalah turunan dari node yang sedang dimutasi — pemeriksaan pakai path node kandidat parent (query ancestor_of). Siklus yang terdeteksi menghasilkan VALIDATION_ERROR (422) sebelum transaksi commit. Backend ini tidak mengandalkan recursive CTE untuk deteksi siklus — hanya prefix-match string.

Standar terbuka (CC0) dengan implementasi referensi.