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