Skip to main content

reddb_engine/query/
records.rs

1//! Canonical entity-to-query-record materialization. Callers apply visibility,
2//! authorization, tenant and secret policy before/after these representation helpers.
3use super::unified::*;
4use crate::storage::entity::{EntityData, EntityKind, RefType, UnifiedEntity};
5use reddb_types::Value;
6use std::collections::BTreeSet;
7
8mod document_body {
9    pub use reddb_types::document_body_codec::decode_container_to_json;
10    pub fn is_binary_container(bytes: &[u8]) -> bool {
11        bytes.starts_with(reddb_types::document_body_codec::MAGIC)
12    }
13    pub fn read_body_field(bytes: &[u8], name: &str) -> Option<reddb_types::Value> {
14        reddb_types::document_body_codec::read_field_by_name(bytes, name)
15            .ok()
16            .flatten()
17    }
18    pub fn body_fields(bytes: &[u8]) -> Option<Vec<(String, reddb_types::Value)>> {
19        reddb_types::document_body_codec::decode(bytes).ok()
20    }
21}
22
23/// Marker prepended to JSON-encoded tag values written through the
24/// SQL `INSERT INTO <ts> (tags, ...)` path. The character is `\x01`
25/// (Start-of-Heading) — invalid as a leading byte in well-formed
26/// JSON, never used by Prometheus label values, and small enough to
27/// keep storage overhead at one byte per tag. See
28/// `impl_dml::json_tag_value_to_string` and `decode_stored_tag_value`
29/// below for the read/write pair.
30pub const TIMESERIES_TAG_JSON_PREFIX: char = '\u{1}';
31
32/// Decode a stored tag value back into a JSON value (issue #543).
33///
34/// Values written through the SQL INSERT path carry a
35/// [`TIMESERIES_TAG_JSON_PREFIX`] marker and a compact JSON suffix;
36/// we strip the marker and parse the suffix so numbers, booleans,
37/// nulls, arrays, and nested objects come back as their native JSON
38/// kinds rather than stringified placeholders. Values without the
39/// marker — Prometheus remote-write labels, internal `__tenant_id` /
40/// `__namespace` / `__reddb_kind` tags, and any pre-#543 on-disk
41/// rows — are passed through as `JsonValue::String(raw)` so the
42/// Prometheus and metrics surfaces keep their plain-string contract.
43pub fn decode_stored_tag_value(stored: &str) -> crate::json::Value {
44    if let Some(suffix) = stored.strip_prefix(TIMESERIES_TAG_JSON_PREFIX) {
45        crate::json::from_str(suffix)
46            .unwrap_or_else(|_| crate::json::Value::String(suffix.to_string()))
47    } else {
48        crate::json::Value::String(stored.to_string())
49    }
50}
51
52/// Per-thread cache of composite schemas `[<user columns…>, rid, collection,
53/// kind, tenant, created_at, updated_at]`, keyed on the `Arc<Vec<String>>`
54/// identity of the underlying row schema. A 4.5k-row scan hits this
55/// cache on every row after the first, so the Arc<Vec<Arc<str>>>
56/// result is refcount-cloned instead of rebuilt.
57fn sys_schema_with_row_columns(
58    schema: &std::sync::Arc<Vec<String>>,
59) -> std::sync::Arc<Vec<std::sync::Arc<str>>> {
60    use std::cell::RefCell;
61    thread_local! {
62        static CACHE: RefCell<Option<(usize, std::sync::Arc<Vec<std::sync::Arc<str>>>)>> =
63            const { RefCell::new(None) };
64    }
65    let key = std::sync::Arc::as_ptr(schema) as usize;
66    CACHE.with(|c| {
67        let mut slot = c.borrow_mut();
68        if let Some((k, v)) = slot.as_ref() {
69            if *k == key {
70                return std::sync::Arc::clone(v);
71            }
72        }
73        let mut out: Vec<std::sync::Arc<str>> = Vec::with_capacity(6 + schema.len());
74        for name in schema.iter() {
75            out.push(std::sync::Arc::from(name.as_str()));
76        }
77        out.push(sys_key_rid());
78        out.push(sys_key_collection());
79        out.push(sys_key_kind());
80        out.push(sys_key_tenant());
81        out.push(sys_key_created_at());
82        out.push(sys_key_updated_at());
83        let arc = std::sync::Arc::new(out);
84        *slot = Some((key, std::sync::Arc::clone(&arc)));
85        arc
86    })
87}
88
89fn runtime_row_tenant_value(row: &crate::storage::entity::RowData) -> Value {
90    for key in ["tenant_id", "tenant"] {
91        if let Some(value) = row.get_field(key) {
92            return value.clone();
93        }
94    }
95    Value::Null
96}
97
98fn set_public_row_envelope(
99    record: &mut UnifiedRecord,
100    entity: &UnifiedEntity,
101    row: &crate::storage::entity::RowData,
102) {
103    record.set_arc(
104        sys_key_rid(),
105        Value::UnsignedInteger(entity.logical_id().raw()),
106    );
107    record.set_arc(
108        sys_key_collection(),
109        Value::text(entity.kind.collection().to_string()),
110    );
111    record.set_arc(
112        sys_key_kind(),
113        Value::text(public_row_kind(row).to_string()),
114    );
115    record.set_arc(sys_key_tenant(), runtime_row_tenant_value(row));
116    // Managed `created_at` / `updated_at` row columns (WITH timestamps =
117    // true) take precedence over the second-precision envelope values.
118    record.set_arc_if_absent(
119        sys_key_created_at(),
120        Value::UnsignedInteger(entity.created_at),
121    );
122    record.set_arc_if_absent(
123        sys_key_updated_at(),
124        Value::UnsignedInteger(entity.updated_at),
125    );
126}
127
128fn set_public_graph_envelope(record: &mut UnifiedRecord, entity: &UnifiedEntity, kind: &str) {
129    record.set_arc(
130        sys_key_rid(),
131        Value::UnsignedInteger(entity.logical_id().raw()),
132    );
133    record.set_arc(
134        sys_key_collection(),
135        Value::text(entity.kind.collection().to_string()),
136    );
137    record.set_arc(sys_key_kind(), Value::text(kind.to_string()));
138    record.set_arc(sys_key_tenant(), Value::Null);
139    record.set_arc(
140        sys_key_created_at(),
141        Value::UnsignedInteger(entity.created_at),
142    );
143    record.set_arc(
144        sys_key_updated_at(),
145        Value::UnsignedInteger(entity.updated_at),
146    );
147}
148
149fn graph_endpoint_rid_value(endpoint: &str) -> Value {
150    endpoint
151        .parse::<u64>()
152        .map(Value::UnsignedInteger)
153        .unwrap_or_else(|_| Value::text(endpoint.to_string()))
154}
155
156fn public_row_kind(row: &crate::storage::entity::RowData) -> &'static str {
157    if runtime_row_is_kv(row) {
158        "kv"
159    } else if runtime_row_has_document_capability(row) {
160        "document"
161    } else {
162        "row"
163    }
164}
165
166/// Lean materialization for the index scan hot path.
167///
168/// Emits `rid`, `created_at`, `updated_at`, plus the raw user data
169/// columns. Skips the heavier red_* metadata fields (collection, kind, type,
170/// capabilities, sequence_id, row_id). Each skipped field is one fewer string
171/// clone and one fewer HashMap insert per entity.
172///
173/// Used when the caller already knows the collection name and doesn't need
174/// the full metadata in the result (e.g. SELECT * range/filtered scans).
175/// Borrow-based variant of `runtime_table_record_lean` — used by scan
176/// hot paths (via `SegmentManager::for_each_id`) that can't afford the
177/// full `UnifiedEntity::clone`. Copies only the field values that land
178/// in the output record.
179#[inline]
180pub fn runtime_table_record_lean_ref(entity: &UnifiedEntity) -> Option<UnifiedRecord> {
181    let created_at = entity.created_at;
182    let updated_at = entity.updated_at;
183    let row = match &entity.data {
184        EntityData::Row(row) => row,
185        // Issue #414: graph/vector/queue/etc. entities must surface in
186        // SELECT scans. Lean path can't construct a "pure row" for them,
187        // so delegate to the universal materializer.
188        _ => return runtime_any_record_from_entity_ref(entity),
189    };
190    if let Some(named) = &row.named {
191        let mut record = UnifiedRecord::with_capacity(6 + named.len());
192        for (key, value) in named.iter() {
193            record.set(key, value.clone());
194        }
195        set_public_row_envelope(&mut record, entity, row);
196        Some(record)
197    } else if let Some(schema) = &row.schema {
198        // Columnar fast-path: build the record with the schema-shared
199        // Vec<Arc<str>> side-channel so 4.5k rows × N fields deallocate
200        // as one contiguous Vec each instead of a HashMap per record.
201        let sys_schema = sys_schema_with_row_columns(schema);
202        let mut values: Vec<Value> = Vec::with_capacity(sys_schema.len());
203        values.extend(row.columns.iter().cloned());
204        values.push(Value::UnsignedInteger(entity.logical_id().raw()));
205        values.push(Value::text(entity.kind.collection().to_string()));
206        values.push(Value::text(public_row_kind(row).to_string()));
207        values.push(runtime_row_tenant_value(row));
208        values.push(Value::UnsignedInteger(created_at));
209        values.push(Value::UnsignedInteger(updated_at));
210        Some(UnifiedRecord::from_columnar(sys_schema, values))
211    } else {
212        let mut record = UnifiedRecord::with_capacity(6 + row.columns.len());
213        for (i, value) in row.columns.iter().enumerate() {
214            record.set(&format!("c{i}"), value.clone());
215        }
216        set_public_row_envelope(&mut record, entity, row);
217        Some(record)
218    }
219}
220
221#[inline]
222pub fn runtime_table_record_lean(entity: UnifiedEntity) -> Option<UnifiedRecord> {
223    // Issue #414: surface graph/vector/queue/etc. entities in SELECT scans.
224    if !matches!(entity.data, EntityData::Row(_)) {
225        return runtime_any_record_from_entity(entity);
226    }
227    let created_at = entity.created_at;
228    let updated_at = entity.updated_at;
229    let logical_id = entity.logical_id().raw();
230    let row = match entity.data {
231        EntityData::Row(row) => row,
232        _ => unreachable!(),
233    };
234    let collection = entity.kind.collection().to_string();
235    let tenant = runtime_row_tenant_value(&row);
236    let kind = public_row_kind(&row).to_string();
237    // Capture the document body before `named` is consumed below.
238    let doc_body = document_row_body_bytes(&row);
239    if let Some(named) = row.named {
240        let mut record = UnifiedRecord::with_capacity(6 + named.len());
241        // `set_owned` consumes the already-heap-allocated String key
242        // instead of `&str → String` clone. For SELECT * on a wide
243        // result set this saves ~15k allocations per query.
244        for (key, value) in named {
245            record.set_owned(key, value);
246        }
247        record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
248        record.set_arc(sys_key_collection(), Value::text(collection));
249        record.set_arc(sys_key_kind(), Value::text(kind));
250        record.set_arc(sys_key_tenant(), tenant);
251        record.set_arc_if_absent(sys_key_created_at(), Value::UnsignedInteger(created_at));
252        record.set_arc_if_absent(sys_key_updated_at(), Value::UnsignedInteger(updated_at));
253        // Lean SELECT * over a single-source document — expand body fields.
254        fill_document_body_fields(&mut record, None, doc_body.as_deref());
255        Some(record)
256    } else if let Some(ref schema) = row.schema {
257        let mut record = UnifiedRecord::with_capacity(6 + schema.len());
258        for (name, value) in schema.iter().zip(row.columns) {
259            record.set(name, value);
260        }
261        record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
262        record.set_arc(sys_key_collection(), Value::text(collection));
263        record.set_arc(sys_key_kind(), Value::text(kind));
264        record.set_arc(sys_key_tenant(), tenant);
265        record.set_arc_if_absent(sys_key_created_at(), Value::UnsignedInteger(created_at));
266        record.set_arc_if_absent(sys_key_updated_at(), Value::UnsignedInteger(updated_at));
267        Some(record)
268    } else {
269        let mut record = UnifiedRecord::with_capacity(6 + row.columns.len());
270        for (i, value) in row.columns.into_iter().enumerate() {
271            record.set(&format!("c{i}"), value);
272        }
273        record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
274        record.set_arc(sys_key_collection(), Value::text(collection));
275        record.set_arc(sys_key_kind(), Value::text(kind));
276        record.set_arc(sys_key_tenant(), tenant);
277        record.set_arc_if_absent(sys_key_created_at(), Value::UnsignedInteger(created_at));
278        record.set_arc_if_absent(sys_key_updated_at(), Value::UnsignedInteger(updated_at));
279        Some(record)
280    }
281}
282
283pub fn runtime_table_record_lean_in_collection(
284    entity: UnifiedEntity,
285    collection: &str,
286) -> Option<UnifiedRecord> {
287    let is_graph = matches!(entity.data, EntityData::Node(_) | EntityData::Edge(_));
288    let mut record = runtime_table_record_lean(entity)?;
289    if is_graph {
290        record.set_arc(sys_key_collection(), Value::text(collection.to_string()));
291    }
292    Some(record)
293}
294
295#[inline(never)]
296/// Clone a row's stored binary document `body` bytes, if any.
297///
298/// Sourced from the row rather than the record because a projection that does
299/// not SELECT `body` would never place it on the record — yet the promoted
300/// fields still have to be read out of it.
301fn document_row_body_bytes(row: &crate::storage::entity::RowData) -> Option<Vec<u8>> {
302    match row.get_field("body") {
303        Some(Value::Json(bytes)) if document_body::is_binary_container(bytes) => {
304            Some(bytes.clone())
305        }
306        _ => None,
307    }
308}
309
310/// Fill a single-source document's top-level fields into `record` by reading
311/// them from the binary `body` (`body` = the row's container bytes).
312///
313/// `columns = None` expands every top-level body field (SELECT *); `Some(cols)`
314/// offset-reads only the requested fields. A no-op when `body` is `None`.
315pub fn fill_document_body_fields(
316    record: &mut UnifiedRecord,
317    columns: Option<&[String]>,
318    body: Option<&[u8]>,
319) {
320    let Some(bytes) = body else {
321        return;
322    };
323    if let Some(body_json) = document_body::decode_container_to_json(bytes) {
324        if let Ok(json_bytes) = crate::json::to_vec(&body_json) {
325            record.set("body", Value::Json(json_bytes));
326        }
327    }
328    let fields: Vec<(String, Value)> = match columns {
329        None => match document_body::body_fields(bytes) {
330            Some(fields) => fields,
331            None => return,
332        },
333        Some(cols) => cols
334            .iter()
335            .filter_map(|col| {
336                document_body::read_body_field(bytes, col).map(|value| (col.clone(), value))
337            })
338            .collect(),
339    };
340    for (name, value) in fields {
341        record.set(&name, value);
342    }
343}
344
345pub fn runtime_table_record_from_entity(entity: UnifiedEntity) -> Option<UnifiedRecord> {
346    // Issue #414: surface graph/vector/queue/etc. entities in SELECT scans.
347    if !matches!(entity.data, EntityData::Row(_) | EntityData::TimeSeries(_)) {
348        return runtime_any_record_from_entity(entity);
349    }
350    let logical_id = entity.logical_id().raw();
351    match entity.data {
352        EntityData::Row(row) => {
353            // Capture the document body before `named` is consumed below.
354            let doc_body = document_row_body_bytes(&row);
355            // Pre-allocate: ~9 system fields + user fields
356            let user_field_count = row
357                .named
358                .as_ref()
359                .map(|n| n.len())
360                .unwrap_or(row.columns.len());
361            let mut record = UnifiedRecord::with_capacity(6 + user_field_count);
362            let collection = entity.kind.collection().to_string();
363            let tenant = runtime_row_tenant_value(&row);
364            let kind = public_row_kind(&row).to_string();
365
366            if let Some(named) = row.named {
367                for (key, value) in named {
368                    record.set(&key, value);
369                }
370            } else if let Some(ref schema) = row.schema {
371                // Columnar storage: use shared schema for field names
372                for (name, value) in schema.iter().zip(row.columns) {
373                    record.set(name, value);
374                }
375            } else {
376                for (index, value) in row.columns.into_iter().enumerate() {
377                    record.set(&format!("c{index}"), value);
378                }
379            }
380            record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
381            record.set_arc(sys_key_collection(), Value::text(collection));
382            record.set_arc(sys_key_kind(), Value::text(kind));
383            record.set_arc(sys_key_tenant(), tenant);
384            record.set_arc_if_absent(
385                sys_key_created_at(),
386                Value::UnsignedInteger(entity.created_at),
387            );
388            record.set_arc_if_absent(
389                sys_key_updated_at(),
390                Value::UnsignedInteger(entity.updated_at),
391            );
392
393            // SELECT * over a single-source document: expand fields from the body.
394            fill_document_body_fields(&mut record, None, doc_body.as_deref());
395
396            Some(record)
397        }
398        EntityData::TimeSeries(ts) => {
399            let mut record = UnifiedRecord::with_capacity(12 + ts.tags.len());
400            record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
401            record.set(
402                "red_collection",
403                Value::text(entity.kind.collection().to_string()),
404            );
405            record.set(
406                "red_kind",
407                Value::text(entity.kind.storage_type().to_string()),
408            );
409            record.set_arc(
410                sys_key_created_at(),
411                Value::UnsignedInteger(entity.created_at),
412            );
413            record.set_arc(
414                sys_key_updated_at(),
415                Value::UnsignedInteger(entity.updated_at),
416            );
417            record.set(
418                "red_sequence_id",
419                Value::UnsignedInteger(entity.sequence_id),
420            );
421            record.set_arc(
422                sys_key_red_entity_type(),
423                Value::text("timeseries".to_string()),
424            );
425            record.set(
426                "red_capabilities",
427                Value::text("document,timeseries,metric,temporal".to_string()),
428            );
429            append_timeseries_record_fields(&mut record, &ts);
430            Some(record)
431        }
432        _ => None,
433    }
434}
435
436/// Borrowed version of `runtime_table_record_from_entity` — avoids cloning the full entity.
437/// Only the field values inserted into the record are cloned, not the entity struct itself.
438#[inline(never)]
439pub fn runtime_table_record_from_entity_ref(entity: &UnifiedEntity) -> Option<UnifiedRecord> {
440    runtime_table_record_from_entity_ref_with_schema(entity, None)
441}
442
443pub fn runtime_table_record_from_entity_ref_with_schema(
444    entity: &UnifiedEntity,
445    fallback_schema: Option<&std::sync::Arc<Vec<String>>>,
446) -> Option<UnifiedRecord> {
447    // Issue #414: surface graph/vector/queue/etc. entities in SELECT scans.
448    if !matches!(&entity.data, EntityData::Row(_) | EntityData::TimeSeries(_)) {
449        return runtime_any_record_from_entity_ref(entity);
450    }
451    match &entity.data {
452        EntityData::Row(row) => {
453            let user_field_count = row
454                .named
455                .as_ref()
456                .map(|n| n.len())
457                .unwrap_or(row.columns.len());
458            let mut record = UnifiedRecord::with_capacity(6 + user_field_count);
459
460            if let Some(named) = &row.named {
461                for (key, value) in named {
462                    record.set(key, value.clone());
463                }
464            } else if let Some(schema) = row.schema.as_ref().or(fallback_schema) {
465                for (name, value) in schema.iter().zip(row.columns.iter()) {
466                    record.set(name, value.clone());
467                }
468            } else {
469                for (index, value) in row.columns.iter().enumerate() {
470                    record.set(&format!("c{index}"), value.clone());
471                }
472            }
473            set_public_row_envelope(&mut record, entity, row);
474
475            // SELECT * over a single-source document — expand body fields.
476            fill_document_body_fields(&mut record, None, document_row_body_bytes(row).as_deref());
477
478            Some(record)
479        }
480        EntityData::TimeSeries(ts) => {
481            let mut record = UnifiedRecord::with_capacity(12 + ts.tags.len());
482            record.set_arc(
483                sys_key_rid(),
484                Value::UnsignedInteger(entity.logical_id().raw()),
485            );
486            record.set(
487                "red_collection",
488                Value::text(entity.kind.collection().to_string()),
489            );
490            record.set(
491                "red_kind",
492                Value::text(entity.kind.storage_type().to_string()),
493            );
494            record.set_arc(
495                sys_key_created_at(),
496                Value::UnsignedInteger(entity.created_at),
497            );
498            record.set_arc(
499                sys_key_updated_at(),
500                Value::UnsignedInteger(entity.updated_at),
501            );
502            record.set(
503                "red_sequence_id",
504                Value::UnsignedInteger(entity.sequence_id),
505            );
506            record.set_arc(
507                sys_key_red_entity_type(),
508                Value::text("timeseries".to_string()),
509            );
510            record.set(
511                "red_capabilities",
512                Value::text("document,timeseries,metric,temporal".to_string()),
513            );
514            append_timeseries_record_fields(&mut record, ts);
515            Some(record)
516        }
517        _ => None,
518    }
519}
520
521/// Projected version — only materializes requested columns for better performance.
522/// Falls back to full materialization if columns is empty (SELECT *).
523#[inline(never)]
524pub fn runtime_table_record_from_entity_projected(
525    entity: UnifiedEntity,
526    columns: &[String],
527) -> Option<UnifiedRecord> {
528    if columns.is_empty() {
529        return runtime_table_record_from_entity(entity);
530    }
531    // Issue #414: graph/vector/queue/etc. — produce a full record so SELECT
532    // can project named graph properties. Extra fields are harmless; the
533    // outer projection layer keeps only the requested ones.
534    if !matches!(entity.data, EntityData::Row(_) | EntityData::TimeSeries(_)) {
535        return runtime_any_record_from_entity(entity);
536    }
537    let logical_id = entity.logical_id().raw();
538
539    match entity.data {
540        EntityData::Row(row) => {
541            // Capture the document body before `named` is consumed below.
542            let doc_body = document_row_body_bytes(&row);
543            let mut record = UnifiedRecord::with_capacity(6 + columns.len());
544            let collection = entity.kind.collection().to_string();
545            let tenant = runtime_row_tenant_value(&row);
546            let kind = public_row_kind(&row).to_string();
547
548            if let Some(named) = row.named {
549                // Named path (single-insert entities): O(1) HashMap lookup per column.
550                for col in columns {
551                    if let Some(value) = named.get(col) {
552                        record.set(col, value.clone());
553                    }
554                }
555            } else if let Some(ref schema) = row.schema {
556                // Columnar path (bulk-insert entities): resolve column name → index
557                // in schema, then access row.columns[idx]. O(n_schema * n_projected)
558                // but n_projected is small (explicit SELECT list).
559                for col in columns {
560                    if let Some(idx) = schema.iter().position(|s| s == col) {
561                        if let Some(value) = row.columns.get(idx) {
562                            record.set(col, value.clone());
563                        }
564                    }
565                }
566            } else {
567                // Positional-only (no schema, no names): map c0/c1/... or fallback
568                for col in columns {
569                    if let Some(idx) = col.strip_prefix('c').and_then(|s| s.parse::<usize>().ok()) {
570                        if let Some(value) = row.columns.get(idx) {
571                            record.set(col, value.clone());
572                        }
573                    }
574                }
575            }
576            record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
577            record.set_arc(sys_key_collection(), Value::text(collection));
578            record.set_arc(sys_key_kind(), Value::text(kind));
579            record.set_arc(sys_key_tenant(), tenant);
580            record.set_arc_if_absent(
581                sys_key_created_at(),
582                Value::UnsignedInteger(entity.created_at),
583            );
584            record.set_arc_if_absent(
585                sys_key_updated_at(),
586                Value::UnsignedInteger(entity.updated_at),
587            );
588
589            // Single-source document: offset-read just the projected fields
590            // from the body.
591            fill_document_body_fields(&mut record, Some(columns), doc_body.as_deref());
592
593            Some(record)
594        }
595        EntityData::TimeSeries(ts) => {
596            let mut record = UnifiedRecord::new();
597            record.set_arc(sys_key_rid(), Value::UnsignedInteger(logical_id));
598
599            for col in columns {
600                match col.as_str() {
601                    "metric" => record.set("metric", Value::text(ts.metric.clone())),
602                    "timestamp_ns" => {
603                        record.set("timestamp_ns", Value::UnsignedInteger(ts.timestamp_ns))
604                    }
605                    "timestamp" => record.set("timestamp", Value::UnsignedInteger(ts.timestamp_ns)),
606                    "time" => record.set("time", Value::UnsignedInteger(ts.timestamp_ns)),
607                    "value" => record.set("value", Value::Float(ts.value)),
608                    "tags" if !ts.tags.is_empty() => {
609                        record.set("tags", timeseries_tags_value(&ts.tags));
610                    }
611                    _ => {
612                        if let Some(value) = ts.fields.get(col) {
613                            record.set(col, value.clone());
614                        }
615                    }
616                }
617            }
618
619            Some(record)
620        }
621        _ => None,
622    }
623}
624
625/// Ref-based projected materialization — avoids cloning the whole entity.
626/// Only clones the K projected field values, not the N-K ignored ones.
627/// Pre-computed index path for columnar (bulk-inserted) entities.
628///
629/// `idx_map` is `&[(col_pos_in_columns, schema_idx)]` — precomputed once
630/// before the scan loop so every row does O(k) direct indexed access instead
631/// of O(schema_len × k) linear searches.
632///
633/// Returns `None` when the entity is not a columnar Row (schema = None).
634pub fn runtime_table_record_with_col_indices(
635    entity: &UnifiedEntity,
636    columns: &[String],
637    idx_map: &[(usize, usize)],
638) -> Option<UnifiedRecord> {
639    if idx_map.is_empty() {
640        return None;
641    }
642    let row = entity.data.as_row()?;
643    // Applies to columnar entities. After a persistent reopen, rows written in
644    // compact positional form may not carry row.schema; callers can still pass
645    // an idx_map derived from the persisted collection contract.
646    if row.named.is_some() || row.columns.is_empty() {
647        return None;
648    }
649    let mut record = UnifiedRecord::with_capacity(6 + idx_map.len());
650    for &(ci, si) in idx_map {
651        if let Some(value) = row.columns.get(si) {
652            // Use set_owned to skip the hidden to_string() clone.
653            record.set_owned(columns[ci].clone(), value.clone());
654        }
655    }
656    set_public_row_envelope(&mut record, entity, row);
657    Some(record)
658}
659
660/// Used by the fast-path scan when `select_cols` is non-empty.
661///
662/// Returns `None` for non-Row entities (caller falls back to owned path).
663pub fn runtime_table_record_from_entity_ref_projected(
664    entity: &UnifiedEntity,
665    columns: &[String],
666) -> Option<UnifiedRecord> {
667    if columns.is_empty() {
668        return None; // caller should use full materialization for SELECT *
669    }
670    // Issue #414: non-Row entities (graph/vector/queue) must be surfaced in
671    // SELECT scans. Fall back to the universal materializer; the outer
672    // projection layer keeps only the requested columns.
673    if !matches!(&entity.data, EntityData::Row(_)) {
674        return runtime_any_record_from_entity_ref(entity);
675    }
676    let row = entity.data.as_row()?;
677
678    let mut record = UnifiedRecord::with_capacity(6 + columns.len());
679
680    if let Some(ref named) = row.named {
681        for col in columns {
682            if let Some(value) = named.get(col.as_str()) {
683                record.set(col, value.clone());
684            }
685        }
686    } else if let Some(ref schema) = row.schema {
687        for col in columns {
688            if let Some(idx) = schema.iter().position(|s| s == col) {
689                if let Some(value) = row.columns.get(idx) {
690                    record.set(col, value.clone());
691                }
692            }
693        }
694    } else {
695        for col in columns {
696            if let Some(idx) = col.strip_prefix('c').and_then(|s| s.parse::<usize>().ok()) {
697                if let Some(value) = row.columns.get(idx) {
698                    record.set(col, value.clone());
699                }
700            }
701        }
702    }
703    set_public_row_envelope(&mut record, entity, row);
704    // Single-source document: offset-read the projected body fields.
705    fill_document_body_fields(
706        &mut record,
707        Some(columns),
708        document_row_body_bytes(row).as_deref(),
709    );
710    Some(record)
711}
712
713#[inline(never)]
714pub fn runtime_any_record_from_entity(entity: UnifiedEntity) -> Option<UnifiedRecord> {
715    let identity_entity = entity.clone();
716    let kind = entity.kind.clone();
717    let collection = kind.collection().to_string();
718    let storage_type = kind.storage_type().to_string();
719    let entity_id = entity.logical_id().raw();
720    let created_at = entity.created_at;
721    let updated_at = entity.updated_at;
722    let sequence_id = entity.sequence_id;
723
724    let (entity_type, capabilities, mut record) = match (kind, entity.data) {
725        (EntityKind::TableRow { row_id, .. }, EntityData::Row(row)) => {
726            let capabilities = runtime_row_capabilities(&row);
727            let entity_type = runtime_row_entity_type(&row);
728            let doc_body = document_row_body_bytes(&row);
729            let mut record = UnifiedRecord::new();
730            record.set_arc(sys_key_row_id(), Value::UnsignedInteger(row_id));
731            if let Some(named) = row.named {
732                for (key, value) in named {
733                    record.set(&key, value);
734                }
735            } else {
736                for (index, value) in row.columns.into_iter().enumerate() {
737                    record.set(&format!("c{index}"), value);
738                }
739            }
740            fill_document_body_fields(&mut record, None, doc_body.as_deref());
741            (entity_type, capabilities, record)
742        }
743        (EntityKind::GraphNode(node), EntityData::Node(node_data)) => {
744            let mut record = UnifiedRecord::new();
745            record.set("label", Value::text(node.label));
746            record.set("node_type", Value::text(node.node_type));
747            for (key, value) in node_data.properties {
748                record.set(&key, value);
749            }
750            set_public_graph_envelope(&mut record, &identity_entity, "node");
751            (
752                "graph_node",
753                runtime_record_capability_list(["graph", "graph_node"]),
754                record,
755            )
756        }
757        (EntityKind::GraphEdge(edge_kind), EntityData::Edge(edge)) => {
758            let mut record = UnifiedRecord::new();
759            record.set("label", Value::text(edge_kind.label));
760            record.set("from_rid", graph_endpoint_rid_value(&edge_kind.from_node));
761            record.set("to_rid", graph_endpoint_rid_value(&edge_kind.to_node));
762            record.set("weight", Value::Float(edge.weight as f64));
763            for (key, value) in edge.properties {
764                record.set(&key, value);
765            }
766            set_public_graph_envelope(&mut record, &identity_entity, "edge");
767            (
768                "graph_edge",
769                runtime_record_capability_list(["graph", "graph_edge"]),
770                record,
771            )
772        }
773        (EntityKind::Vector { .. }, EntityData::Vector(vector)) => {
774            let mut record = UnifiedRecord::new();
775            record.set(
776                "dimension",
777                Value::UnsignedInteger(vector.dense.len() as u64),
778            );
779            if let Some(content) = vector.content {
780                record.set("content", Value::text(content));
781            }
782            (
783                "vector",
784                runtime_record_capability_list(["vector", "similarity", "embedding"]),
785                record,
786            )
787        }
788        (EntityKind::TimeSeriesPoint(_), EntityData::TimeSeries(ts)) => {
789            let mut record = UnifiedRecord::new();
790            append_timeseries_record_fields(&mut record, &ts);
791            (
792                "timeseries",
793                runtime_record_capability_list(["document", "timeseries", "metric", "temporal"]),
794                record,
795            )
796        }
797        (EntityKind::QueueMessage { position, .. }, EntityData::QueueMessage(msg)) => {
798            // Phase 2.5.5 RLS universal: queue messages surface their
799            // `payload` Value, priority, attempts, ack state, and
800            // position so policies like
801            // `USING (payload.tenant = CURRENT_TENANT())` can reach
802            // the JSON payload via the dotted-path resolver.
803            let mut record = UnifiedRecord::new();
804            record.set("position", Value::UnsignedInteger(position));
805            record.set("payload", msg.payload);
806            record.set("attempts", Value::UnsignedInteger(msg.attempts as u64));
807            record.set("acked", Value::Boolean(msg.acked));
808            if let Some(priority) = msg.priority {
809                record.set("priority", Value::Integer(priority as i64));
810            }
811            (
812                "queue_message",
813                runtime_record_capability_list(["document", "queue", "message"]),
814                record,
815            )
816        }
817        _ => return None,
818    };
819
820    if let EntityData::Row(row) = &identity_entity.data {
821        set_public_row_envelope(&mut record, &identity_entity, row);
822    } else if matches!(
823        identity_entity.data,
824        EntityData::Node(_) | EntityData::Edge(_)
825    ) {
826        set_runtime_entity_metadata(&mut record, entity_type, capabilities);
827        apply_runtime_identity_hints(&mut record, &identity_entity);
828    } else {
829        // #1369 — every entity model exposes its entity-id as `rid`
830        // (vector / time-series / queue rows). `rid` is mandatory for
831        // every model, never optional.
832        record.set_arc(sys_key_rid(), Value::UnsignedInteger(entity_id));
833        record.set_arc(sys_key_red_collection(), Value::text(collection));
834        record.set_arc(sys_key_red_kind(), Value::text(storage_type));
835        record.set_arc_if_absent(sys_key_created_at(), Value::UnsignedInteger(created_at));
836        record.set_arc_if_absent(sys_key_updated_at(), Value::UnsignedInteger(updated_at));
837        record.set_arc(
838            sys_key_red_sequence_id(),
839            Value::UnsignedInteger(sequence_id),
840        );
841        set_runtime_entity_metadata(&mut record, entity_type, capabilities);
842        apply_runtime_identity_hints(&mut record, &identity_entity);
843    }
844
845    Some(record)
846}
847
848pub fn runtime_any_record_from_entity_ref(entity: &UnifiedEntity) -> Option<UnifiedRecord> {
849    let kind = &entity.kind;
850    let collection = kind.collection().to_string();
851    let storage_type = kind.storage_type().to_string();
852    let entity_id = entity.logical_id().raw();
853    let created_at = entity.created_at;
854    let updated_at = entity.updated_at;
855    let sequence_id = entity.sequence_id;
856
857    let (entity_type, capabilities, mut record) = match (kind, &entity.data) {
858        (EntityKind::TableRow { row_id, .. }, EntityData::Row(row)) => {
859            let capabilities = runtime_row_capabilities(row);
860            let entity_type = runtime_row_entity_type(row);
861            let doc_body = document_row_body_bytes(row);
862            let mut record = UnifiedRecord::new();
863            record.set_arc(sys_key_row_id(), Value::UnsignedInteger(*row_id));
864            if let Some(named) = row.named.as_ref() {
865                for (key, value) in named {
866                    record.set(key, value.clone());
867                }
868            } else if let Some(schema) = row.schema.as_ref() {
869                for (name, value) in schema.iter().zip(row.columns.iter()) {
870                    record.set(name, value.clone());
871                }
872            } else {
873                for (index, value) in row.columns.iter().enumerate() {
874                    record.set(&format!("c{index}"), value.clone());
875                }
876            }
877            fill_document_body_fields(&mut record, None, doc_body.as_deref());
878            (entity_type, capabilities, record)
879        }
880        (EntityKind::GraphNode(node), EntityData::Node(node_data)) => {
881            let mut record = UnifiedRecord::new();
882            record.set("label", Value::text(node.label.clone()));
883            record.set("node_type", Value::text(node.node_type.clone()));
884            for (key, value) in &node_data.properties {
885                record.set(key, value.clone());
886            }
887            set_public_graph_envelope(&mut record, entity, "node");
888            (
889                "graph_node",
890                runtime_record_capability_list(["graph", "graph_node"]),
891                record,
892            )
893        }
894        (EntityKind::GraphEdge(edge_kind), EntityData::Edge(edge)) => {
895            let mut record = UnifiedRecord::new();
896            record.set("label", Value::text(edge_kind.label.clone()));
897            record.set("from_rid", graph_endpoint_rid_value(&edge_kind.from_node));
898            record.set("to_rid", graph_endpoint_rid_value(&edge_kind.to_node));
899            record.set("weight", Value::Float(edge.weight as f64));
900            for (key, value) in &edge.properties {
901                record.set(key, value.clone());
902            }
903            // #1369 — graph edges surface their endpoints as `from_rid`/`to_rid`
904            // (the canonical rid references), NOT raw `from`/`to`, so the public
905            // envelope stays rid-based and never leaks the internal endpoint text
906            // the INSERT stored in properties.
907            set_public_graph_envelope(&mut record, entity, "edge");
908            (
909                "graph_edge",
910                runtime_record_capability_list(["graph", "graph_edge"]),
911                record,
912            )
913        }
914        (EntityKind::Vector { .. }, EntityData::Vector(vector)) => {
915            let mut record = UnifiedRecord::new();
916            record.set(
917                "dimension",
918                Value::UnsignedInteger(vector.dense.len() as u64),
919            );
920            if let Some(content) = vector.content.as_ref() {
921                record.set("content", Value::text(content.clone()));
922            }
923            (
924                "vector",
925                runtime_record_capability_list(["vector", "similarity", "embedding"]),
926                record,
927            )
928        }
929        (EntityKind::TimeSeriesPoint(_), EntityData::TimeSeries(ts)) => {
930            let mut record = UnifiedRecord::new();
931            append_timeseries_record_fields(&mut record, ts);
932            (
933                "timeseries",
934                runtime_record_capability_list(["document", "timeseries", "metric", "temporal"]),
935                record,
936            )
937        }
938        (EntityKind::QueueMessage { position, .. }, EntityData::QueueMessage(msg)) => {
939            let mut record = UnifiedRecord::new();
940            record.set("position", Value::UnsignedInteger(*position));
941            record.set("payload", msg.payload.clone());
942            record.set("attempts", Value::UnsignedInteger(msg.attempts as u64));
943            record.set("acked", Value::Boolean(msg.acked));
944            if let Some(priority) = msg.priority {
945                record.set("priority", Value::Integer(priority as i64));
946            }
947            (
948                "queue_message",
949                runtime_record_capability_list(["document", "queue", "message"]),
950                record,
951            )
952        }
953        _ => return None,
954    };
955
956    if let EntityData::Row(row) = &entity.data {
957        set_public_row_envelope(&mut record, entity, row);
958    } else if matches!(entity.data, EntityData::Node(_) | EntityData::Edge(_)) {
959        set_runtime_entity_metadata(&mut record, entity_type, capabilities);
960        apply_runtime_identity_hints(&mut record, entity);
961    } else {
962        // #1369 — every entity model exposes its entity-id as `rid`
963        // (vector / time-series / queue rows). `rid` is mandatory for
964        // every model, never optional.
965        record.set_arc(sys_key_rid(), Value::UnsignedInteger(entity_id));
966        record.set_arc(sys_key_red_collection(), Value::text(collection));
967        record.set_arc(sys_key_red_kind(), Value::text(storage_type));
968        record.set_arc_if_absent(sys_key_created_at(), Value::UnsignedInteger(created_at));
969        record.set_arc_if_absent(sys_key_updated_at(), Value::UnsignedInteger(updated_at));
970        record.set_arc(
971            sys_key_red_sequence_id(),
972            Value::UnsignedInteger(sequence_id),
973        );
974        set_runtime_entity_metadata(&mut record, entity_type, capabilities);
975        apply_runtime_identity_hints(&mut record, entity);
976    }
977
978    Some(record)
979}
980
981fn append_timeseries_record_fields(
982    record: &mut UnifiedRecord,
983    ts: &crate::storage::entity::TimeSeriesData,
984) {
985    record.set("metric", Value::text(ts.metric.clone()));
986    record.set("timestamp_ns", Value::UnsignedInteger(ts.timestamp_ns));
987    record.set("timestamp", Value::UnsignedInteger(ts.timestamp_ns));
988    record.set("time", Value::UnsignedInteger(ts.timestamp_ns));
989    record.set("value", Value::Float(ts.value));
990    if !ts.tags.is_empty() {
991        record.set("tags", timeseries_tags_value(&ts.tags));
992    }
993    for (key, value) in &ts.fields {
994        record.set(key, value.clone());
995    }
996}
997
998fn timeseries_tags_value(tags: &std::collections::HashMap<String, String>) -> Value {
999    let object = tags
1000        .iter()
1001        .map(|(key, value)| (key.clone(), decode_stored_tag_value(value)))
1002        .collect();
1003    let json = crate::json::Value::Object(object);
1004    Value::Json(crate::json::to_vec(&json).unwrap_or_default())
1005}
1006
1007#[inline(never)]
1008pub fn set_runtime_entity_metadata(
1009    record: &mut UnifiedRecord,
1010    entity_type: &str,
1011    capabilities: BTreeSet<String>,
1012) {
1013    let capabilities_text = capabilities.into_iter().collect::<Vec<_>>().join(",");
1014    record.set_arc(
1015        sys_key_red_entity_type(),
1016        Value::text(entity_type.to_string()),
1017    );
1018    record.set_arc(sys_key_red_capabilities(), Value::text(capabilities_text));
1019}
1020
1021pub fn runtime_record_capability_list<const N: usize>(values: [&str; N]) -> BTreeSet<String> {
1022    values.into_iter().map(|value| value.to_string()).collect()
1023}
1024
1025pub fn runtime_row_capabilities(row: &crate::storage::entity::RowData) -> BTreeSet<String> {
1026    let mut capabilities = runtime_record_capability_list(["table", "structured"]);
1027    if runtime_row_is_kv(row) {
1028        capabilities.insert("kv".to_string());
1029    }
1030    if runtime_row_has_document_capability(row) {
1031        capabilities.insert("document".to_string());
1032    }
1033    capabilities
1034}
1035
1036/// Fast capability string for table rows — avoids BTreeSet allocation.
1037/// Returns a pre-computed comma-separated capabilities string.
1038pub fn runtime_row_capabilities_str(row: &crate::storage::entity::RowData) -> &'static str {
1039    let is_kv = runtime_row_is_kv(row);
1040    let is_doc = runtime_row_has_document_capability(row);
1041    match (is_kv, is_doc) {
1042        (false, false) => "structured,table",
1043        (true, false) => "kv,structured,table",
1044        (false, true) => "document,structured,table",
1045        (true, true) => "document,kv,structured,table",
1046    }
1047}
1048
1049pub fn runtime_row_entity_type(row: &crate::storage::entity::RowData) -> &'static str {
1050    if runtime_row_is_kv(row) {
1051        return "kv";
1052    }
1053
1054    if runtime_row_has_document_capability(row) {
1055        "document"
1056    } else {
1057        "table"
1058    }
1059}
1060
1061fn runtime_row_is_kv(row: &crate::storage::entity::RowData) -> bool {
1062    let Some(named) = row.named.as_ref() else {
1063        return false;
1064    };
1065
1066    if named.len() == 2 {
1067        named.contains_key("key") && named.contains_key("value")
1068    } else if named.len() == 1 {
1069        named.contains_key("key") || named.contains_key("value")
1070    } else {
1071        false
1072    }
1073}
1074
1075pub fn runtime_row_has_document_capability(row: &crate::storage::entity::RowData) -> bool {
1076    row.named
1077        .as_ref()
1078        .map(|named| named.values().any(runtime_documentish_value))
1079        .unwrap_or(false)
1080        || row.columns.iter().any(runtime_documentish_value)
1081}
1082
1083pub fn runtime_documentish_value(value: &Value) -> bool {
1084    matches!(value, Value::Json(_) | Value::Blob(_))
1085}
1086
1087pub fn runtime_entity_type_and_capabilities(
1088    entity: &UnifiedEntity,
1089) -> (&'static str, BTreeSet<String>) {
1090    match (&entity.kind, &entity.data) {
1091        (EntityKind::TableRow { .. }, EntityData::Row(row)) => {
1092            (runtime_row_entity_type(row), runtime_row_capabilities(row))
1093        }
1094        (EntityKind::GraphNode(_), EntityData::Node(_)) => (
1095            "graph_node",
1096            runtime_record_capability_list(["graph", "graph_node"]),
1097        ),
1098        (EntityKind::GraphEdge(_), EntityData::Edge(_)) => (
1099            "graph_edge",
1100            runtime_record_capability_list(["graph", "graph_edge"]),
1101        ),
1102        (EntityKind::Vector { .. }, EntityData::Vector(_)) => (
1103            "vector",
1104            runtime_record_capability_list(["vector", "similarity", "embedding"]),
1105        ),
1106        (EntityKind::TimeSeriesPoint(_), EntityData::TimeSeries(_)) => (
1107            "timeseries",
1108            runtime_record_capability_list(["document", "timeseries", "metric", "temporal"]),
1109        ),
1110        (EntityKind::QueueMessage { .. }, EntityData::QueueMessage(_)) => (
1111            "queue_message",
1112            runtime_record_capability_list(["document", "queue", "message"]),
1113        ),
1114        _ => ("unknown", BTreeSet::new()),
1115    }
1116}
1117
1118pub fn apply_runtime_identity_hints(record: &mut UnifiedRecord, entity: &UnifiedEntity) {
1119    for cross_ref in entity.cross_refs() {
1120        let value = match cross_ref.ref_type {
1121            RefType::VectorToRow | RefType::NodeToRow => Some(Value::RowRef(
1122                cross_ref.target_collection.clone(),
1123                cross_ref.target.raw(),
1124            )),
1125            RefType::VectorToNode | RefType::RowToNode => Some(Value::NodeRef(format!(
1126                "{}:{}",
1127                cross_ref.target_collection, cross_ref.target
1128            ))),
1129            RefType::RowToEdge | RefType::EdgeToVector => Some(Value::EdgeRef(format!(
1130                "{}:{}",
1131                cross_ref.target_collection, cross_ref.target
1132            ))),
1133            _ => Some(Value::text(format!(
1134                "{}:{}",
1135                cross_ref.target_collection, cross_ref.target
1136            ))),
1137        };
1138
1139        if let Some(value) = value {
1140            let link_key: std::sync::Arc<str> = std::sync::Arc::from("_linked_identity");
1141            match cross_ref.ref_type {
1142                RefType::VectorToRow | RefType::NodeToRow => {
1143                    record.set_arc(std::sync::Arc::from("_source_row"), value.clone());
1144                    record.overflow_entry_or_insert(link_key, value);
1145                }
1146                RefType::VectorToNode | RefType::RowToNode => {
1147                    record.set_arc(std::sync::Arc::from("_source_node"), value.clone());
1148                    record.overflow_entry_or_insert(link_key, value);
1149                }
1150                RefType::RowToEdge | RefType::EdgeToVector => {
1151                    record.set_arc(std::sync::Arc::from("_source_edge"), value.clone());
1152                    record.overflow_entry_or_insert(link_key, value);
1153                }
1154                _ => {
1155                    record.overflow_entry_or_insert(
1156                        std::sync::Arc::from("_source_entity"),
1157                        value.clone(),
1158                    );
1159                    record.overflow_entry_or_insert(link_key, value);
1160                }
1161            }
1162        }
1163    }
1164}