Skip to content

feat(ogar-render-askama): T5 — SurrealqlTable (completes the +5 kit)#86

Merged
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-render-askama-t5-surrealql-table
Jun 20, 2026
Merged

feat(ogar-render-askama): T5 — SurrealqlTable (completes the +5 kit)#86
AdaWorldAPI merged 2 commits into
mainfrom
claude/ogar-render-askama-t5-surrealql-table

Conversation

@AdaWorldAPI

Copy link
Copy Markdown
Owner

What

T5 (SurrealqlTable) — the codegen-flavour finisher of the Northstar +5 kit. Stacks on top of #85 (T4); the duplicated T4 commit drops on rebase when #85 merges first.

After this lands, 5/7 ArtifactKind variants are real. Only the deprecated OpenapiSchema (anti-pattern #8) and the roadmap-only NodeGuidRoutingArm remain stubbed.

Direct sibling of T1 (RustStruct) — both are codegen-flavour, both consume &Class at build time, both emit typed source for downstream compilers/engines.

What it emits

Sample render of project_role():

-- ProjectRole — canonical class generated from `ogar_vocab::project_role()`.
-- DO NOT EDIT BY HAND. Re-render via `ogar-render-askama`.

DEFINE TABLE project_role SCHEMAFULL COMMENT "OGAR codebook id 0x0117 (project_role)";

DEFINE FIELD name ON project_role TYPE string;
DEFINE FIELD position ON project_role TYPE int;
DEFINE FIELD permissions ON project_role TYPE string;
DEFINE FIELD memberships ON project_role TYPE array<record<project_membership>> COMMENT "has_many -> project_membership";
DEFINE INDEX idx_name ON project_role FIELDS name;

Mapping

Rails type SurrealQL TYPE
string / text string
integer / big_integer / bigint int
float / double float
decimal / monetary decimal
boolean / bool bool
date / datetime / timestamp datetime
json / jsonb object
Association kind SurrealQL TYPE
belongs_to / has_one option<record<target>>
has_many / has_and_belongs_to_many array<record<target>>

Target table is the canonical concept name when it's promoted (tax_policy, project_membership, …) or snake_case'd class_name otherwise.

Reserved-word escape (SurQL-side companion of Rust + TS escapes)

SurrealQL reserves type / value / id / for / select / where / etc. The emitter back-tick-quotes field names that collide:

DEFINE FIELD `type` ON project_actor TYPE string;

project_actor declares an attribute literally named type (Rails STI convention) — the same hazard that bit Rust on codex P1 #78 and TypeScript on T2. Each emitter handles it locally per the per-target convention.

Default index

Classes with a name / subject / title / label primary attribute get a non-unique index on it (idx_<name>). Classes without one (billable_work_entry, etc.) emit no default — consumers add per their own policy.

Tests (+6 T5, 44/44 total)

  • surrealql_table_emits_define_table_with_codebook_meta — proof of shape on project_work_item: TABLE name + CODEBOOK comment 0x0102 + DEFINE FIELD per attribute.
  • surrealql_table_maps_rails_types_to_surqlproject_role's name/string, position/int, permissions/string mapping.
  • surrealql_table_emits_record_links_for_family_edges — all 12 billable_work_entry edges emit DEFINE FIELD; at least one option<record<…>> link with snake_case'd target (tax_policy).
  • surrealql_table_emits_primary_label_indexproject_role gets DEFINE INDEX idx_name ON project_role FIELDS name.
  • surrealql_table_escapes_reserved_identifiersproject_actor.typeDEFINE FIELD `type` ON project_actor.
  • surrealql_table_default_indexes_only_when_class_has_primary_label — negative pin: billable_work_entry gets no default index.

Workspace check + workspace test green.

Note on op-surreal-ast

This crate emits DDL for the canonical layer's own consumption (render(class, ArtifactKind::SurrealqlTable)). The byte-identical-output pin in openproject-nexgen-rs::op-surreal-ast still holds for that crate's own emission — the two paths are sibling, not competing. See Northstar plan §2.1 / §2.4 for the call-site picture; C3 (the op-surreal-ast::from_class_view adapter) is the bridge.

+5 kit complete

PR Kind Flavour
T1 RustStruct codegen ✅
T2 HtmlListView render ✅
T3 HtmlDetailView render ✅
T4 HtmlForm render ⏳ (#85)
T5 SurrealqlTable codegen — this PR

C-series consumer wirings (C1–C5) are the next move; each PR is small and lands across the openproject-nexgen-rs / redmine-rs repos.

claude added 2 commits June 20, 2026 01:21
…assView)

Fourth real emitter per Northstar plan §3. Mirror of Redmine's
_form.html.erb shape: one shared form template per resource, here
substrate-agnostic via the codebook + ClassView. Same plumbing as
T2/T3 with a new dimension (InputKind for form-input controls,
parallel to ColumnKind for cell formatters).

Surface (additive):
- ArtifactKind::HtmlForm variant (slot 3, append-only). ALL.len() now 7.
- form_view::InputKind — 9-variant enum: Text, TextArea, Number, Range,
  Checkbox, Date, DateTime, Select, Hidden. Append-only; template stems
  stable.
- form_view::default_input_kind_for(name, type_name) — resolver mirroring
  list_view::default_kind_for. id/*_id → Hidden, *_ratio → Range,
  text+prose → TextArea, boolean → Checkbox, date/datetime → date inputs,
  numeric Rails types → Number, else → Text.
- artifact_kinds::inputs — one binding-struct + askama template per
  InputKind (9 total). Each escape="html" so per-input variables are
  HTML-escaped; spine concats with |safe.
- artifact_kinds::inputs::InputData — owned data variant the caller
  supplies, keyed by InputKind. Carries value, required, options, etc.
- artifact_kinds::html_form::FormFieldSource + FormSource — the public
  call-site shapes for one field and the whole form.
- artifact_kinds::html_form::render_form(class_id, concept, &FormSource)
  — the substantive entry point.
- HtmlFormEmitter — codebook-only proof-of-shape for the for_kind
  dispatch path; synthesises a new-record form from class attributes.
- empty_input_for(InputKind) — convenience for callers that want a
  blank form.
- templates/dispatch/html_form.askama — spine; <form> + <fieldset> +
  per-field <div class="form-field-…"> wrappers (hidden fields emitted
  bare without wrapper).
- 9 templates/dispatch/input/*.askama: text, textarea, number, range,
  checkbox, date, datetime, select, hidden.

Refactor — cell-dispatch factored:
- New artifact_kinds::cells::render_cell_body(&CellData) → String —
  shared per-kind dispatch. T2's render_cell and T3's render_cell_body
  now call it. T4 follows the same pattern with InputKind via
  inputs::render_input_body. "Three points form a line" (Northstar §1.6
  notice from #84).

Tests (+13, 38/38 total):
- form_view::tests (8): template stems stable, html_input_type
  None-vs-Some, every default_input_kind_for arm (Hidden / Range /
  TextArea / Number / Checkbox / Date / DateTime / Text fallback).
- html_form_proof_of_shape_renders_inputs_for_class_attributes — proof
  of shape on project_role; data-class-id="0x0117" + per-attribute
  form-field-<name> wrappers + POST/Create defaults.
- html_form_dispatches_input_kinds_to_their_sub_templates — explicit
  field per kind; pins every <input type=…>, textarea, select option
  selected="…", checkbox hidden-zero idiom, date/datetime types,
  hidden-without-wrapper, edit-form record_id, CSRF token, legend,
  submit/cancel buttons.
- html_form_escapes_data_derived_strings_xss_regression — same XSS
  contract as T2/T3 spines: label / hint / css_classes / action /
  legend / submit_label / values / placeholder / csrf_token all escape;
  action="/<bad-action>" → action="/&lt;bad-action&gt;".
- input_kind_resolver_is_wired_through_render_kit — pin the public
  re-export.
- artifact_kind_all_const_enumerates_every_variant — ALL.len() 6 → 7.

Workspace check + workspace test green. 4/7 real emitters now (T1
RustStruct, T2 HtmlListView, T3 HtmlDetailView, T4 HtmlForm); 3 stubbed
for T5 (SurrealqlTable) and the two roadmap-only / deprecated variants.
…er of the +5 kit)

Fifth real emitter, completing the Northstar plan §3 +5 kit. Codegen
flavour: emits SurrealQL DDL the SurrealDB engine consumes at boot.
Direct sibling of T1 (RustStruct) — both consume `&Class` at build time
and emit typed source for downstream compilers/engines.

Stacked on top of T4 (PR #85) since T4 isn't merged yet; rebase drops
the duplicated T4 commit cleanly when #85 merges first.

Surface:
- artifact_kinds::surrealql_table::SurrealqlTableEmitter — the concrete
  emitter; pure read of &Class, no I/O.
- templates/dispatch/surrealql_table.askama — spine: DEFINE TABLE +
  DEFINE FIELD per attribute + DEFINE FIELD per family edge (with the
  comment naming the kind + target) + DEFINE INDEX per primary-label
  attribute.
- Dispatcher: SurrealqlTable now routes to the real emitter; stub list
  in `stub_emits_marker_for_unimplemented_kinds` shrinks to just
  OpenapiSchema + NodeGuidRoutingArm.

Mapping (Rails → SurrealQL):
- string/text → string
- integer/big_integer/bigint → int
- float/double → float
- decimal/monetary → decimal
- boolean/bool → bool
- date/datetime/timestamp → datetime
- json/jsonb → object
- belongs_to / has_one / future-variants → option<record<target>>
- has_many / habtm → array<record<target>>

Reserved-word escape: SurrealQL reserves `type` / `value` / `id` /
`for` / `select` / `where` / etc. Field names that collide get back-tick-
quoted (project_actor's `type` attribute → `DEFINE FIELD \`type\``).
TS-side companion of Rust's `r#type` (codex P1 on #78) and TS's
`"type":` (T2 from #83).

Default index: classes with a `name` / `subject` / `title` / `label`
attribute get a non-unique index on it (`idx_<name>`). Other classes
(billable_work_entry, etc.) emit no default index — consumers add per
their own policy.

Tests (+6 for T5, 44/44 total):
- surrealql_table_emits_define_table_with_codebook_meta — proof of shape
  on project_work_item: TABLE name + CODEBOOK comment with 0x0102
  + DEFINE FIELD per attribute.
- surrealql_table_maps_rails_types_to_surql — project_role's name/string,
  position/int, permissions/string mapping pinned.
- surrealql_table_emits_record_links_for_family_edges — all 12
  billable_work_entry edges emit DEFINE FIELD; at least one
  `option<record<...>>` link with snake_case'd target (`tax_policy`).
- surrealql_table_emits_primary_label_index — project_role gets
  `DEFINE INDEX idx_name ON project_role FIELDS name`.
- surrealql_table_escapes_reserved_identifiers — project_actor's `type`
  attribute → `DEFINE FIELD \`type\`` (never bare).
- surrealql_table_default_indexes_only_when_class_has_primary_label —
  billable_work_entry (no name/subject/title/label) does NOT get a
  default index.
- stub_emits_marker_for_unimplemented_kinds — SurrealqlTable removed
  from the stub list.

Workspace check + workspace test green. +5 kit complete: T1 RustStruct
(codegen), T2 HtmlListView (render), T3 HtmlDetailView (render), T4
HtmlForm (render), T5 SurrealqlTable (codegen). 5/7 ArtifactKind
variants are real; only OpenapiSchema (deprecated, anti-pattern #8) and
NodeGuidRoutingArm (roadmap-only) remain stubbed.

Note on op-surreal-ast: this crate emits DDL for the canonical layer's
own consumption (the byte-identical pin in op-surreal-ast still holds
for that crate's own emission; the two paths are sibling — see
Northstar plan §2.1 / §2.4 for the call-site picture).

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 61b2c142c0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

{# driven by `fields` (per-attribute pre-rendered <input>s). #}
{# Pre-rendered field bodies are |safe (each sub-template escapes its #}
{# own variables); everything else is HTML-escaped by the spine. #}
<form class="ogar-form" data-class-id="{{ class_id_hex }}" data-concept="{{ canonical_concept }}" method="{{ method }}" action="{{ action }}">

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Submit edit forms with a valid HTML method

When callers use the documented edit path (FormSource.method is described and tested with "patch"), this value is written directly into the HTML method attribute. Browsers only submit forms with GET/POST, so PATCH needs a POST form plus a hidden method override; otherwise these generated edit forms submit with the wrong method and miss PATCH routes.

Useful? React with 👍 / 👎.

Comment on lines +164 to +169
match a.kind {
AssociationKind::HasMany | AssociationKind::HasAndBelongsToMany => {
format!("array<record<{target_table}>>")
}
// BelongsTo + HasOne + any future variant → optional record link.
_ => format!("option<record<{target_table}>>"),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve association ownership in Surreal fields

For classes with HasMany/HasOne associations such as project_work_item.journals, ogar_vocab::AssociationKind marks those as non-owning, but this branch creates fields on the current table, and the wildcard also makes every BelongsTo optional even when optional is unset or false. That corrupts the generated schema by storing inverse collections and allowing required FK records to be absent; emit fields only for owning BelongsTo associations and wrap option<...> based on Association::optional.

Useful? React with 👍 / 👎.

if !concept.is_empty() {
concept.to_string()
} else {
class.name.to_ascii_lowercase()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Quote non-bare SurrealQL table identifiers

When rendering an unpromoted Class whose source name contains dots or namespaces, such as Odoo sale.order, this fallback lowercases and emits the raw string into DEFINE TABLE and ON. That produces invalid SurrealQL like DEFINE TABLE sale.order; run the fallback through a SurrealQL identifier quoting helper, including backtick escaping, instead of writing it bare.

Useful? React with 👍 / 👎.

@AdaWorldAPI AdaWorldAPI merged commit bd62773 into main Jun 20, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants