AI briefing - data tables

Guide for AI assistants declaring tables, loading records, and rendering them on a page.

Who this is for

This briefs an AI assistant working with a site's data tables -- records the site owns, such as a product list or an events calendar. For page content see content authoring; for the operator-facing version of this material see Data tables.

You need the manage_data capability. If the *_data_* tools are not in your tool list, that is your grant, not a fault. The plugin also ships disabled -- an operator enables it on the Extension Manager page, and until they do, every call refuses and says so.

The shape of the work

  1. save_data_table -- declare the table
  2. migrate_data_table -- create or update the stored table
  3. save_data_row -- put records in
  4. a page variable -- render them

list_data_tables first, on any task that mentions stored records. Learn what exists rather than guessing a name.

Declaring a table

You cannot write the descriptor with write_file. It lives under lazysite/, which every general write channel refuses, so save_data_table is the door -- and it validates before it stores, which a file write could not.

title: Products
key: code
fields:
  code:
    type: text
    required: true
  price:
    type: decimal
    digits: 8
    places: 2

key: names the field that identifies a row. Leave it out and the store assigns an id. Types: text, integer, decimal, boolean, date, datetime, enum. A decimal must declare digits and places; an enum must declare values.

Call describe_data_table before writing rows. A write is refused if a value does not fit its declared type, and this is how you learn what fits.

Writing records

save_data_row inserts without a key and updates with one, touching only the fields you send.

Send decimals as strings. "120.00" keeps its trailing zeros; a JSON number would arrive as 120 and lose them. This matters for money and is the single most likely thing to get quietly wrong.

A refusal names the field and the reason. It is information, not a failure:

An empty value means absence. Clearing a number stores nothing rather than 0.

Reading on a page

tt_page_var:
  products: db:products sort=name asc limit=20

sort= takes a declared field name. The layout then iterates it exactly as it would a scan: list.

Do not add | html to a db value. Since SM786 the engine escapes every db: value where it enters the page, so a template writes [% p.title %] and gets escaped text. Whoever can write a row would otherwise control markup - and therefore script - on every page that displays that table, and the escape cannot be left to whoever writes the loop.

If a template written before this still carries | html, the value is escaped twice and the page shows ' where an apostrophe belongs. Remove the filter. The engine says so when it happens rather than leaving it to be found by eye: the site log names the table and the column, and says to remove the filter. Nothing breaks in the meantime - a double-escaped page is readable and wrong, not broken - which is why this is a warning and not a refusal.

There is no switch that turns the escaping off. db_render_raw did, shipped deprecated, and was removed in 0.13.13: a survey of fourteen sites and around 1,100 files found no column carrying authored HTML, and an upgrade deletes the key. A template that needs markup from a table renders it from a column the template trusts, not from data an editor types.

Who sees the rows: the public: key (SM476). A table is closed to anonymous visitors until its descriptor says public: true -- an unpublished table renders nothing for a visitor, on every page that binds it, and the data endpoint answers them nothing either. A signed-in account is different: public: is about anonymous visitors only, so an authenticated reader gets rows from an unpublished table unless a read list narrows it -- an ACL entry on lazysite/db/tables/<name> (users and @groups, same store and shape as a file ACL; a rule on lazysite/db/tables governs every table at once). That composition is what a gated application wants: leave the table unpublished, gate the pages, and add a read list when only named accounts may see the rows. public: true plus a read list still refuses the anonymous visitor -- a list that names accounts cannot name an anonymous one. If a bound page shows nothing and you expected rows, check public: and the read list before anything else; validate_page says which is closed.

If a read returns no rows and says pending_schema, the table is declared and not yet created -- run migrate_data_table. That is different from a table being empty, and the answer says which.

Changing a table

Add a field, add an index, add a default: edit the descriptor, save it, run migrate_data_table. Safe to re-run.

Change a type, make a field required, or remove one: migrate_data_table will refuse and tell you, because all three rewrite the table. Use rebuild_data_table when you mean it. It requires you to name each column whose data will be lost -- call it without confirm_lost first to be told which. Every row is exported first and the path is returned.

Ask the operator before rebuilding a table that holds real records. The migration refuses these by default precisely because they are not decisions for whoever happens to be editing the descriptor.

Checking your work

preview_public_page renders a page as an anonymous visitor receives it, under the Host that owns the path, and reports the layout and theme actually used. It is how to confirm a data-backed page renders -- what a page should look like is a question about configuration, and what a visitor got is a question about the response.

What tables are not for

Tables hold site data. Per-visitor state -- a session, a basket, somebody's profile -- is an application and is out of scope. If a task needs one of those, say so rather than modelling it as a table.

Do not put personal data in a table without asking. A site package can carry table contents to another organisation, and a directory of names is exactly the sort of thing an operator should decide about deliberately.